1- use super :: { ClashResolver , Client , runtime:: DnsRuntimeProvider } ;
1+ use super :: { ClashResolver , Client , EdnsClientSubnet , runtime:: DnsRuntimeProvider } ;
22use std:: {
33 fmt:: { Debug , Display , Formatter } ,
44 net,
@@ -11,7 +11,13 @@ use async_trait::async_trait;
1111
1212use hickory_client:: client;
1313use hickory_proto:: {
14- ProtoError , rustls:: tls_client_connect, tcp:: TcpClientStream ,
14+ ProtoError ,
15+ rr:: {
16+ RecordType ,
17+ rdata:: opt:: { ClientSubnet , EdnsCode , EdnsOption } ,
18+ } ,
19+ rustls:: tls_client_connect,
20+ tcp:: TcpClientStream ,
1521 udp:: UdpClientStream ,
1622} ;
1723use rustls:: ClientConfig ;
@@ -57,6 +63,133 @@ impl Display for DNSNetMode {
5763 }
5864}
5965
66+ #[ cfg( test) ]
67+ mod tests {
68+ use super :: * ;
69+ use crate :: proxy;
70+ use hickory_proto:: {
71+ op,
72+ rr:: { Name , rdata:: opt:: EdnsOption } ,
73+ } ;
74+ use std:: str:: FromStr ;
75+
76+ fn client_with_ecs ( ecs : Option < EdnsClientSubnet > ) -> DnsClient {
77+ let proxy = Arc :: new ( proxy:: direct:: Handler :: new ( "test-proxy" ) ) ;
78+ let addr = net:: SocketAddr :: new ( net:: IpAddr :: from ( [ 127 , 0 , 0 , 1 ] ) , 53 ) ;
79+ DnsClient {
80+ inner : Arc :: new ( RwLock :: new ( Inner {
81+ c : None ,
82+ bg_handle : None ,
83+ } ) ) ,
84+ cfg : DnsConfig :: Udp ( addr, None , proxy. clone ( ) ) ,
85+ proxy,
86+ host : "example.org" . to_string ( ) ,
87+ port : 53 ,
88+ net : DNSNetMode :: Udp ,
89+ iface : None ,
90+ ecs,
91+ }
92+ }
93+
94+ fn build_message ( record_type : RecordType ) -> Message {
95+ let mut msg = Message :: new ( ) ;
96+ let mut query = op:: Query :: new ( ) ;
97+ query. set_name ( Name :: from_ascii ( "example.org" ) . expect ( "valid name" ) ) ;
98+ query. set_query_type ( record_type) ;
99+ msg. add_query ( query) ;
100+ msg
101+ }
102+
103+ #[ test]
104+ fn apply_edns_client_subnet_adds_ipv4_option ( ) {
105+ let ecs = EdnsClientSubnet {
106+ ipv4 : Some ( "1.2.3.4/24" . parse ( ) . unwrap ( ) ) ,
107+ ipv6 : None ,
108+ } ;
109+ let client = client_with_ecs ( Some ( ecs) ) ;
110+ let mut msg = build_message ( RecordType :: A ) ;
111+
112+ client. apply_edns_client_subnet ( & mut msg) ;
113+
114+ let edns = msg. extensions ( ) . as_ref ( ) . expect ( "edns should exist" ) ;
115+ let option = edns
116+ . option ( EdnsCode :: Subnet )
117+ . expect ( "subnet option missing" ) ;
118+ match option {
119+ EdnsOption :: Subnet ( subnet) => {
120+ assert_eq ! ( subnet. addr( ) , net:: IpAddr :: from( [ 1 , 2 , 3 , 0 ] ) ) ;
121+ assert_eq ! ( subnet. source_prefix( ) , 24 ) ;
122+ assert_eq ! ( subnet. scope_prefix( ) , 24 ) ;
123+ }
124+ _ => panic ! ( "unexpected edns option" ) ,
125+ }
126+ }
127+
128+ #[ test]
129+ fn apply_edns_client_subnet_prefers_ipv6_for_aaaa ( ) {
130+ let ecs = EdnsClientSubnet {
131+ ipv4 : Some ( "1.2.3.4/24" . parse ( ) . unwrap ( ) ) ,
132+ ipv6 : Some ( "2001:db8::/48" . parse ( ) . unwrap ( ) ) ,
133+ } ;
134+ let client = client_with_ecs ( Some ( ecs) ) ;
135+ let mut msg = build_message ( RecordType :: AAAA ) ;
136+
137+ client. apply_edns_client_subnet ( & mut msg) ;
138+
139+ let edns = msg. extensions ( ) . as_ref ( ) . expect ( "edns should exist" ) ;
140+ let option = edns
141+ . option ( EdnsCode :: Subnet )
142+ . expect ( "subnet option missing" ) ;
143+ match option {
144+ EdnsOption :: Subnet ( subnet) => {
145+ assert_eq ! (
146+ subnet. addr( ) ,
147+ net:: IpAddr :: from_str( "2001:db8::" ) . unwrap( )
148+ ) ;
149+ assert_eq ! ( subnet. source_prefix( ) , 48 ) ;
150+ assert_eq ! ( subnet. scope_prefix( ) , 48 ) ;
151+ }
152+ _ => panic ! ( "unexpected edns option" ) ,
153+ }
154+ }
155+
156+ #[ test]
157+ fn apply_edns_client_subnet_respects_existing_option ( ) {
158+ let ecs = EdnsClientSubnet {
159+ ipv4 : Some ( "1.2.3.4/24" . parse ( ) . unwrap ( ) ) ,
160+ ipv6 : None ,
161+ } ;
162+ let client = client_with_ecs ( Some ( ecs) ) ;
163+ let mut msg = build_message ( RecordType :: A ) ;
164+
165+ let mut edns = hickory_proto:: op:: Edns :: new ( ) ;
166+ {
167+ let opts = edns. options_mut ( ) ;
168+ opts. insert ( EdnsOption :: Subnet ( ClientSubnet :: new (
169+ net:: IpAddr :: from ( [ 9 , 8 , 7 , 0 ] ) ,
170+ 24 ,
171+ 24 ,
172+ ) ) ) ;
173+ }
174+ msg. set_edns ( edns) ;
175+
176+ client. apply_edns_client_subnet ( & mut msg) ;
177+
178+ let edns = msg. extensions ( ) . as_ref ( ) . expect ( "edns should remain" ) ;
179+ let option = edns
180+ . option ( EdnsCode :: Subnet )
181+ . expect ( "subnet option missing" ) ;
182+ match option {
183+ EdnsOption :: Subnet ( subnet) => {
184+ assert_eq ! ( subnet. addr( ) , net:: IpAddr :: from( [ 9 , 8 , 7 , 0 ] ) ) ;
185+ assert_eq ! ( subnet. source_prefix( ) , 24 ) ;
186+ assert_eq ! ( subnet. scope_prefix( ) , 24 ) ;
187+ }
188+ _ => panic ! ( "unexpected edns option" ) ,
189+ }
190+ }
191+ }
192+
60193impl FromStr for DNSNetMode {
61194 type Err = Error ;
62195
@@ -80,6 +213,7 @@ pub struct Opts {
80213 pub net : DNSNetMode ,
81214 pub iface : Option < OutboundInterface > ,
82215 pub proxy : Arc < dyn OutboundHandler > ,
216+ pub ecs : Option < EdnsClientSubnet > ,
83217}
84218
85219enum DnsConfig {
@@ -163,6 +297,7 @@ pub struct DnsClient {
163297 port : u16 ,
164298 net : DNSNetMode ,
165299 iface : Option < OutboundInterface > ,
300+ ecs : Option < EdnsClientSubnet > ,
166301}
167302
168303impl DnsClient {
@@ -216,6 +351,7 @@ impl DnsClient {
216351 port : opts. port ,
217352 net : opts. net ,
218353 iface : opts. iface ,
354+ ecs : opts. ecs . clone ( ) ,
219355 } ) )
220356 }
221357 DNSNetMode :: Tcp => {
@@ -237,6 +373,7 @@ impl DnsClient {
237373 port : opts. port ,
238374 net : opts. net ,
239375 iface : opts. iface ,
376+ ecs : opts. ecs . clone ( ) ,
240377 } ) )
241378 }
242379 DNSNetMode :: DoT => {
@@ -259,6 +396,7 @@ impl DnsClient {
259396 port : opts. port ,
260397 net : opts. net ,
261398 iface : opts. iface ,
399+ ecs : opts. ecs . clone ( ) ,
262400 } ) )
263401 }
264402 DNSNetMode :: DoH => {
@@ -281,13 +419,67 @@ impl DnsClient {
281419 port : opts. port ,
282420 net : opts. net ,
283421 iface : opts. iface ,
422+ ecs : opts. ecs . clone ( ) ,
284423 } ) )
285424 }
286425 _ => unreachable ! ( "." ) ,
287426 }
288427 }
289428 }
290429 }
430+
431+ fn apply_edns_client_subnet ( & self , message : & mut Message ) {
432+ let Some ( ecs) = & self . ecs else {
433+ return ;
434+ } ;
435+
436+ if ecs. ipv4 . is_none ( ) && ecs. ipv6 . is_none ( ) {
437+ return ;
438+ }
439+
440+ if message
441+ . extensions ( )
442+ . as_ref ( )
443+ . is_some_and ( |edns| edns. option ( EdnsCode :: Subnet ) . is_some ( ) )
444+ {
445+ return ;
446+ }
447+
448+ let prefer_ipv6 = matches ! (
449+ message. query( ) . map( |q| q. query_type( ) ) ,
450+ Some ( RecordType :: AAAA )
451+ ) ;
452+
453+ let candidate = if prefer_ipv6 {
454+ ecs. ipv6
455+ . map ( |ipv6| ( net:: IpAddr :: from ( ipv6. network ( ) ) , ipv6. prefix_len ( ) ) )
456+ . or_else ( || {
457+ ecs. ipv4 . map ( |ipv4| {
458+ ( net:: IpAddr :: from ( ipv4. network ( ) ) , ipv4. prefix_len ( ) )
459+ } )
460+ } )
461+ } else {
462+ ecs. ipv4
463+ . map ( |ipv4| ( net:: IpAddr :: from ( ipv4. network ( ) ) , ipv4. prefix_len ( ) ) )
464+ . or_else ( || {
465+ ecs. ipv6 . map ( |ipv6| {
466+ ( net:: IpAddr :: from ( ipv6. network ( ) ) , ipv6. prefix_len ( ) )
467+ } )
468+ } )
469+ } ;
470+
471+ let Some ( ( addr, prefix) ) = candidate else {
472+ return ;
473+ } ;
474+
475+ let edns = message
476+ . extensions_mut ( )
477+ . get_or_insert_with ( hickory_proto:: op:: Edns :: new) ;
478+
479+ let options = edns. options_mut ( ) ;
480+ options. remove ( EdnsCode :: Subnet ) ;
481+ options. insert ( EdnsOption :: Subnet ( ClientSubnet :: new ( addr, prefix, prefix) ) ) ;
482+ }
291483}
292484
293485impl Debug for DnsClient {
@@ -345,7 +537,10 @@ impl Client for DnsClient {
345537 }
346538 }
347539
348- let mut req = DnsRequest :: new ( msg. clone ( ) , DnsRequestOptions :: default ( ) ) ;
540+ let mut outbound = msg. clone ( ) ;
541+ self . apply_edns_client_subnet ( & mut outbound) ;
542+
543+ let mut req = DnsRequest :: new ( outbound, DnsRequestOptions :: default ( ) ) ;
349544 if req. id ( ) == 0 {
350545 req. set_id ( rand:: random :: < u16 > ( ) ) ;
351546 }
0 commit comments