best address logic

This commit is contained in:
Aiden McClelland
2025-08-07 17:15:23 -06:00
parent 4d5ff1a97b
commit 3845550e90
9 changed files with 119 additions and 107 deletions

View File

@@ -194,7 +194,12 @@ pub async fn add_domain<Kind: HostApiKind>(
.as_domains() .as_domains()
.keys()? .keys()?
.into_iter() .into_iter()
.find(|root| domain.ends_with(&**root)) .find(|root| {
domain == root
|| domain
.strip_suffix(&**root)
.map_or(false, |d| d.ends_with("."))
})
.or_not_found(lazy_format!("root domain for {domain}"))?; .or_not_found(lazy_format!("root domain for {domain}"))?;
if let Some(acme) = &acme { if let Some(acme) = &acme {
@@ -209,7 +214,6 @@ pub async fn add_domain<Kind: HostApiKind>(
} }
} }
Kind::host_for(&inheritance, db)?.as_domains_mut().insert( Kind::host_for(&inheritance, db)?.as_domains_mut().insert(
domain, domain,
&DomainConfig { &DomainConfig {

View File

@@ -385,8 +385,7 @@ impl NetServiceData {
gateway_id: interface.clone(), gateway_id: interface.clone(),
public, // TODO: check if port forward is active public, // TODO: check if port forward is active
hostname: IpHostname::Domain { hostname: IpHostname::Domain {
domain: address.clone(), value: address.clone(),
subdomain: None,
port: None, port: None,
ssl_port: Some(443), ssl_port: Some(443),
}, },
@@ -396,8 +395,7 @@ impl NetServiceData {
gateway_id: interface.clone(), gateway_id: interface.clone(),
public, public,
hostname: IpHostname::Domain { hostname: IpHostname::Domain {
domain: address.clone(), value: address.clone(),
subdomain: None,
port: bind.net.assigned_port, port: bind.net.assigned_port,
ssl_port: bind.net.assigned_ssl_port, ssl_port: bind.net.assigned_ssl_port,
}, },

View File

@@ -72,9 +72,7 @@ pub enum IpHostname {
}, },
Domain { Domain {
#[ts(type = "string")] #[ts(type = "string")]
domain: InternedString, value: InternedString,
#[ts(type = "string | null")]
subdomain: Option<InternedString>,
port: Option<u16>, port: Option<u16>,
ssl_port: Option<u16>, ssl_port: Option<u16>,
}, },
@@ -85,15 +83,7 @@ impl IpHostname {
Self::Ipv4 { value, .. } => InternedString::from_display(value), Self::Ipv4 { value, .. } => InternedString::from_display(value),
Self::Ipv6 { value, .. } => InternedString::from_display(value), Self::Ipv6 { value, .. } => InternedString::from_display(value),
Self::Local { value, .. } => value.clone(), Self::Local { value, .. } => value.clone(),
Self::Domain { Self::Domain { value, .. } => value.clone(),
domain, subdomain, ..
} => {
if let Some(subdomain) = subdomain {
InternedString::from_display(&lazy_format!("{subdomain}.{domain}"))
} else {
domain.clone()
}
}
} }
} }
} }

View File

@@ -17,8 +17,7 @@ export type IpHostname =
} }
| { | {
kind: "domain" kind: "domain"
domain: string value: string
subdomain: string | null
port: number | null port: number | null
sslPort: number | null sslPort: number | null
} }

View File

@@ -1,25 +0,0 @@
import { HostnameInfo } from "../types"
export function hostnameInfoToAddress(hostInfo: HostnameInfo): string {
if (hostInfo.kind === "onion") {
return `${hostInfo.hostname.value}`
}
if (hostInfo.kind !== "ip") {
throw Error("Expecting that the kind is ip.")
}
const hostname = hostInfo.hostname
if (hostname.kind === "domain") {
return `${hostname.subdomain ? `${hostname.subdomain}.` : ""}${hostname.domain}`
}
const port = hostname.sslPort || hostname.port
const portString = port ? `:${port}` : ""
if ("ipv4" === hostname.kind || "ipv6" === hostname.kind) {
return `${hostname.value}${portString}`
}
if ("local" === hostname.kind) {
return `${hostname.value}${portString}`
}
throw Error(
"Expecting to have a valid hostname kind." + JSON.stringify(hostname),
)
}

View File

