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 addresses: vec![
166 "100.64.0.1/32".parse().unwrap(),
167 "fd7a::1/128".parse().unwrap(),
168 ],
169 tailnet_address: TailnetAddress {
170 ipv4: "100.64.0.1/32".parse().unwrap(),
171 ipv6: "fd7a::1/128".parse().unwrap(),
172 },
173 node_key: [0u8; 32].into(),
174 node_key_expiry: None,
175 online: None,
176 last_seen: None,
177 key_signature: vec![],
178 machine_key: None,
179 disco_key: None,
180 accepted_routes: vec![],
181 underlay_addresses: vec![],
182 derp_region: None,
183 cap: Default::default(),
184 cap_map,
185 peerapi_port: None,
186 peerapi_dns_proxy: false,
187 is_wireguard_only: false,
188 exit_node_dns_resolvers: vec![],
189 peer_relay: false,
190 ssh_host_keys: vec![],
191 service_vips,
192 }
193 }
194
195 fn vip4() -> IpAddr {
196 IpAddr::V4(Ipv4Addr::new(100, 65, 32, 1))
197 }
198
199 #[test]
200 fn tagged_host_with_vip_resolves_listen_addr() {
201 let n = node(&["tag:samba"], "svc:samba", vec![vip4()]);
202 let addr = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
203 .expect("a tagged host with an assigned VIP must resolve");
204 assert_eq!(addr, core::net::SocketAddr::new(vip4(), 445));
205 }
206
207 #[test]
208 fn http_mode_binds_same_vip_and_port() {
209 let n = node(&["tag:web"], "svc:web", vec![vip4()]);
210 let addr =
211 resolve_service_listen(&n, "svc:web", ServiceMode::Http { port: 8080 }, false).unwrap();
212 assert_eq!(addr, core::net::SocketAddr::new(vip4(), 8080));
213 }
214
215 #[test]
216 fn invalid_name_is_rejected() {
217 let n = node(&["tag:x"], "svc:x", vec![vip4()]);
218 let err =
220 resolve_service_listen(&n, "samba", ServiceMode::Tcp { port: 1 }, false).unwrap_err();
221 assert!(matches!(err, ServiceError::InvalidName(_)));
222 let err = resolve_service_listen(&n, "svc:-bad", ServiceMode::Tcp { port: 1 }, false)
224 .unwrap_err();
225 assert!(matches!(err, ServiceError::InvalidName(_)));
226 }
227
228 #[test]
229 fn untagged_host_is_rejected() {
230 let n = node(&[], "svc:samba", vec![vip4()]);
231 let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
232 .unwrap_err();
233 assert_eq!(err, ServiceError::UntaggedHost);
234 }
235
236 #[test]
237 fn no_assigned_vip_is_rejected() {
238 let n = node(&["tag:samba"], "", vec![]);
239 let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
240 .unwrap_err();
241 assert!(matches!(err, ServiceError::NoAssignedVip(_)));
242 }
243
244 #[test]
245 fn ipv6_vip_only_chosen_when_ipv6_enabled() {
246 let vip6: IpAddr = "fd7a:115c:a1e0::1234".parse().unwrap();
247 let n = node(&["tag:samba"], "svc:samba", vec![vip6]);
248
249 let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
251 .unwrap_err();
252 assert!(matches!(err, ServiceError::NoAssignedVip(_)));
253
254 let addr =
256 resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, true).unwrap();
257 assert_eq!(addr, core::net::SocketAddr::new(vip6, 445));
258 }
259
260 #[test]
261 fn ipv4_vip_preferred_over_ipv6() {
262 let vip6: IpAddr = "fd7a:115c:a1e0::1234".parse().unwrap();
263 let n = node(&["tag:samba"], "svc:samba", vec![vip6, vip4()]);
264 let addr =
265 resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, true).unwrap();
266 assert_eq!(addr, core::net::SocketAddr::new(vip4(), 445));
267 }
268
269 #[test]
270 fn co_hosted_services_bind_their_own_vip() {
271 let vip_a = IpAddr::V4(Ipv4Addr::new(100, 65, 32, 1));
274 let vip_b = IpAddr::V4(Ipv4Addr::new(100, 65, 32, 2));
275 let n = node_multi(
276 &["tag:multi"],
277 &[("svc:a", vec![vip_a]), ("svc:b", vec![vip_b])],
278 );
279
280 let a = resolve_service_listen(&n, "svc:a", ServiceMode::Tcp { port: 1 }, false).unwrap();
281 let b = resolve_service_listen(&n, "svc:b", ServiceMode::Tcp { port: 2 }, false).unwrap();
282 assert_eq!(a, core::net::SocketAddr::new(vip_a, 1));
283 assert_eq!(b, core::net::SocketAddr::new(vip_b, 2));
284
285 let err = resolve_service_listen(&n, "svc:absent", ServiceMode::Tcp { port: 3 }, false)
287 .unwrap_err();
288 assert!(matches!(err, ServiceError::NoAssignedVip(_)));
289 }
290}