Skip to main content

eggress_core/
connector.rs

1use std::net::{IpAddr, Ipv6Addr, SocketAddr};
2
3use tokio::net::TcpStream;
4
5use crate::{BoxStream, ConnectError, TargetAddr, TargetHost};
6
7/// Returns `true` if the IP address is reserved, private, or otherwise
8/// unsuitable for direct outbound connections (DNS rebinding protection).
9///
10/// Used as a domain-resolution guard: after resolving a domain name,
11/// this checks whether the result points to a private/reserved/special-use
12/// range. Literal IP targets have a separate opt-in check so explicit
13/// local/LAN destinations remain compatible by default. The DNS guard is
14/// enabled by default; callers that require pproxy-compatible permissive
15/// behavior must explicitly disable it.
16///
17/// Rejected ranges:
18/// - IPv4: loopback (127.0.0.0/8), link-local (169.254.0.0/16),
19///   private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), unspecified (0.0.0.0),
20///   broadcast (255.255.255.255), multicast (224.0.0.0/4),
21///   documentation (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24),
22///   benchmarking (198.18.0.0/15), reserved future (240.0.0.0/4),
23///   this-network (0.0.0.0/8)
24/// - IPv6: loopback (::1), link-local (fe80::/10), unique-local (fc00::/7),
25///   unspecified (::), multicast (ff00::/8),
26///   documentation (2001:db8::/32), discard prefix (0100::/64)
27pub fn is_reserved_or_private_ip(ip: &IpAddr) -> bool {
28    match ip {
29        IpAddr::V4(v4) => {
30            v4.is_loopback()
31                || v4.is_link_local()
32                || v4.is_private()
33                || v4.is_unspecified()
34                || v4.is_multicast()
35                || v4.is_broadcast()
36                || is_v4_documentation(v4)
37                || is_v4_benchmarking(v4)
38                || is_v4_reserved(v4)
39                || is_v4_this_network(v4)
40        }
41        IpAddr::V6(v6) => {
42            // IPv4-mapped IPv6 addresses are another representation of an
43            // IPv4 destination. Treat them identically so `::ffff:127.0.0.1`
44            // cannot bypass the private/reserved-address guard.
45            if let Some(v4) = v6.to_ipv4_mapped() {
46                return is_reserved_or_private_ip(&IpAddr::V4(v4));
47            }
48            v6.is_loopback()
49                || v6.is_unspecified()
50                || v6.is_multicast()
51                || is_v6_documentation(v6)
52                || is_unicast_link_local_v6(v6)
53                || is_unique_local_v6(v6)
54                || is_v6_discard_prefix(v6)
55        }
56    }
57}
58
59/// Check if an IPv6 address is in the fc00::/7 unique-local range.
60fn is_unique_local_v6(ip: &Ipv6Addr) -> bool {
61    let octets = ip.octets();
62    (octets[0] & 0xfe) == 0xfc
63}
64
65/// Check if an IPv6 address is in the fe80::/10 link-local unicast range.
66fn is_unicast_link_local_v6(ip: &Ipv6Addr) -> bool {
67    let octets = ip.octets();
68    octets[0] == 0xfe && (octets[1] & 0xc0) == 0x80
69}
70
71/// Check if an IPv6 address is in the 0100::/64 discard prefix.
72fn is_v6_discard_prefix(ip: &Ipv6Addr) -> bool {
73    let octets = ip.octets();
74    octets[0] == 0x01 && octets[1..8].iter().all(|b| *b == 0)
75}
76
77/// Check if an IPv4 address is in the 0.0.0.0/8 "this network" range.
78fn is_v4_this_network(ip: &std::net::Ipv4Addr) -> bool {
79    ip.octets()[0] == 0
80}
81
82/// Check if an IPv4 address is in any of the documentation ranges
83/// (TEST-NET-1: 192.0.2.0/24, TEST-NET-2: 198.51.100.0/24,
84/// TEST-NET-3: 203.0.113.0/24, 192.88.99.0/24).
85fn is_v4_documentation(ip: &std::net::Ipv4Addr) -> bool {
86    let octets = ip.octets();
87    matches!(
88        octets,
89        [192, 0, 2, _] | [198, 51, 100, _] | [203, 0, 113, _] | [192, 88, 99, _]
90    )
91}
92
93/// Check if an IPv4 address is in the benchmarking range (198.18.0.0/15).
94fn is_v4_benchmarking(ip: &std::net::Ipv4Addr) -> bool {
95    let octets = ip.octets();
96    octets[0] == 198 && (octets[1] == 18 || octets[1] == 19)
97}
98
99/// Check if an IPv4 address is in the reserved-for-future-use range
100/// (240.0.0.0/4 — first octet >= 240, including 255.0.0.0/8; the single
101/// broadcast address is additionally classified elsewhere).
102fn is_v4_reserved(ip: &std::net::Ipv4Addr) -> bool {
103    ip.octets()[0] >= 240
104}
105
106/// Check if an IPv6 address is in the documentation range (2001:db8::/32).
107fn is_v6_documentation(ip: &Ipv6Addr) -> bool {
108    let octets = ip.octets();
109    octets[0] == 0x20 && octets[1] == 0x01 && octets[2] == 0x0d && octets[3] == 0xb8
110}
111
112/// Check if a resolved IP address represents a DNS rebinding risk.
113pub fn is_dns_rebinding_risk(ip: &IpAddr) -> bool {
114    is_reserved_or_private_ip(ip)
115}
116
117/// Trait for connecting to target servers.
118#[trait_variant::make(Connector: Send)]
119pub trait LocalConnector {
120    async fn connect(&self, target: &TargetAddr) -> Result<BoxStream, ConnectError>;
121}
122
123/// Connector that makes direct TCP connections.
124pub struct DirectConnector;
125
126/// Connect options for one outbound socket.
127#[derive(Debug, Clone)]
128pub struct ConnectOptions {
129    pub local_bind: Option<SocketAddr>,
130    /// Reject DNS results in reserved/private ranges. Literal IP targets are
131    /// intentionally allowed for explicit local/LAN proxy compatibility.
132    pub enforce_dns_rebinding_check: bool,
133    /// Also reject literal IP targets in reserved/private ranges when the
134    /// caller is operating a stricter security boundary.
135    pub enforce_literal_ip_check: bool,
136}
137
138impl Default for ConnectOptions {
139    fn default() -> Self {
140        Self {
141            local_bind: None,
142            enforce_dns_rebinding_check: true,
143            enforce_literal_ip_check: false,
144        }
145    }
146}
147
148impl DirectConnector {
149    pub async fn connect_with_options(
150        &self,
151        target: &TargetAddr,
152        options: &ConnectOptions,
153    ) -> Result<BoxStream, ConnectError> {
154        let addrs = resolve_target(
155            target,
156            options.enforce_dns_rebinding_check,
157            options.enforce_literal_ip_check,
158        )
159        .await?;
160        connect_to_addrs(&addrs, options.local_bind).await
161    }
162}
163
164async fn connect_to_addrs(
165    addrs: &[SocketAddr],
166    local_bind: Option<SocketAddr>,
167) -> Result<BoxStream, ConnectError> {
168    let mut last_error = None;
169    for &addr in addrs {
170        let result = if let Some(local) = local_bind {
171            let local = match local {
172                SocketAddr::V6(local) => local
173                    .ip()
174                    .to_ipv4_mapped()
175                    .map(|ip| SocketAddr::new(ip.into(), local.port()))
176                    .unwrap_or(local.into()),
177                local => local,
178            };
179            let socket = if local.is_ipv4() {
180                tokio::net::TcpSocket::new_v4()
181            } else {
182                tokio::net::TcpSocket::new_v6()
183            }
184            .map_err(ConnectError::Io)?;
185            socket.bind(local).map_err(ConnectError::Io)?;
186            socket.connect(addr).await.map_err(ConnectError::Io)
187        } else {
188            TcpStream::connect(addr).await.map_err(ConnectError::Io)
189        };
190        match result {
191            Ok(stream) => return Ok(Box::new(stream)),
192            Err(error) => last_error = Some(error),
193        }
194    }
195    Err(last_error.unwrap_or_else(|| ConnectError::DnsResolution("no addresses found".to_string())))
196}
197
198async fn resolve_target(
199    target: &TargetAddr,
200    enforce_dns_rebinding_check: bool,
201    enforce_literal_ip_check: bool,
202) -> Result<Vec<SocketAddr>, ConnectError> {
203    match &target.host {
204        TargetHost::Ip(ip) => {
205            if enforce_literal_ip_check && is_dns_rebinding_risk(ip) {
206                return Err(ConnectError::ReservedTarget(*ip));
207            }
208            Ok(vec![SocketAddr::new(*ip, target.port)])
209        }
210        TargetHost::Domain(domain) => {
211            let lookup = format!("{}:{}", domain, target.port);
212            let addrs: Vec<_> = tokio::net::lookup_host(&lookup)
213                .await
214                .map_err(|e| ConnectError::DnsResolution(e.to_string()))?
215                .collect();
216            if addrs.is_empty() {
217                return Err(ConnectError::DnsResolution(
218                    "no addresses found".to_string(),
219                ));
220            }
221            if enforce_dns_rebinding_check {
222                if let Some(reserved) = addrs.iter().find(|addr| is_dns_rebinding_risk(&addr.ip()))
223                {
224                    return Err(ConnectError::ReservedTarget(reserved.ip()));
225                }
226            }
227            Ok(addrs)
228        }
229    }
230}
231
232impl Connector for DirectConnector {
233    async fn connect(&self, target: &TargetAddr) -> Result<BoxStream, ConnectError> {
234        self.connect_with_options(target, &ConnectOptions::default())
235            .await
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use std::net::Ipv4Addr;
243    use tokio::io::{AsyncReadExt, AsyncWriteExt};
244
245    #[tokio::test]
246    async fn test_direct_connect_echo() {
247        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
248        let addr = listener.local_addr().unwrap();
249
250        let jh = tokio::spawn(async move {
251            let (mut stream, _) = listener.accept().await.unwrap();
252            let mut buf = [0u8; 1024];
253            let n = stream.read(&mut buf).await.unwrap();
254            stream.write_all(&buf[..n]).await.unwrap();
255        });
256
257        let target = TargetAddr {
258            host: TargetHost::Ip(addr.ip()),
259            port: addr.port(),
260        };
261
262        let connector = DirectConnector;
263        let mut stream = Connector::connect(&connector, &target).await.unwrap();
264
265        stream.write_all(b"ping").await.unwrap();
266        let mut buf = [0u8; 4];
267        stream.read_exact(&mut buf).await.unwrap();
268        assert_eq!(&buf, b"ping");
269
270        jh.await.unwrap();
271    }
272
273    #[tokio::test]
274    async fn dns_rebinding_policy_applies_consistently_to_domains() {
275        let target = TargetAddr {
276            host: TargetHost::Domain("localhost".to_string()),
277            port: 80,
278        };
279
280        assert!(resolve_target(&target, false, false).await.is_ok());
281        assert!(ConnectOptions::default().enforce_dns_rebinding_check);
282        assert!(matches!(
283            resolve_target(
284                &target,
285                ConnectOptions::default().enforce_dns_rebinding_check,
286                ConnectOptions::default().enforce_literal_ip_check,
287            )
288            .await,
289            Err(ConnectError::ReservedTarget(_))
290        ));
291    }
292
293    #[test]
294    fn reserved_ipv4_loopback() {
295        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
296            127, 0, 0, 1
297        ))));
298    }
299
300    #[test]
301    fn reserved_ipv4_private_10() {
302        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
303            10, 0, 0, 1
304        ))));
305    }
306
307    #[test]
308    fn reserved_ipv4_private_172() {
309        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
310            172, 16, 0, 1
311        ))));
312    }
313
314    #[test]
315    fn reserved_ipv4_private_192() {
316        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
317            192, 168, 1, 1
318        ))));
319    }
320
321    #[test]
322    fn reserved_ipv4_link_local() {
323        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
324            169, 254, 1, 1
325        ))));
326    }
327
328    #[test]
329    fn reserved_ipv4_unspecified() {
330        assert!(is_reserved_or_private_ip(&IpAddr::V4(
331            Ipv4Addr::UNSPECIFIED
332        )));
333    }
334
335    #[test]
336    fn not_reserved_ipv4_public() {
337        assert!(!is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
338            8, 8, 8, 8
339        ))));
340    }
341
342    #[test]
343    fn reserved_ipv6_loopback() {
344        assert!(is_reserved_or_private_ip(&IpAddr::V6(Ipv6Addr::LOCALHOST)));
345    }
346
347    #[test]
348    fn reserved_ipv6_link_local() {
349        let ip = "fe80::1".parse::<Ipv6Addr>().unwrap();
350        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
351    }
352
353    #[test]
354    fn reserved_ipv4_mapped_ipv6() {
355        let ip = "::ffff:127.0.0.1".parse::<Ipv6Addr>().unwrap();
356        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
357    }
358
359    #[test]
360    fn reserved_ipv6_unique_local() {
361        let ip = "fd00::1".parse::<Ipv6Addr>().unwrap();
362        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
363    }
364
365    #[test]
366    fn reserved_ipv6_unspecified() {
367        assert!(is_reserved_or_private_ip(&IpAddr::V6(
368            Ipv6Addr::UNSPECIFIED
369        )));
370    }
371
372    #[test]
373    fn not_reserved_ipv6_public() {
374        let ip = "2606:4700:4700::1111".parse::<Ipv6Addr>().unwrap();
375        assert!(!is_reserved_or_private_ip(&IpAddr::V6(ip)));
376    }
377
378    #[test]
379    fn reserved_ipv4_multicast() {
380        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
381            224, 0, 0, 1
382        ))));
383    }
384
385    #[test]
386    fn reserved_ipv4_broadcast() {
387        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::BROADCAST)));
388    }
389
390    #[test]
391    fn reserved_ipv4_documentation() {
392        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
393            192, 0, 2, 1
394        ))));
395        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
396            198, 51, 100, 1
397        ))));
398        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
399            203, 0, 113, 1
400        ))));
401    }
402
403    #[test]
404    fn reserved_ipv4_benchmarking() {
405        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
406            198, 18, 0, 1
407        ))));
408    }
409
410    #[test]
411    fn reserved_ipv4_reserved_future() {
412        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
413            240, 0, 0, 1
414        ))));
415    }
416
417    #[test]
418    fn reserved_ipv4_this_network() {
419        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
420            0, 1, 2, 3
421        ))));
422    }
423
424    #[test]
425    fn reserved_ipv6_multicast() {
426        let ip = "ff02::1".parse::<Ipv6Addr>().unwrap();
427        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
428    }
429
430    #[test]
431    fn reserved_ipv6_documentation() {
432        let ip = "2001:db8::1".parse::<Ipv6Addr>().unwrap();
433        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
434    }
435
436    #[test]
437    fn reserved_ipv6_discard_prefix() {
438        let ip = "0100::1".parse::<Ipv6Addr>().unwrap();
439        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
440    }
441
442    #[tokio::test]
443    async fn reject_domain_resolving_to_loopback() {
444        let connector = DirectConnector;
445        let target = TargetAddr {
446            host: TargetHost::Domain("localhost".to_string()),
447            port: 1,
448        };
449        let result = connector
450            .connect_with_options(
451                &target,
452                &ConnectOptions {
453                    enforce_dns_rebinding_check: true,
454                    ..Default::default()
455                },
456            )
457            .await;
458        assert!(matches!(result, Err(ConnectError::ReservedTarget(_))));
459    }
460
461    #[tokio::test]
462    async fn direct_connect_falls_back_to_next_resolved_address() {
463        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
464        let good_addr = listener.local_addr().unwrap();
465        let bad_addr = SocketAddr::new(good_addr.ip(), good_addr.port() + 1);
466
467        let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
468        let stream = connect_to_addrs(&[bad_addr, good_addr], None)
469            .await
470            .expect("second resolved address should be attempted");
471        drop(stream);
472        accept.await.unwrap();
473    }
474
475    #[tokio::test]
476    async fn mapped_ipv6_local_bind_uses_ipv4_socket() {
477        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
478        let addr = listener.local_addr().unwrap();
479        let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
480
481        let mapped = SocketAddr::new("::ffff:127.0.0.1".parse().unwrap(), 0);
482        let stream = connect_to_addrs(&[addr], Some(mapped))
483            .await
484            .expect("mapped IPv6 local bind should connect to IPv4");
485        drop(stream);
486        accept.await.unwrap();
487    }
488}