pub fn is_tailscale_url(url: &str) -> bool {
url::Url::parse(url)
.ok()
.and_then(|u| u.host_str().map(|h| h.to_ascii_lowercase()))
.is_some_and(|h| h.ends_with(".ts.net"))
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Exposure {
Loopback,
Internal { url: String },
Tailscale { url: String },
Public { url: String },
}
impl Exposure {
pub fn url(&self) -> Option<&str> {
match self {
Exposure::Loopback => None,
Exposure::Internal { url } | Exposure::Tailscale { url } | Exposure::Public { url } => {
Some(url)
}
}
}
pub fn is_tailscale(&self) -> bool {
matches!(self, Exposure::Tailscale { .. })
}
pub fn tailscale_svc_name(&self) -> Option<String> {
let url = match self {
Exposure::Tailscale { url } => url,
_ => return None,
};
url::Url::parse(url)
.ok()
.and_then(|u| u.host_str().map(|h| h.to_ascii_lowercase()))
.and_then(|h| h.split_once('.').map(|(label, _)| label.to_string()))
}
pub fn kind_str(&self) -> &'static str {
match self {
Exposure::Loopback => "loopback",
Exposure::Internal { .. } => "internal",
Exposure::Tailscale { .. } => "tailscale",
Exposure::Public { .. } => "public",
}
}
pub fn from_url(url: &str) -> Self {
let host = url::Url::parse(url)
.ok()
.and_then(|u| u.host_str().map(|h| h.to_ascii_lowercase()));
match host.as_deref() {
Some(h) if h.ends_with(".internal") => Exposure::Internal {
url: url.to_string(),
},
Some(h) if h.ends_with(".ts.net") => Exposure::Tailscale {
url: url.to_string(),
},
_ => Exposure::Public {
url: url.to_string(),
},
}
}
}
pub fn is_public_url(url: &str) -> bool {
let Some(host) = url::Url::parse(url)
.ok()
.and_then(|u| u.host_str().map(|h| h.to_ascii_lowercase()))
else {
return false;
};
let bare = host
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.unwrap_or(&host);
if bare.parse::<std::net::IpAddr>().is_ok() {
return false;
}
if host == "localhost" {
return false;
}
!(host.ends_with(".internal")
|| host.ends_with(".localhost")
|| host.ends_with(".local")
|| host.ends_with(".ts.net"))
}