@@ -3,6 +3,7 @@ import { knownProtocols } from "../interfaces/Host"
import { AddressInfo, Host, Hostname, HostnameInfo } from "../types" import { AddressInfo, Host, Hostname, HostnameInfo } from "../types"
import { Effects } from "../Effects" import { Effects } from "../Effects"
import { DropGenerator, DropPromise } from "./Drop" import { DropGenerator, DropPromise } from "./Drop"
import { IPV6_LINK_LOCAL } from "./ip"
export type UrlString = string export type UrlString = string
export type HostId = string export type HostId = string
@@ -95,9 +96,9 @@ export const addressHostToUrl = (
hostname = host.hostname.value hostname = host.hostname.value
} else if (host.kind === "ip") { } else if (host.kind === "ip") {
if (host.hostname.kind === "domain") { if (host.hostname.kind === "domain") {
hostname = `${host.hostname.subdomain ? `${host.hostname.subdomain}.` : ""}${host.hostname.domain}` hostname = host.hostname.value
} else if (host.hostname.kind === "ipv6") { } else if (host.hostname.kind === "ipv6") {
hostname = host.hostname.value.startsWith("fe80::") hostname = IPV6_LINK_LOCAL.contains(host.hostname.value)
? `[${host.hostname.value}%${host.hostname.scopeId}]` ? `[${host.hostname.value}%${host.hostname.scopeId}]`
: `[${host.hostname.value}]` : `[${host.hostname.value}]`
} else { } else {

View File

@@ -1,6 +1,7 @@
/// Currently being used /// Currently being used
export { addressHostToUrl } from "./getServiceInterface" export { addressHostToUrl } from "./getServiceInterface"
export { getDefaultString } from "./getDefaultString" export { getDefaultString } from "./getDefaultString"
export * from "./ip"
/// Not being used, but known to be browser compatible /// Not being used, but known to be browser compatible
export { GetServiceInterface, getServiceInterface } from "./getServiceInterface" export { GetServiceInterface, getServiceInterface } from "./getServiceInterface"
@@ -16,6 +17,5 @@ export { splitCommand } from "./splitCommand"
export { nullIfEmpty } from "./nullIfEmpty" export { nullIfEmpty } from "./nullIfEmpty"
export { deepMerge, partialDiff } from "./deepMerge" export { deepMerge, partialDiff } from "./deepMerge"
export { deepEqual } from "./deepEqual" export { deepEqual } from "./deepEqual"
export { hostnameInfoToAddress } from "./Hostname"
export * as regexes from "./regexes" export * as regexes from "./regexes"
export { stringFromStdErrOut } from "./stringFromStdErrOut" export { stringFromStdErrOut } from "./stringFromStdErrOut"

View File

@@ -73,3 +73,5 @@ export const PRIVATE_IPV4_RANGES = [
new IpNet("172.16.0.0/12"), new IpNet("172.16.0.0/12"),
new IpNet("192.168.0.0/16"), new IpNet("192.168.0.0/16"),
] ]
export const IPV6_LINK_LOCAL = new IpNet("fe80::/10")

View File

@@ -7,12 +7,24 @@ type AddressWithInfo = {
info: T.HostnameInfo info: T.HostnameInfo
} }
function filterTor(a: AddressWithInfo): a is (AddressWithInfo & { info: { kind: 'onion' } }) { function cmpWithRankedPredicates<T extends AddressWithInfo>(
return a.info.kind === 'onion' a: T,
b: T,
preds: ((x: T) => boolean)[],
): -1 | 0 | 1 {
for (const pred of preds) {
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
if (pred(x) && !pred(y)) return sign
}
}
return 0
} }
function cmpTor(a: AddressWithInfo, b: AddressWithInfo): -1 | 0 | 1 { type TorAddress = AddressWithInfo & { info: { kind: 'onion' } }
if (!filterTor(a) || !filterTor(b)) return 0 function filterTor(a: AddressWithInfo): a is TorAddress {
return a.info.kind === 'onion'
}
function cmpTor(a: TorAddress, b: TorAddress): -1 | 0 | 1 {
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) { for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
if (x.address.protocol === 'http:' && y.address.protocol === 'https:') if (x.address.protocol === 'http:' && y.address.protocol === 'https:')
return sign return sign
@@ -20,17 +32,68 @@ function cmpTor(a: AddressWithInfo, b: AddressWithInfo): -1 | 0 | 1 {
return 0 return 0
} }
function filterLan(a: AddressWithInfo): a is (AddressWithInfo & { info: { kind: 'ip', public: false } }) { type LanAddress = AddressWithInfo & { info: { kind: 'ip'; public: false } }
function filterLan(a: AddressWithInfo): a is LanAddress {
return a.info.kind === 'ip' && !a.info.public return a.info.kind === 'ip' && !a.info.public
} }
function cmpLan(host: T.Host, a: LanAddress, b: LanAddress): -1 | 0 | 1 {
return cmpWithRankedPredicates(a, b, [
x =>
x.info.hostname.kind === 'domain' &&
!host.domains[x.info.hostname.value]?.public, // private domain
x => x.info.hostname.kind === 'local', // .local
x => x.info.hostname.kind === 'ipv4', // ipv4
])
}
function cmpLan(host: T.Host, a: AddressWithInfo, b: AddressWithInfo): -1 | 0 | 1 { type VpnAddress = AddressWithInfo & {
if (!filterLan(a) || !filterLan(b)) return 0 info: {
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) { kind: 'ip'
if (x.info.kind === 'domain' && host.domains.) public: false
return sign hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
} }
return 0 }
function filterVpn(a: AddressWithInfo): a is VpnAddress {
return (
a.info.kind === 'ip' && !a.info.public && a.info.hostname.kind !== 'local'
)
}
function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 {
return cmpWithRankedPredicates(a, b, [
x =>
x.info.hostname.kind === 'domain' &&
!host.domains[x.info.hostname.value]?.public, // private domain
x => x.info.hostname.kind === 'ipv4', // ipv4
])
}
type ClearnetAddress = AddressWithInfo & {
info: {
kind: 'ip'
public: true
hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
}
}
function filterClearnet(a: AddressWithInfo): a is ClearnetAddress {
return a.info.kind === 'ip' && a.info.public
}
function cmpClearnet(
domains: Record<string, T.DomainSettings>,
host: T.Host,
a: ClearnetAddress,
b: ClearnetAddress,
): -1 | 0 | 1 {
return cmpWithRankedPredicates(a, b, [
x =>
x.info.hostname.kind === 'domain' &&
x.info.gatewayId ===
domains[host.domains[x.info.hostname.value]?.root!]?.gateway, // public domain for this gateway
x => x.info.hostname.kind === 'ipv4', // ipv4
])
}
function toDisplayAddress(a: AddressWithInfo): Address {
throw new Error('@TODO: MattHill')
} }
@Injectable({ @Injectable({
@@ -40,6 +103,7 @@ export class InterfaceService {
private readonly config = inject(ConfigService) private readonly config = inject(ConfigService)
getAddresses( getAddresses(
serverDomains: Record<string, T.DomainSettings>,
serviceInterface: T.ServiceInterface, serviceInterface: T.ServiceInterface,
host: T.Host, host: T.Host,
): MappedServiceInterface['addresses'] { ): MappedServiceInterface['addresses'] {
@@ -52,56 +116,35 @@ export class InterfaceService {
if (!hostnamesInfos.length) return addresses if (!hostnamesInfos.length) return addresses
hostnamesInfos.forEach(h => { const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(h =>
const addresses = utils.addressHostToUrl(serviceInterface.addressInfo, h) utils
.addressHostToUrl(serviceInterface.addressInfo, h)
.map(a => ({ address: new URL(a), info: h })),
)
addresses.forEach(url => { const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor)
if (h.kind === 'onion') { const lanAddrs = allAddressesWithInfo
tor.push({ .filter(filterLan)
protocol: /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url) .sort((a, b) => cmpLan(host, a, b))
? new URL(url).protocol.replace(':', '').toUpperCase() const vpnAddrs = allAddressesWithInfo
: null, .filter(filterVpn)
url, .sort((a, b) => cmpVpn(host, a, b))
}) const clearnetAddrs = allAddressesWithInfo
} else { .filter(filterClearnet)
const hostnameKind = h.hostname.kind .sort((a, b) => cmpClearnet(serverDomains, host, a, b))
if ( let bestAddrs = [clearnetAddrs[0], lanAddrs[0], vpnAddrs[0], torAddrs[0]]
h.public || .filter(a => !!a)
(hostnameKind === 'domain' && .reduce((acc, x) => {
host.domains[h.hostname.domain]?.public) if (!acc.includes(x)) acc.push(x)
) { return acc
clearnet.push({ }, [] as AddressWithInfo[])
url,
disabled: !h.public,
isDomain: hostnameKind == 'domain',
authority:
hostnameKind == 'domain'
? host.domains[h.hostname.domain]?.acme || null
: null,
})
} else {
local.push({
nid:
hostnameKind === 'local'
? 'Local'
: `${h.gatewayId} (${hostnameKind})`,
url,
})
}
}
})
})
return { return {
common: common.filter( common: bestAddrs.map(toDisplayAddress),
(value, index, self) => uncommon: allAddressesWithInfo
index === self.findIndex(t => t.url === value.url), .filter(a => !bestAddrs.includes(a))
), .map(toDisplayAddress),
uncommon: uncommon.filter(
(value, index, self) =>
index === self.findIndex(t => t.url === value.url),
),
} }
} }
@@ -158,7 +201,7 @@ export class InterfaceService {
.filter(h => h.kind === 'domain') .filter(h => h.kind === 'domain')
.map(h => h as T.IpHostname & { kind: 'domain' }) .map(h => h as T.IpHostname & { kind: 'domain' })
.map(h => ({ .map(h => ({
value: h.domain, value: h.value,
sslPort: h.sslPort, sslPort: h.sslPort,
port: h.port, port: h.port,
}))[0] }))[0]
@@ -235,7 +278,7 @@ export class InterfaceService {
!( !(
h.kind === 'ip' && h.kind === 'ip' &&
((h.hostname.kind === 'ipv6' && ((h.hostname.kind === 'ipv6' &&
h.hostname.value.startsWith('fe80::')) || utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) ||
h.gatewayId === 'lo') h.gatewayId === 'lo')
), ),
) || [] ) || []