From 3a63f3b8400ae1ea9ef972b76121cef68a7feefe Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Sat, 14 Feb 2026 15:34:48 -0700 Subject: [PATCH] feat: add mdns hostname metadata variant and fix vhost routing - Add HostnameMetadata::Mdns variant to distinguish mDNS from private domains - Mark mDNS addresses as private (public: false) since mDNS is local-only - Fall back to null SNI entry when hostname not found in vhost mapping - Simplify public detection in ProxyTarget filter - Pass hostname to update_addresses for mDNS domain name generation --- container-runtime/CLAUDE.md | 20 +++---- .../DockerProcedureContainer.ts | 4 +- .../Systems/SystemForEmbassy/index.ts | 2 +- core/src/net/gateway.rs | 3 +- core/src/net/host/address.rs | 13 +++-- core/src/net/host/mod.rs | 52 +++++++++++++++++-- core/src/net/net_controller.rs | 8 +-- core/src/net/service_interface.rs | 7 ++- core/src/net/tunnel.rs | 6 ++- core/src/net/vhost.rs | 10 ++-- core/src/s9pk/v2/manifest.rs | 2 +- sdk/base/lib/osBindings/HostnameMetadata.ts | 1 + sdk/base/lib/util/getServiceInterface.ts | 6 +-- .../interfaces/interface.service.ts | 8 +-- .../ui/src/app/services/api/api.fixures.ts | 2 +- .../ui/src/app/services/api/mock-patch.ts | 6 +-- 16 files changed, 105 insertions(+), 45 deletions(-) diff --git a/container-runtime/CLAUDE.md b/container-runtime/CLAUDE.md index ad79fd9f7..f0c40840b 100644 --- a/container-runtime/CLAUDE.md +++ b/container-runtime/CLAUDE.md @@ -16,16 +16,16 @@ The container runtime communicates with the host via JSON-RPC over Unix socket. ## `/media/startos/` Directory (mounted by host into container) -| Path | Description | -|------|-------------| -| `volumes//` | Package data volumes (id-mapped, persistent) | -| `assets/` | Read-only assets from s9pk `assets.squashfs` | -| `images//` | Container images (squashfs, used for subcontainers) | -| `images/.env` | Environment variables for image | -| `images/.json` | Image metadata | -| `backup/` | Backup mount point (mounted during backup operations) | -| `rpc/service.sock` | RPC socket (container runtime listens here) | -| `rpc/host.sock` | Host RPC socket (for effects callbacks to host) | +| Path | Description | +| -------------------- | ----------------------------------------------------- | +| `volumes//` | Package data volumes (id-mapped, persistent) | +| `assets/` | Read-only assets from s9pk `assets.squashfs` | +| `images//` | Container images (squashfs, used for subcontainers) | +| `images/.env` | Environment variables for image | +| `images/.json` | Image metadata | +| `backup/` | Backup mount point (mounted during backup operations) | +| `rpc/service.sock` | RPC socket (container runtime listens here) | +| `rpc/host.sock` | Host RPC socket (for effects callbacks to host) | ## S9PK Structure diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index 1a8975317..799f7dae8 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -89,8 +89,8 @@ export class DockerProcedureContainer extends Drop { `${packageId}.embassy`, ...new Set( Object.values(hostInfo?.bindings || {}) - .flatMap((b) => b.addresses.possible) - .map((h) => h.hostname.value), + .flatMap((b) => b.addresses.available) + .map((h) => h.host), ).values(), ] const certChain = await effects.getSslCertificate({ diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index cb7585ac9..32376de1c 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -1245,7 +1245,7 @@ async function updateConfig( : catchFn( () => filled.addressInfo!.filter({ kind: "mdns" })!.hostnames[0] - .hostname.value, + .host, ) || "" mutConfigValue[key] = url } diff --git a/core/src/net/gateway.rs b/core/src/net/gateway.rs index 49534908a..cb4e2ba6b 100644 --- a/core/src/net/gateway.rs +++ b/core/src/net/gateway.rs @@ -1005,9 +1005,10 @@ impl NetworkInterfaceController { .as_network_mut() .as_gateways_mut() .ser(info)?; + let hostname = crate::hostname::Hostname(db.as_public().as_server_info().as_hostname().de()?); let ports = db.as_private().as_available_ports().de()?; for host in all_hosts(db) { - host?.update_addresses(info, &ports)?; + host?.update_addresses(&hostname, info, &ports)?; } Ok(()) }) diff --git a/core/src/net/host/address.rs b/core/src/net/host/address.rs index 2c3754707..8adfb5095 100644 --- a/core/src/net/host/address.rs +++ b/core/src/net/host/address.rs @@ -10,6 +10,7 @@ use ts_rs::TS; use crate::GatewayId; use crate::context::{CliContext, RpcContext}; use crate::db::model::DatabaseModel; +use crate::hostname::Hostname; use crate::net::acme::AcmeProvider; use crate::net::host::{HostApiKind, all_hosts}; use crate::prelude::*; @@ -194,9 +195,10 @@ pub async fn add_public_domain( .as_public_domains_mut() .insert(&fqdn, &PublicDomainConfig { acme, gateway })?; handle_duplicates(db)?; + let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let ports = db.as_private().as_available_ports().de()?; - Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports) + Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports) }) .await .result?; @@ -225,9 +227,10 @@ pub async fn remove_public_domain( Kind::host_for(&inheritance, db)? .as_public_domains_mut() .remove(&fqdn)?; + let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let ports = db.as_private().as_available_ports().de()?; - Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports) + Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports) }) .await .result?; @@ -255,9 +258,10 @@ pub async fn add_private_domain( .upsert(&fqdn, || Ok(BTreeSet::new()))? .mutate(|d| Ok(d.insert(gateway)))?; handle_duplicates(db)?; + let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let ports = db.as_private().as_available_ports().de()?; - Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports) + Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports) }) .await .result?; @@ -276,9 +280,10 @@ pub async fn remove_private_domain( Kind::host_for(&inheritance, db)? .as_private_domains_mut() .mutate(|d| Ok(d.remove(&domain)))?; + let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let ports = db.as_private().as_available_ports().de()?; - Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports) + Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports) }) .await .result?; diff --git a/core/src/net/host/mod.rs b/core/src/net/host/mod.rs index b6b8fb45d..ff5ea5559 100644 --- a/core/src/net/host/mod.rs +++ b/core/src/net/host/mod.rs @@ -13,7 +13,8 @@ use ts_rs::TS; use crate::context::RpcContext; use crate::db::model::DatabaseModel; -use crate::db::model::public::NetworkInterfaceInfo; +use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType}; +use crate::hostname::Hostname; use crate::net::forward::AvailablePorts; use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api}; use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding}; @@ -66,10 +67,13 @@ impl Host { impl Model { pub fn update_addresses( &mut self, + mdns: &Hostname, gateways: &OrdMap, available_ports: &AvailablePorts, ) -> Result<(), Error> { let this = self.destructure_mut(); + + // ips for (_, bind) in this.bindings.as_entries_mut()? { let net = bind.as_net().de()?; let opt = bind.as_options().de()?; @@ -143,6 +147,46 @@ impl Model { } } } + + // mdns + let mdns_host = mdns.local_domain_name(); + let mdns_gateways: BTreeSet = gateways + .iter() + .filter(|(_, g)| { + matches!( + g.ip_info.as_ref().and_then(|i| i.device_type), + Some(NetworkInterfaceType::Ethernet | NetworkInterfaceType::Wireless) + ) + }) + .map(|(id, _)| id.clone()) + .collect(); + if let Some(port) = net.assigned_port.filter(|_| { + opt.secure + .map_or(true, |s| !(s.ssl && opt.add_ssl.is_some())) + }) { + available.insert(HostnameInfo { + ssl: opt.secure.map_or(false, |s| s.ssl), + public: false, + host: mdns_host.clone(), + port: Some(port), + metadata: HostnameMetadata::Mdns { + gateways: mdns_gateways.clone(), + }, + }); + } + if let Some(mut port) = net.assigned_ssl_port { + available.insert(HostnameInfo { + ssl: true, + public: false, + host: mdns_host, + port: Some(port), + metadata: HostnameMetadata::Mdns { + gateways: mdns_gateways, + }, + }); + } + + // public domains for (domain, info) in this.public_domains.de()? { let metadata = HostnameMetadata::PublicDomain { gateway: info.gateway.clone(), @@ -173,12 +217,14 @@ impl Model { available.insert(HostnameInfo { ssl: true, public: true, - host: domain.clone(), + host: domain, port: Some(port), metadata, }); } } + + // private domains for (domain, domain_gateways) in this.private_domains.de()? { if let Some(port) = net.assigned_port.filter(|_| { opt.secure @@ -213,7 +259,7 @@ impl Model { available.insert(HostnameInfo { ssl: true, public: true, - host: domain.clone(), + host: domain, port: Some(port), metadata: HostnameMetadata::PrivateDomain { gateways: domain_gateways, diff --git a/core/src/net/net_controller.rs b/core/src/net/net_controller.rs index 1b6d87b9f..600c12ccb 100644 --- a/core/src/net/net_controller.rs +++ b/core/src/net/net_controller.rs @@ -539,10 +539,11 @@ impl NetService { .as_network() .as_gateways() .de()?; + let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let mut ports = db.as_private().as_available_ports().de()?; let host = host_for(db, pkg_id.as_ref(), &id)?; host.add_binding(&mut ports, internal_port, options)?; - host.update_addresses(&gateways, &ports)?; + host.update_addresses(&hostname, &gateways, &ports)?; db.as_private_mut().as_available_ports_mut().ser(&ports)?; Ok(()) }) @@ -563,6 +564,7 @@ impl NetService { .as_network() .as_gateways() .de()?; + let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let ports = db.as_private().as_available_ports().de()?; if let Some(ref pkg_id) = pkg_id { for (host_id, host) in db @@ -584,7 +586,7 @@ impl NetService { } Ok(()) })?; - host.update_addresses(&gateways, &ports)?; + host.update_addresses(&hostname, &gateways, &ports)?; } } else { let host = db @@ -603,7 +605,7 @@ impl NetService { } Ok(()) })?; - host.update_addresses(&gateways, &ports)?; + host.update_addresses(&hostname, &gateways, &ports)?; } Ok(()) }) diff --git a/core/src/net/service_interface.rs b/core/src/net/service_interface.rs index 73a201326..48c167bad 100644 --- a/core/src/net/service_interface.rs +++ b/core/src/net/service_interface.rs @@ -32,6 +32,9 @@ pub enum HostnameMetadata { gateway: GatewayId, scope_id: u32, }, + Mdns { + gateways: BTreeSet, + }, PrivateDomain { gateways: BTreeSet, }, @@ -67,7 +70,9 @@ impl HostnameMetadata { Self::Ipv4 { gateway } | Self::Ipv6 { gateway, .. } | Self::PublicDomain { gateway } => Box::new(std::iter::once(gateway)), - Self::PrivateDomain { gateways } => Box::new(gateways.iter()), + Self::PrivateDomain { gateways } | Self::Mdns { gateways } => { + Box::new(gateways.iter()) + } Self::Plugin { .. } => Box::new(std::iter::empty()), } } diff --git a/core/src/net/tunnel.rs b/core/src/net/tunnel.rs index 61b977f3c..117638851 100644 --- a/core/src/net/tunnel.rs +++ b/core/src/net/tunnel.rs @@ -175,13 +175,14 @@ pub async fn remove_tunnel( ctx.db .mutate(|db| { + let hostname = crate::hostname::Hostname(db.as_public().as_server_info().as_hostname().de()?); let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let ports = db.as_private().as_available_ports().de()?; for host in all_hosts(db) { let host = host?; host.as_public_domains_mut() .mutate(|p| Ok(p.retain(|_, v| v.gateway != id)))?; - host.update_addresses(&gateways, &ports)?; + host.update_addresses(&hostname, &gateways, &ports)?; } Ok(()) @@ -193,6 +194,7 @@ pub async fn remove_tunnel( ctx.db .mutate(|db| { + let hostname = crate::hostname::Hostname(db.as_public().as_server_info().as_hostname().de()?); let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let ports = db.as_private().as_available_ports().de()?; for host in all_hosts(db) { @@ -204,7 +206,7 @@ pub async fn remove_tunnel( d.retain(|_, gateways| !gateways.is_empty()); Ok(()) })?; - host.update_addresses(&gateways, &ports)?; + host.update_addresses(&hostname, &gateways, &ports)?; } Ok(()) diff --git a/core/src/net/vhost.rs b/core/src/net/vhost.rs index 9023576c3..85054e62b 100644 --- a/core/src/net/vhost.rs +++ b/core/src/net/vhost.rs @@ -278,8 +278,7 @@ impl Accept for VHostBindListener { cx: &mut std::task::Context<'_>, ) -> Poll> { // Update listeners when ip_info or bind_reqs change - while self.ip_info.poll_changed(cx).is_ready() - || self.bind_reqs.poll_changed(cx).is_ready() + while self.ip_info.poll_changed(cx).is_ready() || self.bind_reqs.poll_changed(cx).is_ready() { let reqs = self.bind_reqs.read_and_mark_seen(); let listeners = &mut self.listeners; @@ -506,10 +505,8 @@ where }; let src = tcp.peer_addr.ip(); - // Public if: source is a gateway/router IP (NAT'd internet), - // or source is outside all known subnets (direct internet) - let is_public = ip_info.lan_ip.contains(&src) - || !ip_info.subnets.iter().any(|s| s.contains(&src)); + // Public: source is outside all known subnets (direct internet) + let is_public = !ip_info.subnets.iter().any(|s| s.contains(&src)); if is_public { self.public.contains(&gw.id) @@ -695,6 +692,7 @@ where let (target, rc) = self.0.peek(|m| { m.get(&hello.server_name().map(InternedString::from)) + .or_else(|| m.get(&None)) .into_iter() .flatten() .filter(|(_, rc)| rc.strong_count() > 0) diff --git a/core/src/s9pk/v2/manifest.rs b/core/src/s9pk/v2/manifest.rs index 00e57f18c..164c91cb2 100644 --- a/core/src/s9pk/v2/manifest.rs +++ b/core/src/s9pk/v2/manifest.rs @@ -240,7 +240,7 @@ impl LocaleString { pub fn localize(&mut self) { self.localize_for(&*rust_i18n::locale()); } - pub fn localized(mut self) -> String { + pub fn localized(self) -> String { self.localized_for(&*rust_i18n::locale()) } } diff --git a/sdk/base/lib/osBindings/HostnameMetadata.ts b/sdk/base/lib/osBindings/HostnameMetadata.ts index 67e2d687b..c5d929730 100644 --- a/sdk/base/lib/osBindings/HostnameMetadata.ts +++ b/sdk/base/lib/osBindings/HostnameMetadata.ts @@ -5,6 +5,7 @@ import type { PackageId } from './PackageId' export type HostnameMetadata = | { kind: 'ipv4'; gateway: GatewayId } | { kind: 'ipv6'; gateway: GatewayId; scopeId: number } + | { kind: 'mdns'; gateways: Array } | { kind: 'private-domain'; gateways: Array } | { kind: 'public-domain'; gateway: GatewayId } | { kind: 'plugin'; package: PackageId } diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts index cb86c96c0..82510bb04 100644 --- a/sdk/base/lib/util/getServiceInterface.ts +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -49,7 +49,7 @@ type VisibilityFilter = V extends 'public' : never type KindFilter = K extends 'mdns' ? - | (HostnameInfo & { metadata: { kind: 'private-domain' } }) + | (HostnameInfo & { metadata: { kind: 'mdns' } }) | KindFilter> : K extends 'domain' ? @@ -199,9 +199,7 @@ function filterRec( hostnames = hostnames.filter( (h) => invert !== - ((kind.has('mdns') && - h.metadata.kind === 'private-domain' && - h.host.endsWith('.local')) || + ((kind.has('mdns') && h.metadata.kind === 'mdns') || (kind.has('domain') && (h.metadata.kind === 'private-domain' || h.metadata.kind === 'public-domain')) || diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts index 17409e39c..e6414b0ce 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts @@ -30,6 +30,7 @@ function getGatewayIds(h: T.HostnameInfo): string[] { case 'ipv6': case 'public-domain': return [h.metadata.gateway] + case 'mdns': case 'private-domain': return h.metadata.gateways case 'plugin': @@ -45,8 +46,10 @@ function getAddressType(h: T.HostnameInfo): string { return 'IPv6' case 'public-domain': return 'Public Domain' + case 'mdns': + return 'mDNS' case 'private-domain': - return h.host.endsWith('.local') ? 'mDNS' : 'Private Domain' + return 'Private Domain' case 'plugin': return 'Plugin' } @@ -84,8 +87,7 @@ export class InterfaceService { const isDomain = h.metadata.kind === 'private-domain' || h.metadata.kind === 'public-domain' - const isMdns = - h.metadata.kind === 'private-domain' && h.host.endsWith('.local') + const isMdns = h.metadata.kind === 'mdns' const address: GatewayAddress = { enabled, diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 31fd0111f..dae769a7e 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -2137,7 +2137,7 @@ export namespace Mock { host: 'adjective-noun.local', port: 1234, metadata: { - kind: 'private-domain', + kind: 'mdns', gateways: ['eth0', 'wlan0'], }, }, diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 5cc33064b..911ba6a68 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -49,7 +49,7 @@ export const mockPatchData: DataModel = { host: 'adjective-noun.local', port: 443, metadata: { - kind: 'private-domain', + kind: 'mdns', gateways: ['eth0', 'wlan0'], }, }, @@ -515,7 +515,7 @@ export const mockPatchData: DataModel = { host: 'adjective-noun.local', port: 443, metadata: { - kind: 'private-domain', + kind: 'mdns', gateways: ['eth0'], }, }, @@ -622,7 +622,7 @@ export const mockPatchData: DataModel = { host: 'adjective-noun.local', port: 8332, metadata: { - kind: 'private-domain', + kind: 'mdns', gateways: ['eth0'], }, },