From 3845550e90d85ee8823e2dba46d81a3689604db0 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 7 Aug 2025 17:15:23 -0600 Subject: [PATCH] best address logic --- core/startos/src/net/host/address.rs | 8 +- core/startos/src/net/net_controller.rs | 6 +- core/startos/src/net/service_interface.rs | 14 +- sdk/base/lib/osBindings/IpHostname.ts | 3 +- sdk/base/lib/util/Hostname.ts | 25 --- sdk/base/lib/util/getServiceInterface.ts | 5 +- sdk/base/lib/util/index.ts | 2 +- sdk/base/lib/util/ip.ts | 2 + .../components/interfaces/interface.utils.ts | 161 +++++++++++------- 9 files changed, 119 insertions(+), 107 deletions(-) delete mode 100644 sdk/base/lib/util/Hostname.ts diff --git a/core/startos/src/net/host/address.rs b/core/startos/src/net/host/address.rs index f35a03696..1eb39d26d 100644 --- a/core/startos/src/net/host/address.rs +++ b/core/startos/src/net/host/address.rs @@ -194,7 +194,12 @@ pub async fn add_domain( .as_domains() .keys()? .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}"))?; if let Some(acme) = &acme { @@ -209,7 +214,6 @@ pub async fn add_domain( } } - Kind::host_for(&inheritance, db)?.as_domains_mut().insert( domain, &DomainConfig { diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index 3373d6f3d..a86c5a4cf 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -385,8 +385,7 @@ impl NetServiceData { gateway_id: interface.clone(), public, // TODO: check if port forward is active hostname: IpHostname::Domain { - domain: address.clone(), - subdomain: None, + value: address.clone(), port: None, ssl_port: Some(443), }, @@ -396,8 +395,7 @@ impl NetServiceData { gateway_id: interface.clone(), public, hostname: IpHostname::Domain { - domain: address.clone(), - subdomain: None, + value: address.clone(), port: bind.net.assigned_port, ssl_port: bind.net.assigned_ssl_port, }, diff --git a/core/startos/src/net/service_interface.rs b/core/startos/src/net/service_interface.rs index a56c5e8c2..0b37c4ad8 100644 --- a/core/startos/src/net/service_interface.rs +++ b/core/startos/src/net/service_interface.rs @@ -72,9 +72,7 @@ pub enum IpHostname { }, Domain { #[ts(type = "string")] - domain: InternedString, - #[ts(type = "string | null")] - subdomain: Option, + value: InternedString, port: Option, ssl_port: Option, }, @@ -85,15 +83,7 @@ impl IpHostname { Self::Ipv4 { value, .. } => InternedString::from_display(value), Self::Ipv6 { value, .. } => InternedString::from_display(value), Self::Local { value, .. } => value.clone(), - Self::Domain { - domain, subdomain, .. - } => { - if let Some(subdomain) = subdomain { - InternedString::from_display(&lazy_format!("{subdomain}.{domain}")) - } else { - domain.clone() - } - } + Self::Domain { value, .. } => value.clone(), } } } diff --git a/sdk/base/lib/osBindings/IpHostname.ts b/sdk/base/lib/osBindings/IpHostname.ts index 9b3ddd6d1..0fb7c3f3c 100644 --- a/sdk/base/lib/osBindings/IpHostname.ts +++ b/sdk/base/lib/osBindings/IpHostname.ts @@ -17,8 +17,7 @@ export type IpHostname = } | { kind: "domain" - domain: string - subdomain: string | null + value: string port: number | null sslPort: number | null } diff --git a/sdk/base/lib/util/Hostname.ts b/sdk/base/lib/util/Hostname.ts deleted file mode 100644 index ee68abe4f..000000000 --- a/sdk/base/lib/util/Hostname.ts +++ /dev/null @@ -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), - ) -} diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts index d51fb8880..037a95854 100644 --- a/sdk/base/lib/util/getServiceInterface.ts +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -3,6 +3,7 @@ import { knownProtocols } from "../interfaces/Host" import { AddressInfo, Host, Hostname, HostnameInfo } from "../types" import { Effects } from "../Effects" import { DropGenerator, DropPromise } from "./Drop" +import { IPV6_LINK_LOCAL } from "./ip" export type UrlString = string export type HostId = string @@ -95,9 +96,9 @@ export const addressHostToUrl = ( hostname = host.hostname.value } else if (host.kind === "ip") { 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") { - hostname = host.hostname.value.startsWith("fe80::") + hostname = IPV6_LINK_LOCAL.contains(host.hostname.value) ? `[${host.hostname.value}%${host.hostname.scopeId}]` : `[${host.hostname.value}]` } else { diff --git a/sdk/base/lib/util/index.ts b/sdk/base/lib/util/index.ts index 2f3e981e6..c4c900192 100644 --- a/sdk/base/lib/util/index.ts +++ b/sdk/base/lib/util/index.ts @@ -1,6 +1,7 @@ /// Currently being used export { addressHostToUrl } from "./getServiceInterface" export { getDefaultString } from "./getDefaultString" +export * from "./ip" /// Not being used, but known to be browser compatible export { GetServiceInterface, getServiceInterface } from "./getServiceInterface" @@ -16,6 +17,5 @@ export { splitCommand } from "./splitCommand" export { nullIfEmpty } from "./nullIfEmpty" export { deepMerge, partialDiff } from "./deepMerge" export { deepEqual } from "./deepEqual" -export { hostnameInfoToAddress } from "./Hostname" export * as regexes from "./regexes" export { stringFromStdErrOut } from "./stringFromStdErrOut" diff --git a/sdk/base/lib/util/ip.ts b/sdk/base/lib/util/ip.ts index 1dfcd2b8b..6d2bd72b6 100644 --- a/sdk/base/lib/util/ip.ts +++ b/sdk/base/lib/util/ip.ts @@ -73,3 +73,5 @@ export const PRIVATE_IPV4_RANGES = [ new IpNet("172.16.0.0/12"), new IpNet("192.168.0.0/16"), ] + +export const IPV6_LINK_LOCAL = new IpNet("fe80::/10") diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts index 06fe430cf..67461a041 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts @@ -7,12 +7,24 @@ type AddressWithInfo = { info: T.HostnameInfo } -function filterTor(a: AddressWithInfo): a is (AddressWithInfo & { info: { kind: 'onion' } }) { - return a.info.kind === 'onion' +function cmpWithRankedPredicates( + 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 { - if (!filterTor(a) || !filterTor(b)) return 0 +type TorAddress = AddressWithInfo & { info: { kind: 'onion' } } +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]) { if (x.address.protocol === 'http:' && y.address.protocol === 'https:') return sign @@ -20,17 +32,68 @@ function cmpTor(a: AddressWithInfo, b: AddressWithInfo): -1 | 0 | 1 { 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 } +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 { - if (!filterLan(a) || !filterLan(b)) return 0 - for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) { - if (x.info.kind === 'domain' && host.domains.) - return sign +type VpnAddress = AddressWithInfo & { + info: { + kind: 'ip' + public: false + 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, + 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({ @@ -40,6 +103,7 @@ export class InterfaceService { private readonly config = inject(ConfigService) getAddresses( + serverDomains: Record, serviceInterface: T.ServiceInterface, host: T.Host, ): MappedServiceInterface['addresses'] { @@ -52,56 +116,35 @@ export class InterfaceService { if (!hostnamesInfos.length) return addresses - hostnamesInfos.forEach(h => { - const addresses = utils.addressHostToUrl(serviceInterface.addressInfo, h) + const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(h => + utils + .addressHostToUrl(serviceInterface.addressInfo, h) + .map(a => ({ address: new URL(a), info: h })), + ) - addresses.forEach(url => { - if (h.kind === 'onion') { - tor.push({ - protocol: /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url) - ? new URL(url).protocol.replace(':', '').toUpperCase() - : null, - url, - }) - } else { - const hostnameKind = h.hostname.kind + const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor) + const lanAddrs = allAddressesWithInfo + .filter(filterLan) + .sort((a, b) => cmpLan(host, a, b)) + const vpnAddrs = allAddressesWithInfo + .filter(filterVpn) + .sort((a, b) => cmpVpn(host, a, b)) + const clearnetAddrs = allAddressesWithInfo + .filter(filterClearnet) + .sort((a, b) => cmpClearnet(serverDomains, host, a, b)) - if ( - h.public || - (hostnameKind === 'domain' && - host.domains[h.hostname.domain]?.public) - ) { - clearnet.push({ - 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, - }) - } - } - }) - }) + let bestAddrs = [clearnetAddrs[0], lanAddrs[0], vpnAddrs[0], torAddrs[0]] + .filter(a => !!a) + .reduce((acc, x) => { + if (!acc.includes(x)) acc.push(x) + return acc + }, [] as AddressWithInfo[]) return { - common: common.filter( - (value, index, self) => - index === self.findIndex(t => t.url === value.url), - ), - uncommon: uncommon.filter( - (value, index, self) => - index === self.findIndex(t => t.url === value.url), - ), + common: bestAddrs.map(toDisplayAddress), + uncommon: allAddressesWithInfo + .filter(a => !bestAddrs.includes(a)) + .map(toDisplayAddress), } } @@ -158,7 +201,7 @@ export class InterfaceService { .filter(h => h.kind === 'domain') .map(h => h as T.IpHostname & { kind: 'domain' }) .map(h => ({ - value: h.domain, + value: h.value, sslPort: h.sslPort, port: h.port, }))[0] @@ -235,7 +278,7 @@ export class InterfaceService { !( h.kind === 'ip' && ((h.hostname.kind === 'ipv6' && - h.hostname.value.startsWith('fe80::')) || + utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) || h.gatewayId === 'lo') ), ) || []