Skip to content

Commit 93a0c88

Browse files
committed
feat: allow control private traffic through ACL
1 parent 652c8cb commit 93a0c88

File tree

3 files changed

+128
-3
lines changed

3 files changed

+128
-3
lines changed

tuic-server/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ stream_timeout = "60s"
146146
147147
# Format 1: Array of tables format
148148
[[acl]]
149-
# Address: IPv4/IPv6, CIDR, domain, wildcard, or localhost
149+
# Address: IPv4/IPv6, CIDR, domain, wildcard, localhost, or private
150150
addr = "127.0.0.1"
151151
152152
# Ports: comma-separated list, can specify protocol (e.g. "udp/53,tcp/80,udp/10000-20000,443")
@@ -160,11 +160,19 @@ hijack = "1.1.1.1"
160160
addr = "localhost"
161161
outbound = "drop"
162162
163+
# You can also use 'private' to match all LAN/private IP addresses
164+
# This includes: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 (IPv4)
165+
# and fc00::/7, fe80::/10 (IPv6)
166+
[[acl]]
167+
addr = "private"
168+
outbound = "drop"
169+
163170
# Format 2: Multi-line string format (more concise)
164171
acl = '''
165172
# Format: <outbound_name> <address> [<ports>] [<hijack_address>]
166173
direct localhost tcp/80,tcp/443,udp/443
167174
drop localhost
175+
drop private
168176
default 8.8.4.4 udp/53 1.1.1.1
169177
'''
170178

tuic-server/src/acl.pest

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ outbound = @{ (ASCII_ALPHANUMERIC | "_" | "-")+ }
1414
// Address patterns
1515
address = {
1616
localhost_kw
17+
| private_kw
1718
| suffix_localhost
1819
| wildcard_domain
1920
| cidr
@@ -25,6 +26,7 @@ address = {
2526

2627
// Special keywords
2728
localhost_kw = { "localhost" }
29+
private_kw = { "private" }
2830
suffix_localhost = { "suffix:localhost" }
2931
any_addr = { "*" }
3032

tuic-server/src/acl.rs

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ pub enum AclAddress {
5252
/// Special localhost identifier
5353
#[display("localhost")]
5454
Localhost,
55+
/// Special private address identifier (LAN addresses)
56+
#[display("private")]
57+
Private,
5558
/// Match any address (when address is omitted)
5659
#[display("*")]
5760
Any,
@@ -125,6 +128,7 @@ impl AclRule {
125128
AclAddress::Domain(domain) => Self::match_domain(domain, ip),
126129
AclAddress::WildcardDomain(pattern) => Self::match_wildcard_domain(pattern, ip),
127130
AclAddress::Localhost => Self::is_loopback(ip),
131+
AclAddress::Private => Self::is_private(ip),
128132
AclAddress::Any => true,
129133
}
130134
}
@@ -174,6 +178,31 @@ impl AclRule {
174178
IpAddr::V6(v6) => v6.is_loopback(),
175179
}
176180
}
181+
182+
/// Check if an IP address is private (LAN address)
183+
#[inline]
184+
fn is_private(ip: IpAddr) -> bool {
185+
match ip {
186+
IpAddr::V4(ipv4) => {
187+
let octets = ipv4.octets();
188+
// 10.0.0.0/8
189+
octets[0] == 10
190+
// 172.16.0.0/12
191+
|| (octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31))
192+
// 192.168.0.0/16
193+
|| (octets[0] == 192 && octets[1] == 168)
194+
// 169.254.0.0/16 (Link-local)
195+
|| (octets[0] == 169 && octets[1] == 254)
196+
}
197+
IpAddr::V6(ipv6) => {
198+
let octets = ipv6.octets();
199+
// fc00::/7 (Unique Local Address)
200+
octets[0] & 0xfe == 0xfc
201+
// fe80::/10 (Link-local)
202+
|| (octets[0] == 0xfe && (octets[1] & 0xc0) == 0x80)
203+
}
204+
}
205+
}
177206
}
178207

