Skip to main content

ts_control/
service.rs

1//! Tailscale VIP-service hosting (`tsnet`'s `ListenService`).
2//!
3//! A node hosts a **VIP service** (`svc:<label>`) by binding a listener on the virtual IP
4//! address(es) control assigned that service. This module provides the fail-closed gating that
5//! decides *whether* and *on which address* a node may listen for a service, mirroring Go's
6//! `tsnet.Server.ListenService` preconditions:
7//!
8//! 1. the service name must be a valid `svc:<dns-label>` ([`crate::validate_service_name`]);
9//! 2. the host must be **tagged** (Go `ErrUntaggedServiceHost`) — an untagged node cannot host
10//!    services;
11//! 3. control must have assigned the service a VIP address on this node (delivered via the
12//!    `service-host` node-capability, parsed into [`Node::service_addresses`]).
13//!
14//! When all hold, [`resolve_service_listen`] returns the [`core::net::SocketAddr`] the embedder
15//! should bind via `Device::tcp_listen` on the overlay netstack. Otherwise it returns a typed
16//! [`ServiceError`] and the node serves nothing — never a host socket, never an unbound listen.
17//!
18//! The L3/`Tun` service mode (Go `ServiceConfig.Tun`) is intentionally unsupported: it is a TODO in
19//! upstream tsnet and the fork's default data path is the userspace netstack.
20
21use crate::node::Node;
22
23/// How a VIP service terminates incoming connections (a scoped mirror of tsnet's `ServiceMode`).
24///
25/// Both modes bind a TCP listener on the service VIP; the distinction is what terminates the
26/// stream. L3/`Tun` forwarding (Go `ServiceConfig.Tun`) is deliberately omitted — see the module
27/// docs.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ServiceMode {
30    /// Raw TCP on `port`: the embedder is handed the accepted overlay stream (like tsnet's
31    /// `ServiceModeTCP`).
32    Tcp {
33        /// The service port to listen on.
34        port: u16,
35    },
36    /// HTTP(S) on `port`: the embedder serves an HTTP handler over the accepted stream (like
37    /// tsnet's `ServiceModeHTTP`). The fork treats this identically to [`ServiceMode::Tcp`] at the
38    /// listen layer — it binds the same VIP:port and hands back the stream; TLS termination /
39    /// HTTP handling is the embedder's concern.
40    Http {
41        /// The service port to listen on.
42        port: u16,
43    },
44}
45
46impl ServiceMode {
47    /// The TCP port this mode listens on.
48    pub fn port(&self) -> u16 {
49        match self {
50            ServiceMode::Tcp { port } | ServiceMode::Http { port } => *port,
51        }
52    }
53}
54
55/// Why a VIP-service listen request was refused. Fail-closed by construction: there is no variant
56/// that yields a usable listen address without a genuine control-assigned VIP on a tagged host.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum ServiceError {
59    /// The service name is not a valid `svc:<dns-label>` (Go `ServiceName.Validate`).
60    InvalidName(String),
61    /// The node is not tagged, so it cannot host VIP services (Go `ErrUntaggedServiceHost`).
62    UntaggedHost,
63    /// Control has not assigned this node a VIP address for the service (no `service-host` cap
64    /// entry, or none covering this service). The node serves nothing rather than binding an
65    /// arbitrary address — fail-closed.
66    NoAssignedVip(String),
67    /// Binding the overlay listener on the resolved VIP failed (e.g. the netstack is unavailable,
68    /// as in TUN transport mode, or the address is already in use). Carries a human-readable detail.
69    Listen(String),
70}
71
72impl core::fmt::Display for ServiceError {
73    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
74        match self {
75            ServiceError::InvalidName(name) => {
76                write!(
77                    f,
78                    "invalid VIP service name {name:?} (expected svc:<dns-label>)"
79                )
80            }
81            ServiceError::UntaggedHost => write!(
82                f,
83                "this node is untagged and cannot host a Tailscale service (it must be tagged)"
84            ),
85            ServiceError::NoAssignedVip(name) => write!(
86                f,
87                "control has not assigned a VIP address for service {name:?} on this node"
88            ),
89            ServiceError::Listen(detail) => {
90                write!(f, "failed to bind the VIP service listener: {detail}")
91            }
92        }
93    }
94}
95
96impl std::error::Error for ServiceError {}
97
98/// Resolve the overlay [`core::net::SocketAddr`] a node should bind to host VIP service `name` in
99/// `mode`, enforcing the three fail-closed preconditions (valid name, tagged host, control-assigned
100/// VIP). See the module docs.
101///
102/// Picks the first hosted VIP whose IP family the netstack can serve: an IPv6 VIP is only chosen
103/// when `enable_ipv6` is set (the fork is IPv4-only by default), preferring an IPv4 VIP otherwise.
104pub fn resolve_service_listen(
105    node: &Node,
106    name: &str,
107    mode: ServiceMode,
108    enable_ipv6: bool,
109) -> Result<core::net::SocketAddr, ServiceError> {
110    if crate::validate_service_name(name).is_none() {
111        return Err(ServiceError::InvalidName(name.to_string()));
112    }
113
114    // Go refuses to host a service from an untagged node (ErrUntaggedServiceHost).
115    if node.tags.is_empty() {
116        return Err(ServiceError::UntaggedHost);
117    }
118
119    // Pick a VIP assigned to *this specific service* that the netstack can actually serve. Prefer
120    // IPv4; only use an IPv6 VIP when IPv6 is enabled on the overlay (matching the netstack's
121    // accepted-address set). Using the per-service mapping (not the flattened host set) ensures a
122    // multi-service co-host binds the correct VIP for `name`.
123    let vips = node.service_addresses_for(name);
124    let vip = vips
125        .iter()
126        .find(|ip| ip.is_ipv4())
127        .or_else(|| vips.iter().find(|ip| enable_ipv6 && ip.is_ipv6()))
128        .ok_or_else(|| ServiceError::NoAssignedVip(name.to_string()))?;
129
130    Ok(core::net::SocketAddr::new(*vip, mode.port()))
131}
132
133#[cfg(test)]
134mod tests {
135    use alloc::{collections::BTreeMap, string::ToString, vec, vec::Vec};
136    use core::net::{IpAddr, Ipv4Addr};
137
138    use super::*;
139    use crate::node::{NodeCapMap, StableId, TailnetAddress};
140
141    /// Build a node hosting `service` on `vips` (when both are non-empty), tagged with `tags`.
142    fn node(tags: &[&str], service: &str, vips: Vec<IpAddr>) -> Node {
143        node_multi(tags, &[(service, vips)])
144    }
145
146    /// Build a node hosting several `(service, vips)` mappings.
147    fn node_multi(tags: &[&str], services: &[(&str, Vec<IpAddr>)]) -> Node {
148        let mut cap_map = NodeCapMap::new();
149        let mut service_vips: BTreeMap<String, Vec<IpAddr>> = BTreeMap::new();
150        for (service, vips) in services {
151            if !service.is_empty() && !vips.is_empty() {
152                service_vips.insert((*service).to_string(), vips.clone());
153            }
154        }
155        if !service_vips.is_empty() {
156            cap_map.insert(ts_control_serde::NODE_ATTR_SERVICE_HOST.to_string(), vec![]);
157        }
158        Node {
159            id: 1,
160            stable_id: StableId("n1".to_string()),
161            hostname: "host".to_string(),
162            user_id: 0,
163            tailnet: Some("tail1.ts.net".to_string()),
164            tags: tags.iter().map(|t| t.to_string()).collect(),
165            tailnet_address: TailnetAddress {
166                ipv4: "100.64.0.1/32".parse().unwrap(),
167                ipv6: "fd7a::1/128".parse().unwrap(),
168            },
169            node_key: [0u8; 32].into(),
170            node_key_expiry: None,
171            online: None,
172            last_seen: None,
173            key_signature: vec![],
174            machine_key: None,
175            disco_key: None,
176            accepted_routes: vec![],
177            underlay_addresses: vec![],
178            derp_region: None,
179            cap: Default::default(),
180            cap_map,
181            peerapi_port: None,
182            peerapi_dns_proxy: false,
183            is_wireguard_only: false,
184            exit_node_dns_resolvers: vec![],
185            peer_relay: false,
186            service_vips,
187        }
188    }
189
190    fn vip4() -> IpAddr {
191        IpAddr::V4(Ipv4Addr::new(100, 65, 32, 1))
192    }
193
194    #[test]
195    fn tagged_host_with_vip_resolves_listen_addr() {
196        let n = node(&["tag:samba"], "svc:samba", vec![vip4()]);
197        let addr = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
198            .expect("a tagged host with an assigned VIP must resolve");
199        assert_eq!(addr, core::net::SocketAddr::new(vip4(), 445));
200    }
201
202    #[test]
203    fn http_mode_binds_same_vip_and_port() {
204        let n = node(&["tag:web"], "svc:web", vec![vip4()]);
205        let addr =
206            resolve_service_listen(&n, "svc:web", ServiceMode::Http { port: 8080 }, false).unwrap();
207        assert_eq!(addr, core::net::SocketAddr::new(vip4(), 8080));
208    }
209
210    #[test]
211    fn invalid_name_is_rejected() {
212        let n = node(&["tag:x"], "svc:x", vec![vip4()]);
213        // Missing svc: prefix.
214        let err =
215            resolve_service_listen(&n, "samba", ServiceMode::Tcp { port: 1 }, false).unwrap_err();
216        assert!(matches!(err, ServiceError::InvalidName(_)));
217        // Bad label.
218        let err = resolve_service_listen(&n, "svc:-bad", ServiceMode::Tcp { port: 1 }, false)
219            .unwrap_err();
220        assert!(matches!(err, ServiceError::InvalidName(_)));
221    }
222
223    #[test]
224    fn untagged_host_is_rejected() {
225        let n = node(&[], "svc:samba", vec![vip4()]);
226        let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
227            .unwrap_err();
228        assert_eq!(err, ServiceError::UntaggedHost);
229    }
230
231    #[test]
232    fn no_assigned_vip_is_rejected() {
233        let n = node(&["tag:samba"], "", vec![]);
234        let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
235            .unwrap_err();
236        assert!(matches!(err, ServiceError::NoAssignedVip(_)));
237    }
238
239    #[test]
240    fn ipv6_vip_only_chosen_when_ipv6_enabled() {
241        let vip6: IpAddr = "fd7a:115c:a1e0::1234".parse().unwrap();
242        let n = node(&["tag:samba"], "svc:samba", vec![vip6]);
243
244        // IPv6 disabled: no servable VIP -> fail closed.
245        let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
246            .unwrap_err();
247        assert!(matches!(err, ServiceError::NoAssignedVip(_)));
248
249        // IPv6 enabled: the v6 VIP is used.
250        let addr =
251            resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, true).unwrap();
252        assert_eq!(addr, core::net::SocketAddr::new(vip6, 445));
253    }
254
255    #[test]
256    fn ipv4_vip_preferred_over_ipv6() {
257        let vip6: IpAddr = "fd7a:115c:a1e0::1234".parse().unwrap();
258        let n = node(&["tag:samba"], "svc:samba", vec![vip6, vip4()]);
259        let addr =
260            resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, true).unwrap();
261        assert_eq!(addr, core::net::SocketAddr::new(vip4(), 445));
262    }
263
264    #[test]
265    fn co_hosted_services_bind_their_own_vip() {
266        // Two services on distinct VIPs: each resolves to its OWN VIP, not the other's (the fix for
267        // the flattened-set wrong-bind).
268        let vip_a = IpAddr::V4(Ipv4Addr::new(100, 65, 32, 1));
269        let vip_b = IpAddr::V4(Ipv4Addr::new(100, 65, 32, 2));
270        let n = node_multi(
271            &["tag:multi"],
272            &[("svc:a", vec![vip_a]), ("svc:b", vec![vip_b])],
273        );
274
275        let a = resolve_service_listen(&n, "svc:a", ServiceMode::Tcp { port: 1 }, false).unwrap();
276        let b = resolve_service_listen(&n, "svc:b", ServiceMode::Tcp { port: 2 }, false).unwrap();
277        assert_eq!(a, core::net::SocketAddr::new(vip_a, 1));
278        assert_eq!(b, core::net::SocketAddr::new(vip_b, 2));
279
280        // A service this host does not have is denied even though the host has OTHER VIPs.
281        let err = resolve_service_listen(&n, "svc:absent", ServiceMode::Tcp { port: 3 }, false)
282            .unwrap_err();
283        assert!(matches!(err, ServiceError::NoAssignedVip(_)));
284    }
285}