Skip to main content

rama_net/address/ip/
canonical.rs

1use core::net::{IpAddr, SocketAddr};
2
3use crate::address::{Authority, Host, HostWithOptPort, HostWithPort, ProxyAddress, SocketAddress};
4
5/// Converts an IP address into a canonical representation.
6///
7/// Canonical means:
8/// - IPv4 stays IPv4.
9/// - IPv6 stays IPv6, except for IPv4-mapped addresses (`::ffff:a.b.c.d`,
10///   [RFC 4291, Section 2.5.5.2]), which convert to the embedded IPv4 address.
11///
12/// IPv4-mapped addresses represent IPv4 connectivity exposed through the
13/// IPv6 socket API of dual-stack hosts ([RFC 4038, Section 4.2]); they never
14/// appear on the wire as IPv6 and are not routable as IPv6 ([IANA registry]).
15/// Connecting to one is therefore always IPv4 wire traffic, and dialing the
16/// embedded IPv4 address directly is the only form that also works on hosts
17/// without (working) dual-stack IPv6 sockets — e.g. Windows, where sockets
18/// default to `IPV6_V6ONLY`.
19///
20/// [RFC 4291, Section 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2
21/// [RFC 4038, Section 4.2]: https://datatracker.ietf.org/doc/html/rfc4038#section-4.2
22/// [IANA registry]: https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
23pub trait IntoCanonicalIpAddr {
24    #[must_use]
25    fn into_canonical_ip_addr(self) -> Self;
26}
27
28impl IntoCanonicalIpAddr for IpAddr {
29    #[inline(always)]
30    fn into_canonical_ip_addr(self) -> Self {
31        self.to_canonical()
32    }
33}
34
35impl IntoCanonicalIpAddr for SocketAddress {
36    #[inline(always)]
37    fn into_canonical_ip_addr(mut self) -> Self {
38        self.ip_addr = self.ip_addr.into_canonical_ip_addr();
39        self
40    }
41}
42
43impl IntoCanonicalIpAddr for SocketAddr {
44    #[inline(always)]
45    fn into_canonical_ip_addr(self) -> Self {
46        let ip_addr = self.ip().into_canonical_ip_addr();
47        Self::new(ip_addr, self.port())
48    }
49}
50
51impl IntoCanonicalIpAddr for Host {
52    fn into_canonical_ip_addr(self) -> Self {
53        // Bridge `Uninterpreted` to `IpAddr` before canonicalising —
54        // a pct-encoded IPv4-mapped IPv6 (`%3A%3Affff%3A192.0.2.1`)
55        // would otherwise pass through unchanged and miss the v4
56        // fold-down. Non-promotable inputs (Name, sub-delim Uninterpreted,
57        // IPvFuture) keep their original shape.
58        match self.try_as_ip() {
59            Ok(ip) => Self::Address(ip.into_canonical_ip_addr()),
60            Err(_) => self,
61        }
62    }
63}
64
65impl IntoCanonicalIpAddr for HostWithPort {
66    #[inline(always)]
67    fn into_canonical_ip_addr(self) -> Self {
68        Self {
69            host: self.host.into_canonical_ip_addr(),
70            port: self.port,
71        }
72    }
73}
74
75impl IntoCanonicalIpAddr for HostWithOptPort {
76    #[inline(always)]
77    fn into_canonical_ip_addr(self) -> Self {
78        Self {
79            host: self.host.into_canonical_ip_addr(),
80            port: self.port,
81        }
82    }
83}
84
85impl IntoCanonicalIpAddr for ProxyAddress {
86    #[inline(always)]
87    fn into_canonical_ip_addr(self) -> Self {
88        Self {
89            protocol: self.protocol,
90            address: self.address.into_canonical_ip_addr(),
91            credential: self.credential,
92        }
93    }
94}
95
96impl IntoCanonicalIpAddr for Authority {
97    fn into_canonical_ip_addr(self) -> Self {
98        Self {
99            user_info: self.user_info,
100            address: self.address.into_canonical_ip_addr(),
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn ipv4_loopback_is_unchanged() {
111        let socket_addr = SocketAddress::local_ipv4(8080);
112        assert_eq!(socket_addr.into_canonical_ip_addr(), socket_addr);
113    }
114
115    #[test]
116    fn ipv6_loopback_is_unchanged() {
117        let socket_addr = SocketAddress::local_ipv6(8080);
118        assert_eq!(socket_addr.into_canonical_ip_addr(), socket_addr);
119    }
120
121    #[test]
122    fn ipv4_is_unchanged() {
123        let socket_addr = SocketAddress::from(([192, 168, 1, 1], 8080));
124        assert_eq!(socket_addr.into_canonical_ip_addr(), socket_addr);
125    }
126
127    #[test]
128    fn ipv6_is_unchanged() {
129        let socket_addr = SocketAddress::from(([0x2001, 0x0db8, 0, 0, 0, 0, 0xdead, 0xbeef], 8080));
130        assert_eq!(socket_addr.into_canonical_ip_addr(), socket_addr);
131    }
132
133    #[test]
134    fn ipv4_mapped_ipv6_is_converted_to_ipv4() {
135        // ::ffff:192.10.2.255
136        let socket_addr = SocketAddress::from(([0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x02ff], 8080));
137        assert_eq!(
138            socket_addr.into_canonical_ip_addr(),
139            SocketAddress::from(([192, 10, 2, 255], 8080))
140        );
141    }
142
143    #[test]
144    fn ipv4_mapped_loopback_ipv6_is_converted_to_ipv4() {
145        // ::ffff:127.0.0.1
146        let socket_addr = SocketAddress::from(([0, 0, 0, 0, 0, 0xffff, 0x7f00, 0x0001], 8080));
147        assert_eq!(
148            socket_addr.into_canonical_ip_addr(),
149            SocketAddress::from(([127, 0, 0, 1], 8080))
150        );
151    }
152
153    #[test]
154    fn ipv4_compatible_ipv6_is_not_converted_to_ipv4() {
155        // ::192.0.2.33, represented as 0:0:0:0:0:0:c000:0221
156        let socket_addr = SocketAddress::from(([0, 0, 0, 0, 0, 0, 0xc000, 0x0221], 8080));
157        assert_eq!(socket_addr.into_canonical_ip_addr(), socket_addr);
158    }
159
160    #[test]
161    fn ipv4_mapped_zero_zero_zero_one_is_converted_to_ipv4() {
162        // ::ffff:0.0.0.1
163        let socket_addr = SocketAddress::from(([0, 0, 0, 0, 0, 0xffff, 0x0000, 0x0001], 8080));
164        assert_eq!(
165            socket_addr.into_canonical_ip_addr(),
166            SocketAddress::from(([0, 0, 0, 1], 8080))
167        );
168    }
169
170    // ---- Host bridging through Uninterpreted ----------------------------
171
172    /// Regression: a pct-encoded IPv4-mapped IPv6 carried in
173    /// `Host::Uninterpreted` must canonicalize to the embedded IPv4,
174    /// not pass through unchanged. The eager parser doesn't promote
175    /// pct-encoded forms — `Host::try_as_ip` does — so the canonical
176    /// op needs to bridge.
177    #[test]
178    fn host_uninterpreted_pct_encoded_v4_mapped_v6_canonicalizes_to_v4() {
179        // ::ffff:192.0.2.1 — but pct-encoded so the URI parser stored
180        // it as `Host::Uninterpreted`.
181        let host = crate::uri::Uri::parse("http://%3A%3Affff%3A192.0.2.1/")
182            .unwrap()
183            .host()
184            .unwrap()
185            .into_owned();
186        assert!(matches!(host, Host::Uninterpreted(_)), "fixture sanity");
187        let canonical = host.into_canonical_ip_addr();
188        assert_eq!(
189            canonical,
190            Host::Address("192.0.2.1".parse::<IpAddr>().unwrap())
191        );
192    }
193
194    /// A pct-encoded plain IPv4 also promotes.
195    #[test]
196    fn host_uninterpreted_pct_encoded_v4_canonicalizes() {
197        let host = crate::uri::Uri::parse("http://%31%32%37.0.0.1/")
198            .unwrap()
199            .host()
200            .unwrap()
201            .into_owned();
202        assert!(matches!(host, Host::Uninterpreted(_)));
203        let canonical = host.into_canonical_ip_addr();
204        assert_eq!(
205            canonical,
206            Host::Address("127.0.0.1".parse::<IpAddr>().unwrap())
207        );
208    }
209
210    /// Non-promotable Uninterpreted (sub-delim) passes through unchanged.
211    #[test]
212    fn host_uninterpreted_subdelim_passes_through() {
213        let host = crate::uri::Uri::parse("http://tag,with,commas/")
214            .unwrap()
215            .host()
216            .unwrap()
217            .into_owned();
218        assert!(matches!(host, Host::Uninterpreted(_)));
219        let canonical = host.clone().into_canonical_ip_addr();
220        assert_eq!(canonical, host);
221    }
222
223    /// IPvFuture (bracketed Uninterpreted) passes through unchanged —
224    /// no IPv4-mapped fold-down for `[vN.X]` shapes.
225    #[test]
226    fn host_uninterpreted_ipvfuture_passes_through() {
227        let host = crate::uri::Uri::parse("http://[v1.fe80::a]/")
228            .unwrap()
229            .host()
230            .unwrap()
231            .into_owned();
232        assert!(matches!(host, Host::Uninterpreted(_)));
233        let canonical = host.clone().into_canonical_ip_addr();
234        assert_eq!(canonical, host);
235    }
236
237    /// `Host::Name` always passes through — domains have no IP shape
238    /// to canonicalize.
239    #[test]
240    fn host_name_passes_through() {
241        let host = Host::Name(crate::address::Domain::from_static("example.com"));
242        let canonical = host.clone().into_canonical_ip_addr();
243        assert_eq!(canonical, host);
244    }
245}