1use crate::node::Node;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ServiceMode {
30 Tcp {
33 port: u16,
35 },
36 Http {
41 port: u16,
43 },
44}
45
46impl ServiceMode {
47 pub fn port(&self) -> u16 {
49 match self {
50 ServiceMode::Tcp { port } | ServiceMode::Http { port } => *port,
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum ServiceError {
59 InvalidName(String),
61 UntaggedHost,
63 NoAssignedVip(String),
67 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
98pub 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 if node.tags.is_empty() {
116 return Err(ServiceError::UntaggedHost);
117 }
118
119 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 fn node(tags: &[&str], service: &str, vips: Vec<IpAddr>) -> Node {
143 node_multi(tags, &[(service, vips)])
144 }
145
146 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 key_signature: vec![],
172 machine_key: None,
173 disco_key: None,
174 accepted_routes: vec![],
175 underlay_addresses: vec![],
176 derp_region: None,
177 cap: Default::default(),
178 cap_map,
179 peerapi_port: None,
180 peerapi_dns_proxy: false,
181 is_wireguard_only: false,
182 exit_node_dns_resolvers: vec![],
183 peer_relay: false,
184 service_vips,
185 }
186 }
187
188 fn vip4() -> IpAddr {
189 IpAddr::V4(Ipv4Addr::new(100, 65, 32, 1))
190 }
191
192 #[test]
193 fn tagged_host_with_vip_resolves_listen_addr() {
194 let n = node(&["tag:samba"], "svc:samba", vec![vip4()]);
195 let addr = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
196 .expect("a tagged host with an assigned VIP must resolve");
197 assert_eq!(addr, core::net::SocketAddr::new(vip4(), 445));
198 }
199
200 #[test]
201 fn http_mode_binds_same_vip_and_port() {
202 let n = node(&["tag:web"], "svc:web", vec![vip4()]);
203 let addr =
204 resolve_service_listen(&n, "svc:web", ServiceMode::Http { port: 8080 }, false).unwrap();
205 assert_eq!(addr, core::net::SocketAddr::new(vip4(), 8080));
206 }
207
208 #[test]
209 fn invalid_name_is_rejected() {
210 let n = node(&["tag:x"], "svc:x", vec![vip4()]);
211 let err =
213 resolve_service_listen(&n, "samba", ServiceMode::Tcp { port: 1 }, false).unwrap_err();
214 assert!(matches!(err, ServiceError::InvalidName(_)));
215 let err = resolve_service_listen(&n, "svc:-bad", ServiceMode::Tcp { port: 1 }, false)
217 .unwrap_err();
218 assert!(matches!(err, ServiceError::InvalidName(_)));
219 }
220
221 #[test]
222 fn untagged_host_is_rejected() {
223 let n = node(&[], "svc:samba", vec![vip4()]);
224 let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
225 .unwrap_err();
226 assert_eq!(err, ServiceError::UntaggedHost);
227 }
228
229 #[test]
230 fn no_assigned_vip_is_rejected() {
231 let n = node(&["tag:samba"], "", vec![]);
232 let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
233 .unwrap_err();
234 assert!(matches!(err, ServiceError::NoAssignedVip(_)));
235 }
236
237 #[test]
238 fn ipv6_vip_only_chosen_when_ipv6_enabled() {
239 let vip6: IpAddr = "fd7a:115c:a1e0::1234".parse().unwrap();
240 let n = node(&["tag:samba"], "svc:samba", vec![vip6]);
241
242 let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
244 .unwrap_err();
245 assert!(matches!(err, ServiceError::NoAssignedVip(_)));
246
247 let addr =
249 resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, true).unwrap();
250 assert_eq!(addr, core::net::SocketAddr::new(vip6, 445));
251 }
252
253 #[test]
254 fn ipv4_vip_preferred_over_ipv6() {
255 let vip6: IpAddr = "fd7a:115c:a1e0::1234".parse().unwrap();
256 let n = node(&["tag:samba"], "svc:samba", vec![vip6, vip4()]);
257 let addr =
258 resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, true).unwrap();
259 assert_eq!(addr, core::net::SocketAddr::new(vip4(), 445));
260 }
261
262 #[test]
263 fn co_hosted_services_bind_their_own_vip() {
264 let vip_a = IpAddr::V4(Ipv4Addr::new(100, 65, 32, 1));
267 let vip_b = IpAddr::V4(Ipv4Addr::new(100, 65, 32, 2));
268 let n = node_multi(
269 &["tag:multi"],
270 &[("svc:a", vec![vip_a]), ("svc:b", vec![vip_b])],
271 );
272
273 let a = resolve_service_listen(&n, "svc:a", ServiceMode::Tcp { port: 1 }, false).unwrap();
274 let b = resolve_service_listen(&n, "svc:b", ServiceMode::Tcp { port: 2 }, false).unwrap();
275 assert_eq!(a, core::net::SocketAddr::new(vip_a, 1));
276 assert_eq!(b, core::net::SocketAddr::new(vip_b, 2));
277
278 let err = resolve_service_listen(&n, "svc:absent", ServiceMode::Tcp { port: 3 }, false)
280 .unwrap_err();
281 assert!(matches!(err, ServiceError::NoAssignedVip(_)));
282 }
283}