1- use ipnetwork:: Ipv4Network ;
1+ use ipnetwork:: IpNetwork ;
22use serde:: { Deserialize , Serialize } ;
33
44use crate :: version:: CURRENT_PROTOCOL_VERSION ;
55
66/// Maximum encoded message size (1 MiB) to prevent denial-of-service via oversized frames.
77pub const MAX_CONTROL_MESSAGE_SIZE : u32 = 1024 * 1024 ;
88
9+ /// A normalized DNS domain name (lowercase).
10+ #[ derive( Debug , Clone , Serialize , Deserialize , PartialEq , Eq ) ]
11+ #[ serde( transparent) ]
12+ pub struct DomainName ( String ) ;
13+
14+ impl DomainName {
15+ pub fn new ( domain : impl Into < String > ) -> Self {
16+ Self ( domain. into ( ) . to_ascii_lowercase ( ) )
17+ }
18+
19+ pub fn as_str ( & self ) -> & str {
20+ & self . 0
21+ }
22+
23+ /// Returns `true` if `hostname` matches this domain via DNS suffix matching.
24+ ///
25+ /// Matches if `hostname == domain` (exact) or `hostname` ends with `.domain`.
26+ pub fn matches_hostname ( & self , hostname : & str ) -> bool {
27+ let hostname = hostname. to_ascii_lowercase ( ) ;
28+ hostname == self . 0
29+ || ( hostname. len ( ) > self . 0 . len ( )
30+ && hostname. as_bytes ( ) [ hostname. len ( ) - self . 0 . len ( ) - 1 ] == b'.'
31+ && hostname. ends_with ( & self . 0 ) )
32+ }
33+ }
34+
35+ impl std:: fmt:: Display for DomainName {
36+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
37+ self . 0 . fmt ( f)
38+ }
39+ }
40+
941/// A DNS domain advertisement with its source.
1042#[ derive( Debug , Clone , Serialize , Deserialize , PartialEq , Eq ) ]
1143pub struct DomainAdvertisement {
1244 /// The DNS domain (e.g., "contoso.local").
13- pub domain : String ,
45+ pub domain : DomainName ,
1446 /// Whether this domain was auto-detected (`true`) or explicitly configured (`false`).
1547 pub auto_detected : bool ,
1648}
@@ -23,8 +55,8 @@ pub enum ControlMessage {
2355 protocol_version : u16 ,
2456 /// Monotonically increasing epoch within this agent process lifetime.
2557 epoch : u64 ,
26- /// Reachable IPv4 subnets .
27- subnets : Vec < Ipv4Network > ,
58+ /// Reachable subnets ( IPv4 and IPv6) .
59+ subnets : Vec < IpNetwork > ,
2860 /// DNS domains this agent can resolve, with source tracking.
2961 domains : Vec < DomainAdvertisement > ,
3062 } ,
@@ -48,7 +80,7 @@ pub enum ControlMessage {
4880
4981impl ControlMessage {
5082 /// Create a new RouteAdvertise with the current protocol version.
51- pub fn route_advertise ( epoch : u64 , subnets : Vec < Ipv4Network > , domains : Vec < DomainAdvertisement > ) -> Self {
83+ pub fn route_advertise ( epoch : u64 , subnets : Vec < IpNetwork > , domains : Vec < DomainAdvertisement > ) -> Self {
5284 Self :: RouteAdvertise {
5385 protocol_version : CURRENT_PROTOCOL_VERSION ,
5486 epoch,
@@ -118,11 +150,11 @@ mod tests {
118150 vec ! [ "10.0.0.0/8" . parse( ) . expect( "valid CIDR" ) ] ,
119151 vec ! [
120152 DomainAdvertisement {
121- domain: "contoso.local" . to_owned ( ) ,
153+ domain: DomainName :: new ( "contoso.local" ) ,
122154 auto_detected: false ,
123155 } ,
124156 DomainAdvertisement {
125- domain: "finance.contoso.local" . to_owned ( ) ,
157+ domain: DomainName :: new ( "finance.contoso.local" ) ,
126158 auto_detected: true ,
127159 } ,
128160 ] ,
@@ -134,9 +166,9 @@ mod tests {
134166 match & decoded {
135167 ControlMessage :: RouteAdvertise { domains, .. } => {
136168 assert_eq ! ( domains. len( ) , 2 ) ;
137- assert_eq ! ( domains[ 0 ] . domain, "contoso.local" ) ;
169+ assert_eq ! ( domains[ 0 ] . domain. as_str ( ) , "contoso.local" ) ;
138170 assert ! ( !domains[ 0 ] . auto_detected) ;
139- assert_eq ! ( domains[ 1 ] . domain, "finance.contoso.local" ) ;
171+ assert_eq ! ( domains[ 1 ] . domain. as_str ( ) , "finance.contoso.local" ) ;
140172 assert ! ( domains[ 1 ] . auto_detected) ;
141173 }
142174 _ => panic ! ( "expected RouteAdvertise" ) ,
@@ -180,25 +212,29 @@ mod proptests {
180212 use crate :: stream:: ControlStream ;
181213 use crate :: version:: CURRENT_PROTOCOL_VERSION ;
182214
183- fn arb_ipv4_network ( ) -> impl Strategy < Value = Ipv4Network > {
215+ fn arb_ip_network ( ) -> impl Strategy < Value = IpNetwork > {
184216 ( any :: < [ u8 ; 4 ] > ( ) , 0u8 ..=32 ) . prop_map ( |( octets, prefix) | {
185217 let ip = std:: net:: Ipv4Addr :: from ( octets) ;
186- Ipv4Network :: new ( ip, prefix)
187- . map ( |n| Ipv4Network :: new ( n. network ( ) , prefix) . expect ( "normalized network should be valid" ) )
188- . unwrap_or_else ( |_| Ipv4Network :: new ( std:: net:: Ipv4Addr :: UNSPECIFIED , 0 ) . expect ( "0.0.0.0/0 is valid" ) )
218+ ipnetwork:: Ipv4Network :: new ( ip, prefix)
219+ . map ( |n| IpNetwork :: V4 ( ipnetwork:: Ipv4Network :: new ( n. network ( ) , prefix) . expect ( "normalized" ) ) )
220+ . unwrap_or_else ( |_| {
221+ IpNetwork :: V4 ( ipnetwork:: Ipv4Network :: new ( std:: net:: Ipv4Addr :: UNSPECIFIED , 0 ) . expect ( "0.0.0.0/0" ) )
222+ } )
189223 } )
190224 }
191225
192226 fn arb_domain_advertisement ( ) -> impl Strategy < Value = DomainAdvertisement > {
193- ( "[a-z]{3,10}\\ .[a-z]{2,5}" , any :: < bool > ( ) )
194- . prop_map ( |( domain, auto_detected) | DomainAdvertisement { domain, auto_detected } )
227+ ( "[a-z]{3,10}\\ .[a-z]{2,5}" , any :: < bool > ( ) ) . prop_map ( |( domain, auto_detected) | DomainAdvertisement {
228+ domain : DomainName :: new ( domain) ,
229+ auto_detected,
230+ } )
195231 }
196232
197233 fn arb_control_message ( ) -> impl Strategy < Value = ControlMessage > {
198234 prop_oneof ! [
199235 (
200236 any:: <u64 >( ) ,
201- proptest:: collection:: vec( arb_ipv4_network ( ) , 0 ..50 ) ,
237+ proptest:: collection:: vec( arb_ip_network ( ) , 0 ..50 ) ,
202238 proptest:: collection:: vec( arb_domain_advertisement( ) , 0 ..5 ) ,
203239 )
204240 . prop_map( |( epoch, subnets, domains) | {
0 commit comments