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}