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 bypass this check for pproxy compatibility.
13///
14/// Rejected ranges:
15/// - IPv4: loopback (127.0.0.0/8), link-local (169.254.0.0/16),
16///   private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), unspecified (0.0.0.0),
17///   broadcast (255.255.255.255), multicast (224.0.0.0/4),
18///   documentation (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24),
19///   benchmarking (198.18.0.0/15), reserved future (240.0.0.0/4),
20///   this-network (0.0.0.0/8)
21/// - IPv6: loopback (::1), link-local (fe80::/10), unique-local (fc00::/7),
22///   unspecified (::), multicast (ff00::/8),
23///   documentation (2001:db8::/32), discard prefix (0100::/64)
24pub fn is_reserved_or_private_ip(ip: &IpAddr) -> bool {
25    match ip {
26        IpAddr::V4(v4) => {
27            v4.is_loopback()
28                || v4.is_link_local()
29                || v4.is_private()
30                || v4.is_unspecified()
31                || v4.is_multicast()
32                || v4.is_broadcast()
33                || is_v4_documentation(v4)
34                || is_v4_benchmarking(v4)
35                || is_v4_reserved(v4)
36                || is_v4_this_network(v4)
37        }
38        IpAddr::V6(v6) => {
39            // IPv4-mapped IPv6 addresses are another representation of an
40            // IPv4 destination. Treat them identically so `::ffff:127.0.0.1`
41            // cannot bypass the private/reserved-address guard.
42            if let Some(v4) = v6.to_ipv4_mapped() {
43                return is_reserved_or_private_ip(&IpAddr::V4(v4));
44            }
45            v6.is_loopback()
46                || v6.is_unspecified()
47                || v6.is_multicast()
48                || is_v6_documentation(v6)
49                || is_unicast_link_local_v6(v6)
50                || is_unique_local_v6(v6)
51                || is_v6_discard_prefix(v6)
52        }
53    }
54}
55
56/// Check if an IPv6 address is in the fc00::/7 unique-local range.
57fn is_unique_local_v6(ip: &Ipv6Addr) -> bool {
58    let octets = ip.octets();
59    (octets[0] & 0xfe) == 0xfc
60}
61
62/// Check if an IPv6 address is in the fe80::/10 link-local unicast range.
63fn is_unicast_link_local_v6(ip: &Ipv6Addr) -> bool {
64    let octets = ip.octets();
65    octets[0] == 0xfe && (octets[1] & 0xc0) == 0x80
66}
67
68/// Check if an IPv6 address is in the 0100::/64 discard prefix.
69fn is_v6_discard_prefix(ip: &Ipv6Addr) -> bool {
70    let octets = ip.octets();
71    octets[0] == 0x01 && octets[1..8].iter().all(|b| *b == 0)
72}
73
74/// Check if an IPv4 address is in the 0.0.0.0/8 "this network" range.
75fn is_v4_this_network(ip: &std::net::Ipv4Addr) -> bool {
76    ip.octets()[0] == 0
77}
78
79/// Check if an IPv4 address is in any of the documentation ranges
80/// (TEST-NET-1: 192.0.2.0/24, TEST-NET-2: 198.51.100.0/24,
81/// TEST-NET-3: 203.0.113.0/24, 192.88.99.0/24).
82fn is_v4_documentation(ip: &std::net::Ipv4Addr) -> bool {
83    let octets = ip.octets();
84    matches!(
85        octets,
86        [192, 0, 2, _] | [198, 51, 100, _] | [203, 0, 113, _] | [192, 88, 99, _]
87    )
88}
89
90/// Check if an IPv4 address is in the benchmarking range (198.18.0.0/15).
91fn is_v4_benchmarking(ip: &std::net::Ipv4Addr) -> bool {
92    let octets = ip.octets();
93    octets[0] == 198 && (octets[1] == 18 || octets[1] == 19)
94}
95
96/// Check if an IPv4 address is in the reserved-for-future-use range
97/// (240.0.0.0/4 — first octet >= 240 and not broadcast).
98fn is_v4_reserved(ip: &std::net::Ipv4Addr) -> bool {
99    let octets = ip.octets();
100    octets[0] >= 240 && octets[0] < 255
101}
102
103/// Check if an IPv6 address is in the documentation range (2001:db8::/32).
104fn is_v6_documentation(ip: &Ipv6Addr) -> bool {
105    let octets = ip.octets();
106    octets[0] == 0x20 && octets[1] == 0x01 && octets[2] == 0x0d && octets[3] == 0xb8
107}
108
109/// Check if a resolved IP address represents a DNS rebinding risk.
110///
111/// Used as a domain-resolution guard: after resolving a domain name,
112/// this checks whether the result points to a private/reserved range.
113/// Literal IP targets bypass this check for pproxy compatibility.
114pub fn is_dns_rebinding_risk(ip: &IpAddr) -> bool {
115    is_reserved_or_private_ip(ip)
116}
117
118/// Trait for connecting to target servers.
119#[trait_variant::make(Connector: Send)]
120pub trait LocalConnector {
121    async fn connect(&self, target: &TargetAddr) -> Result<BoxStream, ConnectError>;
122}
123
124/// Connector that makes direct TCP connections.
125pub struct DirectConnector;
126
127/// Connect options for one outbound socket.
128#[derive(Debug, Clone, Default)]
129pub struct ConnectOptions {
130    pub local_bind: Option<SocketAddr>,
131}
132
133impl DirectConnector {
134    pub async fn connect_with_options(
135        &self,
136        target: &TargetAddr,
137        options: &ConnectOptions,
138    ) -> Result<BoxStream, ConnectError> {
139        let addr = resolve_target(target).await?;
140        let stream = if let Some(local) = options.local_bind {
141            let socket = if local.is_ipv4() {
142                tokio::net::TcpSocket::new_v4()
143            } else {
144                tokio::net::TcpSocket::new_v6()
145            }
146            .map_err(ConnectError::Io)?;
147            socket.bind(local).map_err(ConnectError::Io)?;
148            socket.connect(addr).await.map_err(ConnectError::Io)?
149        } else {
150            TcpStream::connect(addr).await?
151        };
152        Ok(Box::new(stream))
153    }
154}
155
156async fn resolve_target(target: &TargetAddr) -> Result<SocketAddr, ConnectError> {
157    match &target.host {
158        TargetHost::Ip(ip) => Ok(SocketAddr::new(*ip, target.port)),
159        TargetHost::Domain(domain) => {
160            let lookup = format!("{}:{}", domain, target.port);
161            let mut addrs = tokio::net::lookup_host(&lookup)
162                .await
163                .map_err(|e| ConnectError::DnsResolution(e.to_string()))?;
164            let resolved = addrs
165                .next()
166                .ok_or_else(|| ConnectError::DnsResolution("no addresses found".to_string()))?;
167            if is_dns_rebinding_risk(&resolved.ip()) {
168                return Err(ConnectError::ReservedTarget(resolved.ip()));
169            }
170            Ok(resolved)
171        }
172    }
173}
174
175impl Connector for DirectConnector {
176    async fn connect(&self, target: &TargetAddr) -> Result<BoxStream, ConnectError> {
177        self.connect_with_options(target, &ConnectOptions::default())
178            .await
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use std::net::Ipv4Addr;
186    use tokio::io::{AsyncReadExt, AsyncWriteExt};
187
188    #[tokio::test]
189    async fn test_direct_connect_echo() {
190        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
191        let addr = listener.local_addr().unwrap();
192
193        let jh = tokio::spawn(async move {
194            let (mut stream, _) = listener.accept().await.unwrap();
195            let mut buf = [0u8; 1024];
196            let n = stream.read(&mut buf).await.unwrap();
197            stream.write_all(&buf[..n]).await.unwrap();
198        });
199
200        let target = TargetAddr {
201            host: TargetHost::Ip(addr.ip()),
202            port: addr.port(),
203        };
204
205        let connector = DirectConnector;
206        let mut stream = Connector::connect(&connector, &target).await.unwrap();
207
208        stream.write_all(b"ping").await.unwrap();
209        let mut buf = [0u8; 4];
210        stream.read_exact(&mut buf).await.unwrap();
211        assert_eq!(&buf, b"ping");
212
213        jh.await.unwrap();
214    }
215
216    #[test]
217    fn reserved_ipv4_loopback() {
218        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
219            127, 0, 0, 1
220        ))));
221    }
222
223    #[test]
224    fn reserved_ipv4_private_10() {
225        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
226            10, 0, 0, 1
227        ))));
228    }
229
230    #[test]
231    fn reserved_ipv4_private_172() {
232        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
233            172, 16, 0, 1
234        ))));
235    }
236
237    #[test]
238    fn reserved_ipv4_private_192() {
239        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
240            192, 168, 1, 1
241        ))));
242    }
243
244    #[test]
245    fn reserved_ipv4_link_local() {
246        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
247            169, 254, 1, 1
248        ))));
249    }
250
251    #[test]
252    fn reserved_ipv4_unspecified() {
253        assert!(is_reserved_or_private_ip(&IpAddr::V4(
254            Ipv4Addr::UNSPECIFIED
255        )));
256    }
257
258    #[test]
259    fn not_reserved_ipv4_public() {
260        assert!(!is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
261            8, 8, 8, 8
262        ))));
263    }
264
265    #[test]
266    fn reserved_ipv6_loopback() {
267        assert!(is_reserved_or_private_ip(&IpAddr::V6(Ipv6Addr::LOCALHOST)));
268    }
269
270    #[test]
271    fn reserved_ipv6_link_local() {
272        let ip = "fe80::1".parse::<Ipv6Addr>().unwrap();
273        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
274    }
275
276    #[test]
277    fn reserved_ipv4_mapped_ipv6() {
278        let ip = "::ffff:127.0.0.1".parse::<Ipv6Addr>().unwrap();
279        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
280    }
281
282    #[test]
283    fn reserved_ipv6_unique_local() {
284        let ip = "fd00::1".parse::<Ipv6Addr>().unwrap();
285        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
286    }
287
288    #[test]
289    fn reserved_ipv6_unspecified() {
290        assert!(is_reserved_or_private_ip(&IpAddr::V6(
291            Ipv6Addr::UNSPECIFIED
292        )));
293    }
294
295    #[test]
296    fn not_reserved_ipv6_public() {
297        let ip = "2606:4700:4700::1111".parse::<Ipv6Addr>().unwrap();
298        assert!(!is_reserved_or_private_ip(&IpAddr::V6(ip)));
299    }
300
301    #[test]
302    fn reserved_ipv4_multicast() {
303        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
304            224, 0, 0, 1
305        ))));
306    }
307
308    #[test]
309    fn reserved_ipv4_broadcast() {
310        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::BROADCAST)));
311    }
312
313    #[test]
314    fn reserved_ipv4_documentation() {
315        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
316            192, 0, 2, 1
317        ))));
318        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
319            198, 51, 100, 1
320        ))));
321        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
322            203, 0, 113, 1
323        ))));
324    }
325
326    #[test]
327    fn reserved_ipv4_benchmarking() {
328        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
329            198, 18, 0, 1
330        ))));
331    }
332
333    #[test]
334    fn reserved_ipv4_reserved_future() {
335        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
336            240, 0, 0, 1
337        ))));
338    }
339
340    #[test]
341    fn reserved_ipv4_this_network() {
342        assert!(is_reserved_or_private_ip(&IpAddr::V4(Ipv4Addr::new(
343            0, 1, 2, 3
344        ))));
345    }
346
347    #[test]
348    fn reserved_ipv6_multicast() {
349        let ip = "ff02::1".parse::<Ipv6Addr>().unwrap();
350        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
351    }
352
353    #[test]
354    fn reserved_ipv6_documentation() {
355        let ip = "2001:db8::1".parse::<Ipv6Addr>().unwrap();
356        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
357    }
358
359    #[test]
360    fn reserved_ipv6_discard_prefix() {
361        let ip = "0100::1".parse::<Ipv6Addr>().unwrap();
362        assert!(is_reserved_or_private_ip(&IpAddr::V6(ip)));
363    }
364
365    #[tokio::test]
366    async fn reject_domain_resolving_to_loopback() {
367        let connector = DirectConnector;
368        let target = TargetAddr {
369            host: TargetHost::Domain("localhost".to_string()),
370            port: 1,
371        };
372        let result = Connector::connect(&connector, &target).await;
373        assert!(matches!(result, Err(ConnectError::ReservedTarget(_))));
374    }
375}