@@ -31,6 +31,88 @@ use crate::utils;
3131const COOKIE_FILE_SUFFIX : & str = "cookies.json" ;
3232const USER_AGENT : & str = "CorpLink/201000 (GooglePixel; Android 10; en)" ;
3333
34+ fn merge_additional_routes (
35+ mut routes : Vec < String > ,
36+ additional_routes : & [ String ] ,
37+ has_ipv6_address : bool ,
38+ ) -> Vec < String > {
39+ for route in additional_routes {
40+ if !crate :: utils:: is_valid_cidr ( route) {
41+ log:: warn!( "ignoring invalid vpn_additional_routes CIDR: {:?}" , route) ;
42+ continue ;
43+ }
44+ if !has_ipv6_address && route. contains ( ':' ) {
45+ log:: info!(
46+ "ignoring additional IPv6 route {:?} because the server did not assign an IPv6 address" ,
47+ route
48+ ) ;
49+ continue ;
50+ }
51+ if !routes. contains ( route) {
52+ routes. push ( route. clone ( ) ) ;
53+ }
54+ }
55+ routes
56+ }
57+
58+ async fn resolve_additional_domains (
59+ domains : & [ String ] ,
60+ has_ipv6_address : bool ,
61+ ) -> Vec < String > {
62+ let mut routes = Vec :: new ( ) ;
63+ for configured_domain in domains {
64+ let domain = configured_domain. trim ( ) ;
65+ if domain. is_empty ( ) {
66+ log:: warn!( "ignoring empty vpn_additional_domains entry" ) ;
67+ continue ;
68+ }
69+
70+ match tokio:: net:: lookup_host ( ( domain, 0 ) ) . await {
71+ Ok ( addresses) => {
72+ let mut domain_routes = Vec :: new ( ) ;
73+ for address in addresses {
74+ let ip = address. ip ( ) ;
75+ if ip. is_ipv6 ( ) && !has_ipv6_address {
76+ continue ;
77+ }
78+ let route = match ip {
79+ std:: net:: IpAddr :: V4 ( _) => format ! ( "{ip}/32" ) ,
80+ std:: net:: IpAddr :: V6 ( _) => format ! ( "{ip}/128" ) ,
81+ } ;
82+ if !domain_routes. contains ( & route) {
83+ domain_routes. push ( route) ;
84+ }
85+ }
86+ if domain_routes. is_empty ( ) {
87+ log:: warn!(
88+ "vpn_additional_domains entry {:?} returned no usable addresses" ,
89+ domain
90+ ) ;
91+ } else {
92+ log:: info!(
93+ "resolved additional VPN domain {:?} to {:?}" ,
94+ domain,
95+ domain_routes
96+ ) ;
97+ }
98+ for route in domain_routes {
99+ if !routes. contains ( & route) {
100+ routes. push ( route) ;
101+ }
102+ }
103+ }
104+ Err ( err) => {
105+ log:: warn!(
106+ "failed to resolve vpn_additional_domains entry {:?}: {}" ,
107+ domain,
108+ err
109+ ) ;
110+ }
111+ }
112+ }
113+ routes
114+ }
115+
34116#[ derive( Clone ) ]
35117pub struct Client {
36118 conf : Config ,
@@ -919,6 +1001,7 @@ impl Client {
9191001 let address6 = ( !wg_info. ipv6 . is_empty ( ) )
9201002 . then_some ( format ! ( "{}/128" , wg_info. ipv6) )
9211003 . unwrap_or ( "" . into ( ) ) ;
1004+ let has_ipv6_address = !address6. is_empty ( ) ;
9221005 let mut allowed_ips = match self . conf . route_mode . clone ( ) . unwrap_or_default ( ) {
9231006 crate :: config:: RouteMode :: Split => {
9241007 log:: info!( "route_mode = split" ) ;
@@ -952,9 +1035,32 @@ impl Client {
9521035 }
9531036 } ;
9541037
955- // Restrict server routes to the optional whitelist, then carve out the
956- // optional denylist. A configured empty whitelist intentionally yields
957- // no AllowedIPs/routes; invalid whitelist entries fail closed.
1038+ let mut additional_routes = self
1039+ . conf
1040+ . vpn_additional_routes
1041+ . clone ( )
1042+ . unwrap_or_default ( ) ;
1043+ if let Some ( domains) = self . conf . vpn_additional_domains . as_deref ( ) {
1044+ additional_routes
1045+ . extend ( resolve_additional_domains ( domains, has_ipv6_address) . await ) ;
1046+ }
1047+ if !additional_routes. is_empty ( ) {
1048+ let before = allowed_ips. len ( ) ;
1049+ allowed_ips = merge_additional_routes (
1050+ allowed_ips,
1051+ & additional_routes,
1052+ has_ipv6_address,
1053+ ) ;
1054+ log:: info!(
1055+ "additional VPN routes merged: {} -> {} entries" ,
1056+ before,
1057+ allowed_ips. len( )
1058+ ) ;
1059+ }
1060+
1061+ // Restrict server and user-added routes to the optional whitelist, then
1062+ // carve out the optional denylist. A configured empty whitelist
1063+ // intentionally yields no AllowedIPs/routes; invalid entries fail closed.
9581064 if let Some ( allowed) = self . conf . vpn_allowed_routes . as_deref ( ) {
9591065 for route in allowed {
9601066 if !crate :: utils:: is_valid_cidr ( route) {
@@ -1151,3 +1257,58 @@ impl Client {
11511257 Ok ( ( ) )
11521258 }
11531259}
1260+
1261+ #[ cfg( test) ]
1262+ mod tests {
1263+ use super :: { merge_additional_routes, resolve_additional_domains} ;
1264+ use crate :: utils:: apply_route_filters;
1265+
1266+ #[ test]
1267+ fn additional_routes_are_validated_deduplicated_and_merged ( ) {
1268+ let routes = merge_additional_routes (
1269+ vec ! [ "10.0.0.0/8" . to_string( ) ] ,
1270+ & [
1271+ "10.0.0.0/8" . to_string ( ) ,
1272+ "20.205.243.160/28" . to_string ( ) ,
1273+ "invalid" . to_string ( ) ,
1274+ "2001:db8::/32" . to_string ( ) ,
1275+ ] ,
1276+ false ,
1277+ ) ;
1278+
1279+ assert_eq ! ( routes, vec![ "10.0.0.0/8" , "20.205.243.160/28" ] ) ;
1280+ }
1281+
1282+ #[ test]
1283+ fn additional_ipv6_routes_are_kept_with_an_ipv6_address ( ) {
1284+ let routes = merge_additional_routes (
1285+ Vec :: new ( ) ,
1286+ & [ "2001:db8::/32" . to_string ( ) ] ,
1287+ true ,
1288+ ) ;
1289+
1290+ assert_eq ! ( routes, vec![ "2001:db8::/32" ] ) ;
1291+ }
1292+
1293+ #[ test]
1294+ fn additional_routes_are_merged_before_route_filters ( ) {
1295+ let routes = merge_additional_routes (
1296+ vec ! [ "10.0.0.0/8" . to_string( ) ] ,
1297+ & [ "20.205.243.160/28" . to_string ( ) ] ,
1298+ false ,
1299+ ) ;
1300+ let allowed = [ "20.205.243.160/28" . to_string ( ) ] ;
1301+
1302+ assert_eq ! (
1303+ apply_route_filters( & routes, Some ( & allowed) , None ) ,
1304+ vec![ "20.205.243.160/28" ]
1305+ ) ;
1306+ }
1307+
1308+ #[ tokio:: test]
1309+ async fn additional_domains_are_resolved_to_host_routes ( ) {
1310+ let routes = resolve_additional_domains ( & [ "127.0.0.1" . to_string ( ) ] , false ) . await ;
1311+
1312+ assert_eq ! ( routes, vec![ "127.0.0.1/32" ] ) ;
1313+ }
1314+ }
0 commit comments