rusty_penguin_lib/
parse_remote.rs

1//! Client `remote` specification.
2//
3// SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later
4
5use std::{fmt::Display, str::FromStr};
6use thiserror::Error;
7
8#[cfg(not(feature = "default-is-ipv6"))]
9/// Default host for local and unspecified addresses.
10macro_rules! default_host {
11    (local) => {
12        String::from("127.0.0.1")
13    };
14    (unspec) => {
15        String::from("0.0.0.0")
16    };
17}
18#[cfg(feature = "default-is-ipv6")]
19/// Default host for local and unspecified addresses.
20macro_rules! default_host {
21    (local) => {
22        String::from("::1")
23    };
24    (unspec) => {
25        String::from("::")
26    };
27}
28
29// Export this macro for use in `arg.rs`.
30pub(crate) use default_host;
31
32/// Default SOCKS port
33pub const SOCKS_DEFAULT_PORT: u16 = 1080;
34/// Default TPROXY port
35pub const TPROXY_DEFAULT_PORT: u16 = 1234;
36
37/// Configuration for one item to forward
38#[derive(Debug, Clone, Hash, Eq, PartialEq)]
39pub struct Remote {
40    /// Local-side forwarding info
41    pub local_addr: LocalSpec,
42    /// Peer-side forwarding info
43    #[allow(clippy::struct_field_names)]
44    pub remote_addr: RemoteSpec,
45    /// Layer-4 protocol this instance forwards
46    pub protocol: Protocol,
47}
48
49/// The local side can be either IP+port or "stdio"
50#[derive(Debug, Clone, Hash, Eq, PartialEq)]
51pub enum LocalSpec {
52    /// An IP socket
53    Inet((String, u16)),
54    /// Standard input/output
55    Stdio,
56}
57
58/// The remote side can be either IP+port or "socks"
59#[derive(Debug, Clone, Hash, Eq, PartialEq)]
60pub enum RemoteSpec {
61    /// An IP socket
62    Inet((String, u16)),
63    /// Function as a SOCKS proxy
64    Socks,
65    Tproxy,
66}
67
68/// Protocol can be either "tcp" or "udp".
69#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
70pub enum Protocol {
71    /// Transmission Control Protocol
72    Tcp,
73    /// User Datagram Protocol
74    Udp,
75}
76
77/// Errors that can occur when parsing a remote.
78#[derive(Clone, Error, Debug, PartialEq, Eq)]
79pub enum Error {
80    /// Invalid remote specification
81    #[error("Invalid format")]
82    Format,
83    /// Invalid protocol
84    #[error("Invalid protocol")]
85    Protocol,
86    /// Invalid host or address
87    #[error("Invalid host")]
88    Host,
89    /// Invalid port
90    #[error("Invalid port")]
91    Port(#[from] std::num::ParseIntError),
92    /// UDP cannot be used with SOCKS
93    #[error("socks remote must be TCP")]
94    UdpSocks,
95    #[error("stdio cannot work with Transparent Proxy")]
96    StdioTproxy,
97}
98
99impl Display for Protocol {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        f.write_str(match self {
102            Self::Tcp => "tcp",
103            Self::Udp => "udp",
104        })
105    }
106}
107
108impl FromStr for Protocol {
109    type Err = Error;
110
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        match s.to_lowercase().as_str() {
113            "tcp" => Ok(Self::Tcp),
114            "udp" => Ok(Self::Udp),
115            _ => Err(Error::Protocol),
116        }
117    }
118}
119
120/// Tokenize a remote string by splitting on `:`.
121/// We don't need RE!
122fn tokenize_remote(s: &str) -> Result<Vec<&str>, Error> {
123    let mut tokens = Vec::new();
124    let mut stuff = s;
125    loop {
126        // IPv6 address in brackets
127        if stuff.starts_with('[') {
128            let end = stuff.find(']').ok_or(Error::Host)? + 1;
129            tokens.push(&stuff[..end]);
130            // Now stuff[end..] should start with ':', so we assert that and skip it.
131            if !stuff[end..].is_empty() && !stuff[end..].starts_with(':') {
132                return Err(Error::Format);
133            }
134            stuff = &stuff[end + 1..];
135        } else if let Some((token, rest)) = stuff.split_once(':') {
136            tokens.push(token);
137            stuff = rest;
138        } else {
139            tokens.push(stuff);
140            return Ok(tokens);
141        }
142    }
143}
144
145impl Display for Remote {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        match &self.local_addr {
148            LocalSpec::Inet((host, port)) => {
149                if host.contains(':') {
150                    write!(f, "[{host}]:{port}")?;
151                } else {
152                    write!(f, "{host}:{port}")?;
153                }
154            }
155            LocalSpec::Stdio => f.write_str("stdio")?,
156        }
157        match &self.remote_addr {
158            RemoteSpec::Inet((host, port)) => {
159                if host.contains(':') {
160                    write!(f, ":[{host}]:{port}")?;
161                } else {
162                    write!(f, ":{host}:{port}")?;
163                }
164            }
165            RemoteSpec::Socks => f.write_str(":socks")?,
166            RemoteSpec::Tproxy => f.write_str(":tproxy")?,
167        }
168        write!(f, "/{}", self.protocol)?;
169        Ok(())
170    }
171}
172
173impl FromStr for Remote {
174    type Err = Error;
175
176    /// Parse a remote specification.
177    #[allow(clippy::too_many_lines)]
178    fn from_str(s: &str) -> Result<Self, Self::Err> {
179        let (rest, proto) = match s.rsplit_once('/') {
180            Some((rest, proto)) => (rest, proto.parse()?),
181            None => (s, Protocol::Tcp),
182        };
183        let tokens = tokenize_remote(rest)?;
184        let result = match tokens[..] {
185            // One element: either "socks", "tproxy" or a port number.
186            ["socks"] => Ok(Self {
187                local_addr: LocalSpec::Inet((default_host!(local), SOCKS_DEFAULT_PORT)),
188                remote_addr: RemoteSpec::Socks,
189                protocol: proto,
190            }),
191            ["tproxy"] => Ok(Self {
192                local_addr: LocalSpec::Inet((default_host!(local), TPROXY_DEFAULT_PORT)),
193                remote_addr: RemoteSpec::Tproxy,
194                protocol: proto,
195            }),
196            [port] => Ok(Self {
197                local_addr: LocalSpec::Inet((default_host!(unspec), port.parse()?)),
198                remote_addr: RemoteSpec::Inet((default_host!(local), port.parse()?)),
199                protocol: proto,
200            }),
201            // Two elements: either "socks" or "tproxy" and local port number, or remote host and port number.
202            ["stdio", "socks"] => Ok(Self {
203                local_addr: LocalSpec::Stdio,
204                remote_addr: RemoteSpec::Socks,
205                protocol: proto,
206            }),
207            ["stdio", "tproxy"] => Err(Error::StdioTproxy),
208            [port, "socks"] => Ok(Self {
209                local_addr: LocalSpec::Inet((default_host!(local), port.parse()?)),
210                remote_addr: RemoteSpec::Socks,
211                protocol: proto,
212            }),
213            [port, "tproxy"] => Ok(Self {
214                local_addr: LocalSpec::Inet((default_host!(local), port.parse()?)),
215                remote_addr: RemoteSpec::Tproxy,
216                protocol: proto,
217            }),
218            ["stdio", port] => Ok(Self {
219                local_addr: LocalSpec::Stdio,
220                remote_addr: RemoteSpec::Inet((default_host!(local), port.parse()?)),
221                protocol: proto,
222            }),
223            [host, port] => Ok(Self {
224                local_addr: LocalSpec::Inet((default_host!(unspec), port.parse()?)),
225                remote_addr: RemoteSpec::Inet((remove_brackets(host).to_string(), port.parse()?)),
226                protocol: proto,
227            }),
228            // Three elements:
229            // - "stdio", remote host, and port number,
230            // - local host, local port number, and "socks", or
231            // - local port number, remote host, and port number.
232            ["stdio", remote_host, remote_port] => Ok(Self {
233                local_addr: LocalSpec::Stdio,
234                remote_addr: RemoteSpec::Inet((
235                    remove_brackets(remote_host).to_string(),
236                    remote_port.parse()?,
237                )),
238                protocol: proto,
239            }),
240            [local_host, local_port, "socks"] => Ok(Self {
241                local_addr: LocalSpec::Inet((
242                    remove_brackets(local_host).to_string(),
243                    local_port.parse()?,
244                )),
245                remote_addr: RemoteSpec::Socks,
246                protocol: proto,
247            }),
248            [local_host, local_port, "tproxy"] => Ok(Self {
249                local_addr: LocalSpec::Inet((
250                    remove_brackets(local_host).to_string(),
251                    local_port.parse()?,
252                )),
253                remote_addr: RemoteSpec::Tproxy,
254                protocol: proto,
255            }),
256            [local_port, remote_host, remote_port] => Ok(Self {
257                local_addr: LocalSpec::Inet((default_host!(unspec), local_port.parse()?)),
258                remote_addr: RemoteSpec::Inet((
259                    remove_brackets(remote_host).to_string(),
260                    remote_port.parse()?,
261                )),
262                protocol: proto,
263            }),
264            [local_host, local_port, remote_host, remote_port] => Ok(Self {
265                local_addr: LocalSpec::Inet((
266                    remove_brackets(local_host).to_string(),
267                    local_port.parse()?,
268                )),
269                remote_addr: RemoteSpec::Inet((
270                    remove_brackets(remote_host).to_string(),
271                    remote_port.parse()?,
272                )),
273                protocol: proto,
274            }),
275            _ => Err(Error::Format),
276        };
277        // I love Rust's pattern matching
278        // (this sentence is written by GitHub Copilot)
279        if let Ok(Self {
280            remote_addr: RemoteSpec::Socks,
281            protocol: Protocol::Udp,
282            ..
283        }) = &result
284        {
285            Err(Error::UdpSocks)
286        } else {
287            result
288        }
289    }
290}
291
292/// Remove brackets from possbly an IPv6 address
293#[must_use]
294pub fn remove_brackets(s: &str) -> &str {
295    if s.starts_with('[') && s.ends_with(']') {
296        &s[1..s.len() - 1]
297    } else {
298        s
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    // We could have DRYed, but this is to physically show the difference.
307    #[cfg(not(feature = "default-is-ipv6"))]
308    #[test]
309    fn test_default_host() {
310        crate::tests::setup_logging();
311        assert_eq!(
312            std::net::Ipv4Addr::from_str(&default_host!(unspec)).unwrap(),
313            std::net::Ipv4Addr::UNSPECIFIED
314        );
315        assert_eq!(
316            std::net::Ipv4Addr::from_str(&default_host!(local)).unwrap(),
317            std::net::Ipv4Addr::LOCALHOST
318        );
319    }
320    #[cfg(feature = "default-is-ipv6")]
321    #[test]
322    fn test_default_host() {
323        crate::tests::setup_logging();
324        assert_eq!(
325            std::net::Ipv6Addr::from_str(&default_host!(unspec)).unwrap(),
326            std::net::Ipv6Addr::UNSPECIFIED
327        );
328        assert_eq!(
329            std::net::Ipv6Addr::from_str(&default_host!(local)).unwrap(),
330            std::net::Ipv6Addr::LOCALHOST
331        );
332    }
333
334    #[test]
335    #[allow(clippy::too_many_lines)]
336    fn test_parse_remote() {
337        crate::tests::setup_logging();
338        let tests: &[(&str, Remote)] = &[
339            // jpillora's tests and an exhausive list of cases
340            (
341                "3000",
342                Remote {
343                    local_addr: LocalSpec::Inet((default_host!(unspec), 3000)),
344                    remote_addr: RemoteSpec::Inet((default_host!(local), 3000)),
345                    protocol: Protocol::Tcp,
346                },
347            ),
348            (
349                "4000/udp",
350                Remote {
351                    local_addr: LocalSpec::Inet((default_host!(unspec), 4000)),
352                    remote_addr: RemoteSpec::Inet((default_host!(local), 4000)),
353                    protocol: Protocol::Udp,
354                },
355            ),
356            (
357                "google.com:80",
358                Remote {
359                    local_addr: LocalSpec::Inet((default_host!(unspec), 80)),
360                    remote_addr: RemoteSpec::Inet((String::from("google.com"), 80)),
361                    protocol: Protocol::Tcp,
362                },
363            ),
364            (
365                "示例網站.com:80",
366                Remote {
367                    local_addr: LocalSpec::Inet((default_host!(unspec), 80)),
368                    remote_addr: RemoteSpec::Inet((String::from("示例網站.com"), 80)),
369                    protocol: Protocol::Tcp,
370                },
371            ),
372            (
373                "8080:example.com:80",
374                Remote {
375                    local_addr: LocalSpec::Inet((default_host!(unspec), 8080)),
376                    remote_addr: RemoteSpec::Inet((String::from("example.com"), 80)),
377                    protocol: Protocol::Tcp,
378                },
379            ),
380            (
381                "socks",
382                Remote {
383                    local_addr: LocalSpec::Inet((default_host!(local), SOCKS_DEFAULT_PORT)),
384                    remote_addr: RemoteSpec::Socks,
385                    protocol: Protocol::Tcp,
386                },
387            ),
388            (
389                "9050:socks",
390                Remote {
391                    local_addr: LocalSpec::Inet((default_host!(local), 9050)),
392                    remote_addr: RemoteSpec::Socks,
393                    protocol: Protocol::Tcp,
394                },
395            ),
396            (
397                "127.0.0.1:1081:socks",
398                Remote {
399                    local_addr: LocalSpec::Inet((String::from("127.0.0.1"), 1081)),
400                    remote_addr: RemoteSpec::Socks,
401                    protocol: Protocol::Tcp,
402                },
403            ),
404            (
405                "9050:socks",
406                Remote {
407                    local_addr: LocalSpec::Inet((default_host!(local), 9050)),
408                    remote_addr: RemoteSpec::Socks,
409                    protocol: Protocol::Tcp,
410                },
411            ),
412            (
413                "tproxy",
414                Remote {
415                    local_addr: LocalSpec::Inet((default_host!(local), TPROXY_DEFAULT_PORT)),
416                    remote_addr: RemoteSpec::Tproxy,
417                    protocol: Protocol::Tcp,
418                },
419            ),
420            (
421                "tproxy/udp",
422                Remote {
423                    local_addr: LocalSpec::Inet((default_host!(local), TPROXY_DEFAULT_PORT)),
424                    remote_addr: RemoteSpec::Tproxy,
425                    protocol: Protocol::Udp,
426                },
427            ),
428            (
429                "5000:tproxy",
430                Remote {
431                    local_addr: LocalSpec::Inet((default_host!(local), 5000)),
432                    remote_addr: RemoteSpec::Tproxy,
433                    protocol: Protocol::Tcp,
434                },
435            ),
436            (
437                "4567:tproxy/udp",
438                Remote {
439                    local_addr: LocalSpec::Inet((default_host!(local), 4567)),
440                    remote_addr: RemoteSpec::Tproxy,
441                    protocol: Protocol::Udp,
442                },
443            ),
444            (
445                "127.0.0.1:1081:tproxy",
446                Remote {
447                    local_addr: LocalSpec::Inet((String::from("127.0.0.1"), 1081)),
448                    remote_addr: RemoteSpec::Tproxy,
449                    protocol: Protocol::Tcp,
450                },
451            ),
452            (
453                "127.0.0.1:1081:tproxy/udp",
454                Remote {
455                    local_addr: LocalSpec::Inet((String::from("127.0.0.1"), 1081)),
456                    remote_addr: RemoteSpec::Tproxy,
457                    protocol: Protocol::Udp,
458                },
459            ),
460            (
461                "[::1]:12345:tproxy/udp",
462                Remote {
463                    local_addr: LocalSpec::Inet((String::from("::1"), 12345)),
464                    remote_addr: RemoteSpec::Tproxy,
465                    protocol: Protocol::Udp,
466                },
467            ),
468            (
469                "1.1.1.1:53/udp",
470                Remote {
471                    local_addr: LocalSpec::Inet((default_host!(unspec), 53)),
472                    remote_addr: RemoteSpec::Inet((String::from("1.1.1.1"), 53)),
473                    protocol: Protocol::Udp,
474                },
475            ),
476            (
477                "localhost:5353:1.1.1.1:53/udp",
478                Remote {
479                    local_addr: LocalSpec::Inet((String::from("localhost"), 5353)),
480                    remote_addr: RemoteSpec::Inet((String::from("1.1.1.1"), 53)),
481                    protocol: Protocol::Udp,
482                },
483            ),
484            (
485                "22:example.com:22",
486                Remote {
487                    local_addr: LocalSpec::Inet((default_host!(unspec), 22)),
488                    remote_addr: RemoteSpec::Inet((String::from("example.com"), 22)),
489                    protocol: Protocol::Tcp,
490                },
491            ),
492            (
493                "[::1]:8080:google.com:80",
494                Remote {
495                    local_addr: LocalSpec::Inet((String::from("::1"), 8080)),
496                    remote_addr: RemoteSpec::Inet((String::from("google.com"), 80)),
497                    protocol: Protocol::Tcp,
498                },
499            ),
500            (
501                "localhost:5354:[2001:4860:4860:0:0:0:0:8888]:53/udp",
502                Remote {
503                    local_addr: LocalSpec::Inet((String::from("localhost"), 5354)),
504                    remote_addr: RemoteSpec::Inet((
505                        String::from("2001:4860:4860:0:0:0:0:8888"),
506                        53,
507                    )),
508                    protocol: Protocol::Udp,
509                },
510            ),
511            (
512                "stdio:google.com:80",
513                Remote {
514                    local_addr: LocalSpec::Stdio,
515                    remote_addr: RemoteSpec::Inet((String::from("google.com"), 80)),
516                    protocol: Protocol::Tcp,
517                },
518            ),
519            (
520                "stdio:socks",
521                Remote {
522                    local_addr: LocalSpec::Stdio,
523                    remote_addr: RemoteSpec::Socks,
524                    protocol: Protocol::Tcp,
525                },
526            ),
527            (
528                "stdio:443",
529                Remote {
530                    local_addr: LocalSpec::Stdio,
531                    remote_addr: RemoteSpec::Inet((default_host!(local), 443)),
532                    protocol: Protocol::Tcp,
533                },
534            ),
535            (
536                "stdio:5353/udp",
537                Remote {
538                    local_addr: LocalSpec::Stdio,
539                    remote_addr: RemoteSpec::Inet((default_host!(local), 5353)),
540                    protocol: Protocol::Udp,
541                },
542            ),
543        ];
544        for (s, expected) in tests {
545            // Test that the common format is parsed correctly
546            let actual = s.parse::<Remote>().unwrap();
547            assert_eq!(actual, *expected);
548            // Test that the canonical format is made and parsed correctly
549            let reparsed = actual.to_string().parse::<Remote>().unwrap();
550            assert_eq!(reparsed, *expected);
551        }
552        "just_a_hostname".parse::<Remote>().unwrap_err();
553        "socks/udp".parse::<Remote>().unwrap_err();
554        assert!(matches!(
555            "socks/udp".parse::<Remote>().unwrap_err(),
556            Error::UdpSocks
557        ));
558        assert!(matches!(
559            "stdio:tproxy".parse::<Remote>().unwrap_err(),
560            Error::StdioTproxy
561        ));
562    }
563}