mirror of
https://github.com/TrustTunnel/TrustTunnel.git
synced 2026-04-26 04:26:26 +00:00
Pull request 169: Support mask part in client_random_prefix in config export
Squashed commit of the following: commit 0baedf77eb1d6aa39a02c058a13d6d824d5db6a0 Author: Zhavoronkov Aleksei <a.zhavoronkov@adguard.com> Date: Fri Feb 27 15:15:11 2026 +0300 Update client_random_prefix info in spec commit a689720b3cf218ba8095285231a44c495fba14e7 Author: Zhavoronkov Aleksei <a.zhavoronkov@adguard.com> Date: Fri Feb 27 11:12:16 2026 +0300 Support mask part in client_random_prefix in config export
This commit is contained in:
26
DEEP_LINK.md
26
DEEP_LINK.md
@@ -63,19 +63,19 @@ in one or two bytes.
|
||||
|
||||
### Field Tags
|
||||
|
||||
| Tag | Field | Value encoding | Required |
|
||||
| --- | ----- | -------------- | -------- |
|
||||
| `0x01` | `hostname` | UTF-8 string | yes |
|
||||
| `0x02` | `addresses` | UTF-8, one `address:port` per entry; multiple entries are encoded as separate TLVs with the same tag | yes |
|
||||
| `0x03` | `custom_sni` | UTF-8 string | no |
|
||||
| `0x04` | `has_ipv6` | 1 byte: `0x01` = true, `0x00` = false | no (default `true`) |
|
||||
| `0x05` | `username` | UTF-8 string | yes |
|
||||
| `0x06` | `password` | UTF-8 string | yes |
|
||||
| `0x0B` | `client_random_prefix` | UTF-8 hex-encoded string | no |
|
||||
| `0x07` | `skip_verification` | 1 byte: `0x01` = true, `0x00` = false | no (default `false`) |
|
||||
| `0x08` | `certificate` | Concatenated DER-encoded certificates (raw binary); omit if the chain is verified by system CAs | no |
|
||||
| `0x09` | `upstream_protocol` | 1 byte: `0x01` = `http2`, `0x02` = `http3` | no (default `http2`) |
|
||||
| `0x0A` | `anti_dpi` | 1 byte: `0x01` = true, `0x00` = false | no (default `false`) |
|
||||
| Tag | Field | Value encoding | Required |
|
||||
|--------|------------------------|------------------------------------------------------------------------------------------------------|----------------------|
|
||||
| `0x01` | `hostname` | UTF-8 string | yes |
|
||||
| `0x02` | `addresses` | UTF-8, one `address:port` per entry; multiple entries are encoded as separate TLVs with the same tag | yes |
|
||||
| `0x03` | `custom_sni` | UTF-8 string | no |
|
||||
| `0x04` | `has_ipv6` | 1 byte: `0x01` = true, `0x00` = false | no (default `true`) |
|
||||
| `0x05` | `username` | UTF-8 string | yes |
|
||||
| `0x06` | `password` | UTF-8 string | yes |
|
||||
| `0x0B` | `client_random_prefix` | UTF-8 hex-encoded string in the following format: `prefix[/mask]` | no |
|
||||
| `0x07` | `skip_verification` | 1 byte: `0x01` = true, `0x00` = false | no (default `false`) |
|
||||
| `0x08` | `certificate` | Concatenated DER-encoded certificates (raw binary); omit if the chain is verified by system CAs | no |
|
||||
| `0x09` | `upstream_protocol` | 1 byte: `0x01` = `http2`, `0x02` = `http3` | no (default `http2`) |
|
||||
| `0x0A` | `anti_dpi` | 1 byte: `0x01` = true, `0x00` = false | no (default `false`) |
|
||||
|
||||
### Encoding Rules
|
||||
|
||||
|
||||
@@ -144,12 +144,19 @@ pub fn decode_tlv_payload(payload: &[u8]) -> Result<DeepLinkConfig> {
|
||||
TlvTag::ClientRandomPrefix => {
|
||||
let prefix = decode_string(&value)?;
|
||||
// Validate hex format
|
||||
hex::decode(&prefix).map_err(|e| {
|
||||
let (prefix_part, mask_part) = prefix.split_once('/').unwrap_or((&prefix, ""));
|
||||
hex::decode(prefix_part).map_err(|e| {
|
||||
DeepLinkError::InvalidAddress(format!(
|
||||
"client_random_prefix must be valid hex: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
hex::decode(mask_part).map_err(|e| {
|
||||
DeepLinkError::InvalidAddress(format!(
|
||||
"client_random_prefix mask must be valid hex: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
client_random_prefix = Some(prefix);
|
||||
}
|
||||
}
|
||||
@@ -281,4 +288,16 @@ mod tests {
|
||||
let result = decode("http://example.com");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_client_random_with_mask() {
|
||||
let data = vec![
|
||||
0x0B, 0x09, b'5', b'8', b'4', b'1', b'/', b'7', b'a', b'4', b'3',
|
||||
];
|
||||
let mut parser = TlvParser::new(&data);
|
||||
|
||||
let (tag, value) = parser.next_field().unwrap().unwrap();
|
||||
assert_eq!(tag, Some(TlvTag::ClientRandomPrefix));
|
||||
assert_eq!(value, b"5841/7a43");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,35 +245,85 @@ fn main() {
|
||||
.get_one::<String>(CLIENT_RANDOM_PREFIX_PARAM_NAME)
|
||||
.cloned();
|
||||
if let Some(ref prefix) = client_random_prefix {
|
||||
let has_slash = prefix.contains('/');
|
||||
let (input_prefix, input_mask) = prefix.split_once('/').unwrap_or((prefix, ""));
|
||||
|
||||
// Validate hex format
|
||||
if hex::decode(prefix).is_err() {
|
||||
if hex::decode(input_prefix).is_err() {
|
||||
eprintln!("Error: client_random_prefix '{}' is not valid hex", prefix);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if (has_slash && input_mask.is_empty())
|
||||
|| (!input_mask.is_empty() && hex::decode(input_mask).is_err())
|
||||
{
|
||||
eprintln!(
|
||||
"Error: client_random_prefix mask '{}' is not valid hex",
|
||||
input_mask
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Validate against rules.toml
|
||||
if let Some(rules_engine) = settings.get_rules_engine() {
|
||||
let has_matching_rule = rules_engine.config().rule.iter().any(|rule| {
|
||||
let input_mask: Option<&str> = if input_mask.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(input_mask)
|
||||
};
|
||||
|
||||
let matching_rule = rules_engine.config().rule.iter().find(|rule| {
|
||||
rule.client_random_prefix
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
// Handle both "prefix" and "prefix/mask" formats
|
||||
if let Some(slash) = p.find('/') {
|
||||
&p[..slash] == prefix
|
||||
} else {
|
||||
p == prefix
|
||||
let (rule_prefix, rule_mask): (&str, Option<&str>) = p
|
||||
.split_once('/')
|
||||
.map(|(a, b)| (a, Some(b)))
|
||||
.unwrap_or((p.as_str(), None));
|
||||
|
||||
// Prefix parts must be equal
|
||||
if rule_prefix != input_prefix {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mask compatibility: input mask must be same or stronger than rule mask.
|
||||
// "Stronger" means more bits set, i.e. (input_mask & rule_mask) == rule_mask.
|
||||
match (input_mask, rule_mask) {
|
||||
// Rule has no mask, any input mask is at least as strong
|
||||
(_, None) => true,
|
||||
// Input has no mask, strongest possible
|
||||
(None, Some(_)) => true,
|
||||
// Both have masks, input mask must cover all bits of rule mask
|
||||
(Some(mi_str), Some(mr_str)) => {
|
||||
match (hex::decode(mi_str), hex::decode(mr_str)) {
|
||||
(Ok(mi), Ok(mr)) => {
|
||||
mi.len() >= mr.len()
|
||||
&& (0..mr.len()).all(|i| mi[i] & mr[i] == mr[i])
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
// Print warning and continue, do not panic because it's optional field
|
||||
if !has_matching_rule {
|
||||
eprintln!(
|
||||
"Warning: No rule found in rules.toml matching client_random_prefix '{}'. This field will be ignored.",
|
||||
prefix
|
||||
);
|
||||
client_random_prefix = None;
|
||||
match matching_rule {
|
||||
None => {
|
||||
eprintln!(
|
||||
"Warning: No rule found in rules.toml matching client_random_prefix '{}'. This field will be ignored.",
|
||||
prefix
|
||||
);
|
||||
client_random_prefix = None;
|
||||
}
|
||||
Some(rule) if rule.action == trusttunnel::rules::RuleAction::Deny => {
|
||||
eprintln!(
|
||||
"Warning: Matched rule in rules.toml for client_random_prefix '{}' has action 'deny'.",
|
||||
prefix
|
||||
);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user