@@ -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
179208impl 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 = """
13481456allow 192.168.1.0/24 tcp/443
13491457deny *.ads.com
13501458allow 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]]
13761486outbound = "deny"
13771487addr = "*.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