179208
impl AclPortEntry {
@@ -251,6 +280,7 @@ fn parse_address_from_pair(pair: pest::iterators::Pair<Rule>) -> eyre::Result<Ac
251280

252281
Ok(match inner.as_rule() {
253282
Rule::localhost_kw | Rule::suffix_localhost => AclAddress::Localhost,
283+
Rule::private_kw => AclAddress::Private,
254284
Rule::any_addr => AclAddress::Any,
255285
Rule::wildcard_domain => AclAddress::WildcardDomain(inner.as_str().to_string()),
256286
Rule::cidr => AclAddress::Cidr(inner.as_str().to_string()),
@@ -623,6 +653,84 @@ mod tests {
623653
assert!(!rule.matching(v4("192.0.2.1", 0), 0, true));
624654
}
625655

656+
#[test]
657+
fn private_match_ipv4() {
658+
let rule = AclRule {
659+
addr: AclAddress::Private,
660+
ports: None,
661+
outbound: "default".to_string(),
662+
hijack: None,
663+
};
664+
665+
// Test 10.0.0.0/8 range
666+
assert!(rule.matching(v4("10.0.0.0", 0), 0, true));
667+
assert!(rule.matching(v4("10.0.0.1", 0), 0, true));
668+
assert!(rule.matching(v4("10.255.255.255", 0), 0, true));
669+
670+
// Test 172.16.0.0/12 range
671+
assert!(rule.matching(v4("172.16.0.0", 0), 0, true));
672+
assert!(rule.matching(v4("172.16.0.1", 0), 0, true));
673+
assert!(rule.matching(v4("172.31.255.255", 0), 0, true));
674+
assert!(!rule.matching(v4("172.15.255.255", 0), 0, true));
675+
assert!(!rule.matching(v4("172.32.0.0", 0), 0, true));
676+
677+
// Test 192.168.0.0/16 range
678+
assert!(rule.matching(v4("192.168.0.0", 0), 0, true));
679+
assert!(rule.matching(v4("192.168.1.1", 0), 0, true));
680+
assert!(rule.matching(v4("192.168.255.255", 0), 0, true));
681+
682+
// Test 169.254.0.0/16 range (Link-local)
683+
assert!(rule.matching(v4("169.254.0.0", 0), 0, true));
684+
assert!(rule.matching(v4("169.254.1.1", 0), 0, true));
685+
assert!(rule.matching(v4("169.254.255.255", 0), 0, true));
686+
687+
// Test public addresses (should not match)
688+
assert!(!rule.matching(v4("8.8.8.8", 0), 0, true));
689+
assert!(!rule.matching(v4("1.1.1.1", 0), 0, true));
690+
assert!(!rule.matching(v4("203.0.113.1", 0), 0, true));
691+
}
692+
693+
#[test]
694+
fn private_match_ipv6() {
695+
let rule = AclRule {
696+
addr: AclAddress::Private,
697+
ports: None,
698+
outbound: "default".to_string(),
699+
hijack: None,
700+
};
701+
702+
// Test fc00::/7 (Unique Local Address)
703+
assert!(rule.matching(v6("fc00::1", 0), 0, true));
704+
assert!(rule.matching(v6("fd00::1", 0), 0, true));
705+
assert!(rule.matching(v6("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 0), 0, true));
706+
707+
// Test fe80::/10 (Link-local)
708+
assert!(rule.matching(v6("fe80::1", 0), 0, true));
709+
assert!(rule.matching(v6("fe80::dead:beef", 0), 0, true));
710+
assert!(rule.matching(v6("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 0), 0, true));
711+
712+
// Test public addresses (should not match)
713+
assert!(!rule.matching(v6("2001:db8::1", 0), 0, true));
714+
assert!(!rule.matching(v6("2606:4700:4700::1111", 0), 0, true));
715+
}
716+
717+
#[test]
718+
fn parse_private_keyword() {
719+
let result = parse_acl_rule("allow private").unwrap();
720+
assert_eq!(result.outbound, "allow");
721+
assert_eq!(result.addr, AclAddress::Private);
722+
assert_eq!(result.ports, None);
723+
assert_eq!(result.hijack, None);
724+
}
725+
726+
#[test]
727+
fn parse_private_with_ports() {
728+
let result = parse_acl_rule("block private tcp/80,udp/53").unwrap();
729+
assert_eq!(result.outbound, "block");
730+
assert_eq!(result.addr, AclAddress::Private);
731+
assert!(result.ports.is_some());
732+
}
733+
626734
#[test]
627735
fn any_match() {
628736
let rule = AclRule {
@@ -1348,6 +1456,7 @@ acl = """
13481456
allow 192.168.1.0/24 tcp/443
13491457
deny *.ads.com
13501458
allow localhost
1459+
allow private
13511460
"""
13521461
"#;
13531462
#[derive(Deserialize)]
@@ -1357,10 +1466,11 @@ allow localhost
13571466
}
13581467

13591468
let config: Config = toml::from_str(toml)?;
1360-
assert_eq!(config.acl.len(), 3);
1469+
assert_eq!(config.acl.len(), 4);
13611470
assert_eq!(config.acl[0].outbound, "allow");
13621471
assert_eq!(config.acl[1].outbound, "deny");
13631472
assert_eq!(config.acl[2].outbound, "allow");
1473+
assert_eq!(config.acl[3].addr, AclAddress::Private);
13641474
Ok(())
13651475
}
13661476

@@ -1375,6 +1485,10 @@ ports = "tcp/443"
13751485
[[acl]]
13761486
outbound = "deny"
13771487
addr = "*.ads.com"
1488+
1489+
[[acl]]
1490+
outbound = "deny"
1491+
addr = "private"
13781492
"#;
13791493
#[derive(Deserialize)]
13801494
struct Config {
@@ -1383,9 +1497,10 @@ addr = "*.ads.com"
13831497
}
13841498

13851499
let config: Config = toml::from_str(toml)?;
1386-
assert_eq!(config.acl.len(), 2);
1500+
assert_eq!(config.acl.len(), 3);
13871501
assert_eq!(config.acl[0].outbound, "allow");
13881502
assert_eq!(config.acl[1].outbound, "deny");
1503+
assert_eq!(config.acl[2].addr, AclAddress::Private);
13891504
Ok(())
13901505
}
13911506
}

0 commit comments

Comments
 (0)