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            ssh_host_keys: vec![],
187            service_vips,
188        }
189    }
190
191    fn vip4() -> IpAddr {
192        IpAddr::V4(Ipv4Addr::new(100, 65, 32, 1))
193    }
194
195    #[test]
196    fn tagged_host_with_vip_resolves_listen_addr() {
197        let n = node(&["tag:samba"], "svc:samba", vec![vip4()]);
198        let addr = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
199            .expect("a tagged host with an assigned VIP must resolve");
200        assert_eq!(addr, core::net::SocketAddr::new(vip4(), 445));
201    }
202
203    #[test]
204    fn http_mode_binds_same_vip_and_port() {
205        let n = node(&["tag:web"], "svc:web", vec![vip4()]);
206        let addr =
207            resolve_service_listen(&n, "svc:web", ServiceMode::Http { port: 8080 }, false).unwrap();
208        assert_eq!(addr, core::net::SocketAddr::new(vip4(), 8080));
209    }
210
211    #[test]
212    fn invalid_name_is_rejected() {
213        let n = node(&["tag:x"], "svc:x", vec![vip4()]);
214        // Missing svc: prefix.
215        let err =
216            resolve_service_listen(&n, "samba", ServiceMode::Tcp { port: 1 }, false).unwrap_err();
217        assert!(matches!(err, ServiceError::InvalidName(_)));
218        // Bad label.
219        let err = resolve_service_listen(&n, "svc:-bad", ServiceMode::Tcp { port: 1 }, false)
220            .unwrap_err();
221        assert!(matches!(err, ServiceError::InvalidName(_)));
222    }
223
224    #[test]
225    fn untagged_host_is_rejected() {
226        let n = node(&[], "svc:samba", vec![vip4()]);
227        let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
228            .unwrap_err();
229        assert_eq!(err, ServiceError::UntaggedHost);
230    }
231
232    #[test]
233    fn no_assigned_vip_is_rejected() {
234        let n = node(&["tag:samba"], "", vec![]);
235        let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
236            .unwrap_err();
237        assert!(matches!(err, ServiceError::NoAssignedVip(_)));
238    }
239
240    #[test]
241    fn ipv6_vip_only_chosen_when_ipv6_enabled() {
242        let vip6: IpAddr = "fd7a:115c:a1e0::1234".parse().unwrap();
243        let n = node(&["tag:samba"], "svc:samba", vec![vip6]);
244
245        // IPv6 disabled: no servable VIP -> fail closed.
246        let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
247            .unwrap_err();
248        assert!(matches!(err, ServiceError::NoAssignedVip(_)));
249
250        // IPv6 enabled: the v6 VIP is used.
251        let addr =
252            resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, true).unwrap();
253        assert_eq!(addr, core::net::SocketAddr::new(vip6, 445));
254    }
255
256    #[test]
257    fn ipv4_vip_preferred_over_ipv6() {
258        let vip6: IpAddr = "fd7a:115c:a1e0::1234".parse().unwrap();
259        let n = node(&["tag:samba"], "svc:samba", vec![vip6, vip4()]);
260        let addr =
261            resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, true).unwrap();
262        assert_eq!(addr, core::net::SocketAddr::new(vip4(), 445));
263    }
264
265    #[test]
266    fn co_hosted_services_bind_their_own_vip() {
267        // Two services on distinct VIPs: each resolves to its OWN VIP, not the other's (the fix for
268        // the flattened-set wrong-bind).
269        let vip_a = IpAddr::V4(Ipv4Addr::new(100, 65, 32, 1));
270        let vip_b = IpAddr::V4(Ipv4Addr::new(100, 65, 32, 2));
271        let n = node_multi(
272            &["tag:multi"],
273            &[("svc:a", vec![vip_a]), ("svc:b", vec![vip_b])],
274        );
275
276        let a = resolve_service_listen(&n, "svc:a", ServiceMode::Tcp { port: 1 }, false).unwrap();
277        let b = resolve_service_listen(&n, "svc:b", ServiceMode::Tcp { port: 2 }, false).unwrap();
278        assert_eq!(a, core::net::SocketAddr::new(vip_a, 1));
279        assert_eq!(b, core::net::SocketAddr::new(vip_b, 2));
280
281        // A service this host does not have is denied even though the host has OTHER VIPs.
282        let err = resolve_service_listen(&n, "svc:absent", ServiceMode::Tcp { port: 3 }, false)
283            .unwrap_err();
284        assert!(matches!(err, ServiceError::NoAssignedVip(_)));
285    }
286}