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 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 let err =
215 resolve_service_listen(&n, "samba", ServiceMode::Tcp { port: 1 }, false).unwrap_err();
216 assert!(matches!(err, ServiceError::InvalidName(_)));
217 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 let err = resolve_service_listen(&n, "svc:samba", ServiceMode::Tcp { port: 445 }, false)
246 .unwrap_err();
247 assert!(matches!(err, ServiceError::NoAssignedVip(_)));
248
249 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 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 let err = resolve_service_listen(&n, "svc:absent", ServiceMode::Tcp { port: 3 }, false)
282 .unwrap_err();
283 assert!(matches!(err, ServiceError::NoAssignedVip(_)));
284 }
285}