Skip to main content

tailscale/
dial.rs

1//! String-address dialing — the Go `tsnet.Server.Dial` / `ListenPacket` / `HTTPClient` consumer
2//! entry points.
3//!
4//! Go `tsnet` lets an embedder write `srv.Dial(ctx, "tcp", "myhost:443")` and
5//! `srv.ListenPacket("udp", "0.0.0.0:0")` — a `network` string plus a `host:port` string, where
6//! `host` may be a MagicDNS name or an IP literal. This module provides the parsing + dispatch that
7//! [`crate::Device::dial`], [`crate::Device::dial_tcp`], and [`crate::Device::listen_packet`] build
8//! on, mirroring Go `tsnet.go`'s `resolveListenAddr` (the `host:port` parser) and the `listen`
9//! network-validation set.
10//!
11//! The actual transport is the existing netstack: TCP reuses [`crate::Device::tcp_connect`]; UDP
12//! reuses [`crate::Device::udp_bind`] wrapped by [`ConnectedUdpSocket`], which remembers a fixed
13//! peer so it presents the connected-`net.Conn` shape Go's `Dial("udp", …)` returns.
14
15use core::net::{IpAddr, SocketAddr};
16
17use crate::{Error, InternalErrorKind, netstack};
18
19/// The transport selected by a `network` string.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub(crate) enum Transport {
22    Tcp,
23    Udp,
24}
25
26/// The address family forced by a `network` string suffix (`tcp4`/`udp6`/…), or `Any` for the
27/// unsuffixed `tcp`/`udp` (family then follows the resolved/parsed address).
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub(crate) enum Family {
30    Any,
31    V4,
32    V6,
33}
34
35/// A parsed `network` string.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub(crate) struct Network {
38    pub transport: Transport,
39    pub family: Family,
40}
41
42/// Parse a Go-style `network` string into a [`Network`], mirroring the accepted set in Go
43/// `tsnet.go`'s `listen` switch (`"tcp"`, `"tcp4"`, `"tcp6"`, `"udp"`, `"udp4"`, `"udp6"`). Anything
44/// else is [`InternalErrorKind::BadRequest`] ("unsupported network type").
45///
46/// Unlike Go's `listen`, the empty string `""` is **not** accepted here: a bare dial/connect with no
47/// transport is meaningless (Go's `Dial` would route `""` through `tsdial`, which defaults it, but
48/// the typed Rust surface is clearer requiring an explicit transport).
49pub(crate) fn parse_network(network: &str) -> Result<Network, Error> {
50    let n = match network {
51        "tcp" => Network {
52            transport: Transport::Tcp,
53            family: Family::Any,
54        },
55        "tcp4" => Network {
56            transport: Transport::Tcp,
57            family: Family::V4,
58        },
59        "tcp6" => Network {
60            transport: Transport::Tcp,
61            family: Family::V6,
62        },
63        "udp" => Network {
64            transport: Transport::Udp,
65            family: Family::Any,
66        },
67        "udp4" => Network {
68            transport: Transport::Udp,
69            family: Family::V4,
70        },
71        "udp6" => Network {
72            transport: Transport::Udp,
73            family: Family::V6,
74        },
75        _ => return Err(Error::Internal(InternalErrorKind::BadRequest)),
76    };
77    Ok(n)
78}
79
80/// Split a `host:port` string into its host and numeric port, mirroring Go's
81/// `net.SplitHostPort` + `net.LookupPort` as used by `tsnet`'s `resolveListenAddr`.
82///
83/// - The host may be a MagicDNS name, an IPv4 literal, or a **bracketed** IPv6 literal
84///   (`[2001:db8::1]:443`) — brackets are required for v6 exactly as Go/`SplitHostPort` requires.
85/// - The port must be **numeric** in `0..=65535`. Go's `LookupPort` also resolves named ports
86///   (`"http"`→80) via the OS services database; this fork deliberately does **not** pull a
87///   services-file dependency, so named ports are unsupported (a small, documented divergence).
88/// - A missing `:port` is rejected (Go's `SplitHostPort` errors), as is a host with no port.
89///
90/// Returns the host slice (brackets stripped for v6) and the parsed port.
91pub(crate) fn split_host_port(addr: &str) -> Result<(&str, u16), Error> {
92    let bad = || Error::Internal(InternalErrorKind::BadRequest);
93
94    // Bracketed IPv6: "[<v6>]:port". The colon that separates host from port is the one *after*
95    // the closing bracket, so a v6 literal's own colons don't confuse the split.
96    let (host, port_str) = if let Some(rest) = addr.strip_prefix('[') {
97        let close = rest.find(']').ok_or_else(bad)?;
98        let host = &rest[..close];
99        let after = &rest[close + 1..];
100        let port_str = after.strip_prefix(':').ok_or_else(bad)?;
101        (host, port_str)
102    } else {
103        // Unbracketed: split on the LAST colon. A bare IPv6 literal (multiple colons, no brackets)
104        // is therefore rejected — matching Go, which requires brackets for a v6 host:port.
105        let idx = addr.rfind(':').ok_or_else(bad)?;
106        let host = &addr[..idx];
107        let port_str = &addr[idx + 1..];
108        if host.contains(':') {
109            // More than one colon and not bracketed → a bare v6 literal; Go rejects this form.
110            return Err(bad());
111        }
112        (host, port_str)
113    };
114
115    if port_str.is_empty() {
116        return Err(bad());
117    }
118    let port: u16 = port_str.parse().map_err(|_| bad())?;
119    Ok((host, port))
120}
121
122/// A UDP socket bound to a fixed remote peer — the connected-`net.Conn` shape Go's
123/// `Dial(ctx, "udp", …)` returns (a `*gonet.UDPConn` with a fixed destination).
124///
125/// The fork's netstack [`netstack::UdpSocket`] is unconnected (`send_to`/`recv_from` carry the peer
126/// per datagram, the `net.PacketConn` shape). This wrapper stores the peer so [`send`](Self::send)
127/// targets it implicitly and [`recv`](Self::recv) **filters to datagrams from that peer**, dropping
128/// any from other sources — exactly what a kernel `connect(2)` on a UDP socket does. UDP stays
129/// message-oriented (one `send`/`recv` = one datagram), as it is for Go's UDP `net.Conn`.
130pub struct ConnectedUdpSocket {
131    sock: netstack::UdpSocket,
132    peer: SocketAddr,
133}
134
135impl ConnectedUdpSocket {
136    pub(crate) fn new(sock: netstack::UdpSocket, peer: SocketAddr) -> Self {
137        Self { sock, peer }
138    }
139
140    /// The connected peer this socket sends to / receives from.
141    pub fn peer(&self) -> SocketAddr {
142        self.peer
143    }
144
145    /// The local address the underlying socket is bound to.
146    pub fn local_addr(&self) -> SocketAddr {
147        self.sock.local_addr()
148    }
149
150    /// Send one datagram to the connected peer (Go UDP `net.Conn::Write`).
151    pub async fn send(&self, data: &[u8]) -> Result<(), Error> {
152        self.sock.send_to(self.peer, data).await.map_err(Into::into)
153    }
154
155    /// Receive one datagram from the connected peer into `buf`, returning the byte count (Go UDP
156    /// `net.Conn::Read`). Datagrams from any other source are discarded (connected-UDP semantics),
157    /// so this loops until a datagram from the connected peer arrives or the socket errors.
158    pub async fn recv(&self, buf: &mut [u8]) -> Result<usize, Error> {
159        loop {
160            let (from, n) = self.sock.recv_from(buf).await?;
161            if from == self.peer {
162                return Ok(n);
163            }
164            // Not from our connected peer — drop it and keep waiting (mirrors connect(2) filtering).
165        }
166    }
167}
168
169/// The result of a [`crate::Device::dial`]: a connected stream whose transport matches the dialed
170/// `network`. Rust has no `net.Conn` trait object, so this is an explicit enum; the TCP arm is an
171/// async byte stream (`AsyncRead`+`AsyncWrite`), the UDP arm is the message-oriented connected
172/// socket. Use [`crate::Device::dial_tcp`] when you know it's TCP and want the stream directly.
173pub enum DialConn {
174    /// A connected TCP stream (Go `Dial("tcp", …)`).
175    Tcp(netstack::TcpStream),
176    /// A connected UDP socket bound to the dialed peer (Go `Dial("udp", …)`).
177    Udp(ConnectedUdpSocket),
178}
179
180/// Validate a resolved/parsed destination IP against the family forced by the `network` suffix
181/// (`tcp4` with a v6 address → error, and vice versa), mirroring `resolveListenAddr`'s
182/// `…4`/`…6` checks.
183pub(crate) fn check_family(family: Family, ip: IpAddr) -> Result<(), Error> {
184    let ok = match family {
185        Family::Any => true,
186        Family::V4 => ip.is_ipv4(),
187        Family::V6 => ip.is_ipv6(),
188    };
189    if ok {
190        Ok(())
191    } else {
192        Err(Error::Internal(InternalErrorKind::BadRequest))
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn parse_network_accepts_the_tsnet_set() {
202        assert_eq!(parse_network("tcp").unwrap().transport, Transport::Tcp);
203        assert_eq!(parse_network("tcp4").unwrap().family, Family::V4);
204        assert_eq!(parse_network("tcp6").unwrap().family, Family::V6);
205        assert_eq!(parse_network("udp").unwrap().transport, Transport::Udp);
206        assert_eq!(parse_network("udp4").unwrap().family, Family::V4);
207        assert_eq!(parse_network("udp6").unwrap().family, Family::V6);
208    }
209
210    #[test]
211    fn parse_network_rejects_unsupported() {
212        for n in ["", "sctp", "ip", "tcp5", "unix", "TCP"] {
213            assert!(parse_network(n).is_err(), "{n:?} must be rejected");
214        }
215    }
216
217    #[test]
218    fn split_host_port_ipv4() {
219        assert_eq!(split_host_port("1.2.3.4:80").unwrap(), ("1.2.3.4", 80));
220    }
221
222    #[test]
223    fn split_host_port_ipv6_bracketed() {
224        assert_eq!(
225            split_host_port("[2001:db8::1]:443").unwrap(),
226            ("2001:db8::1", 443)
227        );
228    }
229
230    #[test]
231    fn split_host_port_name() {
232        assert_eq!(split_host_port("myhost:22").unwrap(), ("myhost", 22));
233        assert_eq!(
234            split_host_port("host.tail.ts.net:8080").unwrap(),
235            ("host.tail.ts.net", 8080)
236        );
237    }
238
239    #[test]
240    fn split_host_port_rejects_missing_port() {
241        assert!(split_host_port("myhost").is_err());
242        assert!(split_host_port("1.2.3.4").is_err());
243        assert!(split_host_port("host:").is_err());
244    }
245
246    #[test]
247    fn split_host_port_rejects_bare_ipv6() {
248        // Unbracketed v6 (multiple colons) is rejected — Go requires brackets.
249        assert!(split_host_port("2001:db8::1:443").is_err());
250    }
251
252    #[test]
253    fn split_host_port_rejects_bad_port() {
254        assert!(split_host_port("host:99999").is_err()); // > u16::MAX
255        assert!(split_host_port("host:http").is_err()); // named port unsupported (numeric only)
256        assert!(split_host_port("host:-1").is_err());
257    }
258
259    #[test]
260    fn check_family_matches() {
261        let v4: IpAddr = "1.2.3.4".parse().unwrap();
262        let v6: IpAddr = "2001:db8::1".parse().unwrap();
263        assert!(check_family(Family::Any, v4).is_ok());
264        assert!(check_family(Family::V4, v4).is_ok());
265        assert!(check_family(Family::V6, v6).is_ok());
266        assert!(check_family(Family::V4, v6).is_err());
267        assert!(check_family(Family::V6, v4).is_err());
268    }
269}