Skip to main content

io_proxy/socks/v5/
address.rs

1//! SOCKS5 target address ([RFC 1928 §4]): the `ATYP`/`DST.ADDR`/`DST.PORT`
2//! fields of the `CONNECT` request.
3//!
4//! [RFC 1928 §4]: https://www.rfc-editor.org/rfc/rfc1928#section-4
5
6use alloc::{
7    string::{String, ToString},
8    vec::Vec,
9};
10use core::net::IpAddr;
11
12use thiserror::Error;
13
14use crate::socks::v5::{ATYP_DOMAIN, ATYP_IPV4, ATYP_IPV6};
15
16/// Failure building an [`Socks5Address`].
17#[derive(Clone, Debug, Error, PartialEq, Eq)]
18pub enum Socks5AddressError {
19    /// The domain name exceeds the 255-byte field limit.
20    #[error("SOCKS5 domain name too long: {0} bytes (max 255)")]
21    DomainTooLong(usize),
22}
23
24/// A SOCKS5 target address.
25///
26/// A hostname is kept as a [`Domain`](Socks5Address::Domain) so the *proxy*
27/// resolves it (socks5h semantics); a literal IP is sent as
28/// [`Ipv4`](Socks5Address::Ipv4) / [`Ipv6`](Socks5Address::Ipv6).
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum Socks5Address {
31    /// IPv4 address and port.
32    Ipv4(core::net::Ipv4Addr, u16),
33    /// IPv6 address and port.
34    Ipv6(core::net::Ipv6Addr, u16),
35    /// Domain name (≤ 255 bytes) and port, resolved by the proxy.
36    Domain(String, u16),
37}
38
39impl Socks5Address {
40    /// Builds an address from a host string and port.
41    ///
42    /// A host that parses as an IP literal becomes [`Ipv4`]/[`Ipv6`];
43    /// anything else becomes a [`Domain`] (validated against the 255-byte
44    /// limit) for the proxy to resolve.
45    ///
46    /// [`Ipv4`]: Socks5Address::Ipv4
47    /// [`Ipv6`]: Socks5Address::Ipv6
48    /// [`Domain`]: Socks5Address::Domain
49    pub fn new(host: &str, port: u16) -> Result<Socks5Address, Socks5AddressError> {
50        match host.parse::<IpAddr>() {
51            Ok(IpAddr::V4(ip)) => Ok(Socks5Address::Ipv4(ip, port)),
52            Ok(IpAddr::V6(ip)) => Ok(Socks5Address::Ipv6(ip, port)),
53            Err(_) => {
54                if host.len() > 255 {
55                    return Err(Socks5AddressError::DomainTooLong(host.len()));
56                }
57                Ok(Socks5Address::Domain(host.to_string(), port))
58            }
59        }
60    }
61
62    /// Appends the `ATYP`/`DST.ADDR`/`DST.PORT` encoding to `out`.
63    pub(crate) fn encode_into(&self, out: &mut Vec<u8>) {
64        match self {
65            Socks5Address::Ipv4(ip, port) => {
66                out.push(ATYP_IPV4);
67                out.extend_from_slice(&ip.octets());
68                out.extend_from_slice(&port.to_be_bytes());
69            }
70            Socks5Address::Ipv6(ip, port) => {
71                out.push(ATYP_IPV6);
72                out.extend_from_slice(&ip.octets());
73                out.extend_from_slice(&port.to_be_bytes());
74            }
75            Socks5Address::Domain(name, port) => {
76                out.push(ATYP_DOMAIN);
77                out.push(name.len() as u8);
78                out.extend_from_slice(name.as_bytes());
79                out.extend_from_slice(&port.to_be_bytes());
80            }
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use alloc::vec::Vec;
88
89    use super::*;
90
91    fn encode(addr: &Socks5Address) -> Vec<u8> {
92        let mut out = Vec::new();
93        addr.encode_into(&mut out);
94        out
95    }
96
97    #[test]
98    fn new_classifies_host() {
99        assert!(matches!(
100            Socks5Address::new("1.2.3.4", 993),
101            Ok(Socks5Address::Ipv4(_, 993))
102        ));
103        assert!(matches!(
104            Socks5Address::new("::1", 25),
105            Ok(Socks5Address::Ipv6(_, 25))
106        ));
107        assert!(matches!(
108            Socks5Address::new("imap.example.com", 993),
109            Ok(Socks5Address::Domain(_, 993))
110        ));
111    }
112
113    #[test]
114    fn new_rejects_overlong_domain() {
115        let host = "a".repeat(256);
116        assert_eq!(
117            Socks5Address::new(&host, 1),
118            Err(Socks5AddressError::DomainTooLong(256))
119        );
120    }
121
122    #[test]
123    fn encode_ipv4() {
124        // ATYP=1, 1.2.3.4, port 993 = 0x03E1
125        assert_eq!(
126            encode(&Socks5Address::Ipv4([1, 2, 3, 4].into(), 993)),
127            [0x01, 1, 2, 3, 4, 0x03, 0xE1]
128        );
129    }
130
131    #[test]
132    fn encode_domain() {
133        // ATYP=3, len=3, "abc", port 80 = 0x0050
134        assert_eq!(
135            encode(&Socks5Address::Domain("abc".into(), 80)),
136            [0x03, 0x03, b'a', b'b', b'c', 0x00, 0x50]
137        );
138    }
139
140    #[test]
141    fn encode_ipv6() {
142        let addr = Socks5Address::Ipv6(core::net::Ipv6Addr::LOCALHOST, 443);
143        let mut expected = vec![0x04u8];
144        expected.extend_from_slice(&core::net::Ipv6Addr::LOCALHOST.octets());
145        expected.extend_from_slice(&443u16.to_be_bytes());
146        assert_eq!(encode(&addr), expected);
147    }
148}