From d6dfaf8feb9ac834efc726fda146f639b2e99396 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Wed, 6 Aug 2025 14:29:35 -0600 Subject: [PATCH] domains api + migration --- core/Cargo.lock | 47 +++- core/startos/Cargo.toml | 5 +- core/startos/src/bins/startd.rs | 2 +- core/startos/src/db/model/public.rs | 16 +- core/startos/src/lib.rs | 2 +- core/startos/src/net/acme.rs | 2 +- core/startos/src/net/domain.rs | 204 ++++++++++++++++++ core/startos/src/net/forward.rs | 2 +- .../net/{network_interface.rs => gateway.rs} | 26 +-- core/startos/src/net/host/address.rs | 43 ++-- core/startos/src/net/host/binding.rs | 2 +- core/startos/src/net/host/mod.rs | 2 +- core/startos/src/net/mod.rs | 18 +- core/startos/src/net/net_controller.rs | 8 +- core/startos/src/net/tor.rs | 2 +- core/startos/src/net/tunnel.rs | 4 +- core/startos/src/net/vhost.rs | 2 +- core/startos/src/net/web_server.rs | 2 +- core/startos/src/service/effects/net/ssl.rs | 2 +- core/startos/src/tunnel/context.rs | 2 +- core/startos/src/version/mod.rs | 13 +- core/startos/src/version/v0_3_6_alpha_10.rs | 3 +- core/startos/src/version/v0_4_0_alpha_10.rs | 108 ++++++++++ sdk/base/lib/osBindings/DomainConfig.ts | 6 +- sdk/base/lib/osBindings/DomainSettings.ts | 4 + sdk/base/lib/osBindings/NetworkInfo.ts | 4 +- sdk/base/lib/osBindings/index.ts | 1 + sdk/base/lib/util/ip.ts | 5 +- sdk/package/lib/StartSdk.ts | 2 +- web/package-lock.json | 4 +- web/package.json | 2 +- .../routes/domains/domains/domain.service.ts | 8 +- .../routes/gateways/gateways.component.ts | 2 +- .../ui/src/app/services/api/api.fixures.ts | 10 +- .../ui/src/app/services/api/api.types.ts | 2 +- .../services/api/embassy-live-api.service.ts | 2 +- .../services/api/embassy-mock-api.service.ts | 6 +- .../ui/src/app/services/api/mock-patch.ts | 2 +- .../ui/src/app/services/proxy.service.ts | 6 +- 39 files changed, 496 insertions(+), 87 deletions(-) create mode 100644 core/startos/src/net/domain.rs rename core/startos/src/net/{network_interface.rs => gateway.rs} (98%) create mode 100644 core/startos/src/version/v0_4_0_alpha_10.rs create mode 100644 sdk/base/lib/osBindings/DomainSettings.ts diff --git a/core/Cargo.lock b/core/Cargo.lock index 983caab2d..6d4d6e719 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -1923,6 +1923,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -3811,6 +3817,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nix" version = "0.24.3" @@ -4700,6 +4715,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.7.3" @@ -6060,7 +6085,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "start-os" -version = "0.4.0-alpha.9" +version = "0.4.0-alpha.10" dependencies = [ "aes 0.7.5", "async-acme", @@ -6191,6 +6216,7 @@ dependencies = [ "tracing-futures", "tracing-journald", "tracing-subscriber", + "trust-dns-client", "trust-dns-server", "ts-rs", "typed-builder", @@ -6932,6 +6958,25 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trust-dns-client" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14135e72c7e6d4c9b6902d4437881a8598f0145dbb2e3f86f92dbad845b61e63" +dependencies = [ + "cfg-if", + "data-encoding", + "futures-channel", + "futures-util", + "once_cell", + "radix_trie", + "rand 0.8.5", + "thiserror 1.0.69", + "tokio", + "tracing", + "trust-dns-proto", +] + [[package]] name = "trust-dns-proto" version = "0.23.2" diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 7f3e21f5c..752fc69ae 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -14,7 +14,7 @@ keywords = [ name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.4.0-alpha.9" # VERSION_BUMP +version = "0.4.0-alpha.10" # VERSION_BUMP license = "MIT" [lib] @@ -228,7 +228,8 @@ tracing-error = "0.2.0" tracing-futures = "0.2.5" tracing-journald = "0.3.0" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -trust-dns-server = "0.23.1" +trust-dns-server = "0.23.2" +trust-dns-client = "0.23.2" ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8.1.0" typed-builder = "0.21.0" unix-named-pipe = "0.2.0" diff --git a/core/startos/src/bins/startd.rs b/core/startos/src/bins/startd.rs index 0ec28d67d..6258da9aa 100644 --- a/core/startos/src/bins/startd.rs +++ b/core/startos/src/bins/startd.rs @@ -12,7 +12,7 @@ use tracing::instrument; use crate::context::config::ServerConfig; use crate::context::rpc::InitRpcContextPhases; use crate::context::{DiagnosticContext, InitContext, RpcContext}; -use crate::net::network_interface::SelfContainedNetworkInterfaceListener; +use crate::net::gateway::SelfContainedNetworkInterfaceListener; use crate::net::web_server::{Acceptor, UpgradableListener, WebServer}; use crate::shutdown::Shutdown; use crate::system::launch_metrics_task; diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index d00acbd81..7ff339c0a 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -92,8 +92,9 @@ impl Public { enabled: true, ..Default::default() }, - network_interfaces: OrdMap::new(), + gateways: OrdMap::new(), acme: BTreeMap::new(), + domains: BTreeMap::new(), }, status_info: ServerStatus { backup_progress: None, @@ -191,9 +192,12 @@ pub struct NetworkInfo { pub host: Host, #[ts(as = "BTreeMap::")] #[serde(default)] - pub network_interfaces: OrdMap, + pub gateways: OrdMap, #[serde(default)] pub acme: BTreeMap, + #[serde(default)] + #[ts(as = "BTreeMap::")] + pub domains: BTreeMap, } #[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)] @@ -303,6 +307,14 @@ pub struct AcmeSettings { pub contact: Vec, } +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct DomainSettings { + pub gateway: GatewayId, +} + #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[model = "Model"] #[ts(export)] diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index cfd9125e2..04bfb6d39 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -150,7 +150,7 @@ pub fn main_api() -> ParentHandler { ) .subcommand( "net", - net::net::().with_about("Network commands related to tor and dhcp"), + net::net_api::().with_about("Network commands related to tor and dhcp"), ) .subcommand( "auth", diff --git a/core/startos/src/net/acme.rs b/core/startos/src/net/acme.rs index cf5b5fdf6..3348cc2f6 100644 --- a/core/startos/src/net/acme.rs +++ b/core/startos/src/net/acme.rs @@ -174,7 +174,7 @@ impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> { } } -pub fn acme() -> ParentHandler { +pub fn acme_api() -> ParentHandler { ParentHandler::new() .subcommand( "init", diff --git a/core/startos/src/net/domain.rs b/core/startos/src/net/domain.rs new file mode 100644 index 000000000..f3c0644ec --- /dev/null +++ b/core/startos/src/net/domain.rs @@ -0,0 +1,204 @@ +use std::collections::BTreeMap; + +use clap::Parser; +use futures::TryFutureExt; +use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; +use models::GatewayId; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; + +use crate::context::{CliContext, RpcContext}; +use crate::db::model::public::DomainSettings; +use crate::prelude::*; +use crate::util::new_guid; +use crate::util::serde::{display_serializable, HandlerExtSerde}; + +pub fn domain_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "list", + from_fn_async(list) + .with_display_serializable() + .with_custom_display_fn(|HandlerArgs { params, .. }, res| { + use prettytable::*; + + if let Some(format) = params.format { + return display_serializable(format, res); + } + + let mut table = Table::new(); + table.add_row(row![bc => "DOMAIN", "GATEWAY"]); + for (domain, info) in res { + table.add_row(row![domain, info.gateway]); + } + + table.print_tty(false)?; + + Ok(()) + }) + .with_about("List domains available to StartOS") + .with_call_remote::(), + ) + .subcommand( + "add", + from_fn_async(add) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Add a domain for use with StartOS") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Remove a domain for use with StartOS") + .with_call_remote::(), + ) + .subcommand( + "test-dns", + from_fn_async(test_dns) + .with_display_serializable() + .with_custom_display_fn(|HandlerArgs { params, .. }, res| { + use prettytable::*; + + if let Some(format) = params.format { + return display_serializable(format, res); + } + + let mut table = Table::new(); + table.add_row(row![bc -> "ROOT", if res.root { "✅️" } else { "❌️" }]); + table.add_row(row![bc -> "WILDCARD", if res.wildcard { "✅️" } else { "❌️" }]); + + table.print_tty(false)?; + + Ok(()) + }) + .with_about("Test the DNS configuration for a domain"), + ) +} + +pub async fn list(ctx: RpcContext) -> Result, Error> { + ctx.db + .peek() + .await + .into_public() + .into_server_info() + .into_network() + .into_domains() + .de() +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct AddDomainParams { + pub fqdn: InternedString, + pub gateway: GatewayId, +} + +pub async fn add( + ctx: RpcContext, + AddDomainParams { fqdn, gateway }: AddDomainParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_domains_mut() + .insert(&fqdn, &DomainSettings { gateway }) + }) + .await + .result?; + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct RemoveDomainParams { + pub fqdn: InternedString, +} + +pub async fn remove( + ctx: RpcContext, + RemoveDomainParams { fqdn }: RemoveDomainParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_domains_mut() + .remove(&fqdn) + }) + .await + .result?; + Ok(()) +} + +#[derive(Deserialize, Serialize)] +pub struct TestDnsResult { + pub root: bool, + pub wildcard: bool, +} + +pub async fn test_dns( + ctx: RpcContext, + AddDomainParams { fqdn, ref gateway }: AddDomainParams, +) -> Result { + use tokio::net::UdpSocket; + use trust_dns_client::client::{AsyncClient, ClientHandle}; + use trust_dns_client::op::DnsResponse; + use trust_dns_client::proto::error::ProtoError; + use trust_dns_client::rr::{DNSClass, Name, RecordType}; + use trust_dns_client::udp::UdpClientStream; + + let wan_ip = ctx + .db + .peek() + .await + .into_public() + .into_server_info() + .into_network() + .into_gateways() + .into_idx(&gateway) + .or_not_found(&gateway)? + .into_ip_info() + .transpose() + .and_then(|i| i.into_wan_ip().transpose()) + .or_not_found(lazy_format!("WAN IP for {gateway}"))? + .de()?; + let stream = UdpClientStream::::new(([127, 0, 0, 53], 53).into()); + let (mut client, bg) = AsyncClient::connect(stream.map_err(ProtoError::from)) + .await + .with_kind(ErrorKind::Network)?; + let bg: NonDetachingJoinHandle<_> = tokio::spawn(bg).into(); + + let root = fqdn.parse::().with_kind(ErrorKind::Network)?; + let wildcard = new_guid() + .parse::() + .with_kind(ErrorKind::Network)? + .append_domain(&root) + .with_kind(ErrorKind::Network)?; + let q_root = client + .query(root, DNSClass::IN, RecordType::A) + .await + .with_kind(ErrorKind::Network)?; + let q_wildcard = client + .query(wildcard, DNSClass::IN, RecordType::A) + .await + .with_kind(ErrorKind::Network)?; + + bg.abort(); + + let check_q = |q: DnsResponse| { + q.answers().iter().any(|a| { + a.data() + .and_then(|d| d.as_a()) + .map_or(false, |d| d.0 == wan_ip) + }) + }; + Ok(TestDnsResult { + root: check_q(q_root), + wildcard: check_q(q_wildcard), + }) +} diff --git a/core/startos/src/net/forward.rs b/core/startos/src/net/forward.rs index 4b5fd39fc..9a900069e 100644 --- a/core/startos/src/net/forward.rs +++ b/core/startos/src/net/forward.rs @@ -12,7 +12,7 @@ use tokio::process::Command; use tokio::sync::mpsc; use crate::db::model::public::NetworkInterfaceInfo; -use crate::net::network_interface::{DynInterfaceFilter, InterfaceFilter}; +use crate::net::gateway::{DynInterfaceFilter, InterfaceFilter}; use crate::net::utils::ipv6_is_link_local; use crate::prelude::*; use crate::util::sync::Watch; diff --git a/core/startos/src/net/network_interface.rs b/core/startos/src/net/gateway.rs similarity index 98% rename from core/startos/src/net/network_interface.rs rename to core/startos/src/net/gateway.rs index f1c428070..3250879a6 100644 --- a/core/startos/src/net/network_interface.rs +++ b/core/startos/src/net/gateway.rs @@ -32,7 +32,7 @@ use crate::context::{CliContext, RpcContext}; use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType}; use crate::db::model::Database; use crate::net::forward::START9_BRIDGE_IFACE; -use crate::net::network_interface::device::DeviceProxy; +use crate::net::gateway::device::DeviceProxy; use crate::net::utils::ipv6_is_link_local; use crate::net::web_server::Accept; use crate::prelude::*; @@ -43,7 +43,7 @@ use crate::util::serde::{display_serializable, HandlerExtSerde}; use crate::util::sync::{SyncMutex, Watch}; use crate::util::Invoke; -pub fn network_interface_api() -> ParentHandler { +pub fn gateway_api() -> ParentHandler { ParentHandler::new() .subcommand( "list", @@ -88,7 +88,7 @@ pub fn network_interface_api() -> ParentHandler { Ok(()) }) - .with_about("Show network interfaces StartOS can listen on") + .with_about("Show gateways StartOS can listen on") .with_call_remote::(), ) .subcommand( @@ -96,26 +96,26 @@ pub fn network_interface_api() -> ParentHandler { from_fn_async(set_public) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_about("Indicate whether this interface has inbound access from the WAN") + .with_about("Indicate whether this gateway has inbound access from the WAN") .with_call_remote::(), ).subcommand( - "unset-inbound", + "unset-public", from_fn_async(unset_public) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_about("Allow this interface to infer whether it has inbound access from the WAN based on its IPv4 address") + .with_about("Allow this gateway to infer whether it has inbound access from the WAN based on its IPv4 address") .with_call_remote::(), ).subcommand("forget", from_fn_async(forget_iface) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_about("Forget a disconnected interface") + .with_about("Forget a disconnected gateway") .with_call_remote::() ).subcommand("set-name", from_fn_async(set_name) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_about("Rename an interface") + .with_about("Rename a gateway") .with_call_remote::() ) } @@ -814,7 +814,7 @@ impl NetworkInterfaceController { db.as_public_mut() .as_server_info_mut() .as_network_mut() - .as_network_interfaces_mut() + .as_gateways_mut() .ser(info) }) .await @@ -881,7 +881,7 @@ impl NetworkInterfaceController { .as_public() .as_server_info() .as_network() - .as_network_interfaces() + .as_gateways() .de() { Ok(mut info) => { @@ -940,7 +940,7 @@ impl NetworkInterfaceController { let mut sub = self .db .subscribe( - "/public/serverInfo/network/networkInterfaces" + "/public/serverInfo/network/gateways" .parse::>() .with_kind(ErrorKind::Database)?, ) @@ -973,7 +973,7 @@ impl NetworkInterfaceController { let mut sub = self .db .subscribe( - "/public/serverInfo/network/networkInterfaces" + "/public/serverInfo/network/gateways" .parse::>() .with_kind(ErrorKind::Database)?, ) @@ -1043,7 +1043,7 @@ impl NetworkInterfaceController { let (dump, mut sub) = self .db .dump_and_sub( - "/public/serverInfo/network/networkInterfaces" + "/public/serverInfo/network/gateways" .parse::>() .with_kind(ErrorKind::Database)? .join_end(interface.as_str()) diff --git a/core/startos/src/net/host/address.rs b/core/startos/src/net/host/address.rs index 6939399d9..f35a03696 100644 --- a/core/startos/src/net/host/address.rs +++ b/core/startos/src/net/host/address.rs @@ -34,6 +34,8 @@ pub enum HostAddress { #[derive(Debug, Deserialize, Serialize, TS)] pub struct DomainConfig { + #[ts(type = "string")] + pub root: InternedString, pub public: bool, pub acme: Option, } @@ -177,7 +179,7 @@ pub struct AddDomainParams { pub async fn add_domain( ctx: RpcContext, AddDomainParams { - domain, + ref domain, private, acme, }: AddDomainParams, @@ -185,24 +187,41 @@ pub async fn add_domain( ) -> Result<(), Error> { ctx.db .mutate(|db| { + let root = db + .as_public() + .as_server_info() + .as_network() + .as_domains() + .keys()? + .into_iter() + .find(|root| domain.ends_with(&**root)) + .or_not_found(lazy_format!("root domain for {domain}"))?; + if let Some(acme) = &acme { - if !db.as_public().as_server_info().as_network().as_acme().contains_key(&acme)? { + if !db + .as_public() + .as_server_info() + .as_network() + .as_acme() + .contains_key(&acme)? + { return Err(Error::new(eyre!("unknown acme provider {}, please run acme.init for this provider first", acme.0), ErrorKind::InvalidRequest)); } } - Kind::host_for(&inheritance, db)? - .as_domains_mut() - .insert( - &domain, - &DomainConfig { - public: !private, - acme, - }, - )?; + + Kind::host_for(&inheritance, db)?.as_domains_mut().insert( + domain, + &DomainConfig { + root, + public: !private, + acme, + }, + )?; check_duplicates(db) }) - .await.result?; + .await + .result?; Kind::sync_host(&ctx, inheritance).await?; Ok(()) diff --git a/core/startos/src/net/host/binding.rs b/core/startos/src/net/host/binding.rs index 934c9b8be..3d304b716 100644 --- a/core/startos/src/net/host/binding.rs +++ b/core/startos/src/net/host/binding.rs @@ -13,7 +13,7 @@ use crate::context::{CliContext, RpcContext}; use crate::db::model::public::NetworkInterfaceInfo; use crate::net::forward::AvailablePorts; use crate::net::host::HostApiKind; -use crate::net::network_interface::InterfaceFilter; +use crate::net::gateway::InterfaceFilter; use crate::net::vhost::AlpnInfo; use crate::prelude::*; use crate::util::serde::{display_serializable, HandlerExtSerde}; diff --git a/core/startos/src/net/host/mod.rs b/core/startos/src/net/host/mod.rs index 37765a257..70c3009d9 100644 --- a/core/startos/src/net/host/mod.rs +++ b/core/startos/src/net/host/mod.rs @@ -53,7 +53,7 @@ impl Host { self.domains .iter() .map( - |(address, DomainConfig { public, acme })| HostAddress::Domain { + |(address, DomainConfig { public, acme, .. })| HostAddress::Domain { address: address.clone(), public: *public, acme: acme.clone(), diff --git a/core/startos/src/net/mod.rs b/core/startos/src/net/mod.rs index a5f10de3d..bbf5f36d8 100644 --- a/core/startos/src/net/mod.rs +++ b/core/startos/src/net/mod.rs @@ -2,12 +2,13 @@ use rpc_toolkit::{Context, HandlerExt, ParentHandler}; pub mod acme; pub mod dns; +pub mod domain; pub mod forward; +pub mod gateway; pub mod host; pub mod keys; pub mod mdns; pub mod net_controller; -pub mod network_interface; pub mod service_interface; pub mod ssl; pub mod static_server; @@ -18,20 +19,23 @@ pub mod vhost; pub mod web_server; pub mod wifi; -pub fn net() -> ParentHandler { +pub fn net_api() -> ParentHandler { ParentHandler::new() .subcommand( "tor", - tor::tor::().with_about("Tor commands such as list-services, logs, and reset"), + tor::tor_api::().with_about("Tor commands such as list-services, logs, and reset"), ) .subcommand( "acme", - acme::acme::().with_about("Setup automatic clearnet certificate acquisition"), + acme::acme_api::().with_about("Setup automatic clearnet certificate acquisition"), ) .subcommand( - "network-interface", - network_interface::network_interface_api::() - .with_about("View and edit network interface configurations"), + "domain", + domain::domain_api::().with_about("Setup clearnet domains"), + ) + .subcommand( + "gateway", + gateway::gateway_api::().with_about("View and edit gateway configurations"), ) .subcommand( "tunnel", diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index 589d77246..3373d6f3d 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -17,13 +17,13 @@ use crate::error::ErrorCollection; use crate::hostname::Hostname; use crate::net::dns::DnsController; use crate::net::forward::{PortForwardController, START9_BRIDGE_IFACE}; -use crate::net::host::address::HostAddress; -use crate::net::host::binding::{AddSslOptions, BindId, BindOptions}; -use crate::net::host::{host_for, Host, Hosts}; -use crate::net::network_interface::{ +use crate::net::gateway::{ AndFilter, DynInterfaceFilter, InterfaceFilter, LoopbackFilter, NetworkInterfaceController, SecureFilter, }; +use crate::net::host::address::HostAddress; +use crate::net::host::binding::{AddSslOptions, BindId, BindOptions}; +use crate::net::host::{host_for, Host, Hosts}; use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname}; use crate::net::tor::TorController; use crate::net::utils::ipv6_is_local; diff --git a/core/startos/src/net/tor.rs b/core/startos/src/net/tor.rs index 6977d148b..1fb8054ca 100644 --- a/core/startos/src/net/tor.rs +++ b/core/startos/src/net/tor.rs @@ -84,7 +84,7 @@ lazy_static! { static ref PROGRESS_REGEX: Regex = Regex::new("PROGRESS=([0-9]+)").unwrap(); } -pub fn tor() -> ParentHandler { +pub fn tor_api() -> ParentHandler { ParentHandler::new() .subcommand( "list-services", diff --git a/core/startos/src/net/tunnel.rs b/core/startos/src/net/tunnel.rs index e6b59c522..228d736d9 100644 --- a/core/startos/src/net/tunnel.rs +++ b/core/startos/src/net/tunnel.rs @@ -53,7 +53,7 @@ pub async fn add_tunnel( .into_public() .into_server_info() .into_network() - .into_network_interfaces() + .into_gateways() .keys()?; let mut iface = GatewayId::from("wg0"); for id in 1.. { @@ -105,7 +105,7 @@ pub async fn remove_tunnel( .into_public() .into_server_info() .into_network() - .into_network_interfaces() + .into_gateways() .into_idx(&id) .and_then(|e| e.into_ip_info().transpose()) else { diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index f080b7ac1..e62ee120e 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -36,7 +36,7 @@ use crate::context::{CliContext, RpcContext}; use crate::db::model::public::NetworkInterfaceInfo; use crate::db::model::Database; use crate::net::acme::{AcmeCertCache, AcmeProvider}; -use crate::net::network_interface::{ +use crate::net::gateway::{ Accepted, AnyFilter, DynInterfaceFilter, InterfaceFilter, NetworkInterfaceController, NetworkInterfaceListener, }; diff --git a/core/startos/src/net/web_server.rs b/core/startos/src/net/web_server.rs index 0b0fb160a..64c4ff1a1 100644 --- a/core/startos/src/net/web_server.rs +++ b/core/startos/src/net/web_server.rs @@ -14,7 +14,7 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::sync::oneshot; use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; -use crate::net::network_interface::{ +use crate::net::gateway::{ lookup_info_by_addr, NetworkInterfaceListener, SelfContainedNetworkInterfaceListener, }; use crate::net::static_server::{ diff --git a/core/startos/src/service/effects/net/ssl.rs b/core/startos/src/service/effects/net/ssl.rs index 51d8d316f..69d3caae0 100644 --- a/core/startos/src/service/effects/net/ssl.rs +++ b/core/startos/src/service/effects/net/ssl.rs @@ -90,7 +90,7 @@ pub async fn get_ssl_certificate( .as_public() .as_server_info() .as_network() - .as_network_interfaces() + .as_gateways() .as_entries()? .into_iter() .flat_map(|(_, net)| net.as_ip_info().transpose_ref()) diff --git a/core/startos/src/tunnel/context.rs b/core/startos/src/tunnel/context.rs index c618d4ce0..54084cf75 100644 --- a/core/startos/src/tunnel/context.rs +++ b/core/startos/src/tunnel/context.rs @@ -20,7 +20,7 @@ use crate::context::CliContext; use crate::middleware::auth::AuthContext; use crate::middleware::signature::SignatureAuthContext; use crate::net::forward::PortForwardController; -use crate::net::network_interface::NetworkInterfaceWatcher; +use crate::net::gateway::NetworkInterfaceWatcher; use crate::prelude::*; use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations}; use crate::tunnel::db::TunnelDatabase; diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index d134b35fc..16f6e1493 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -49,7 +49,9 @@ mod v0_4_0_alpha_7; mod v0_4_0_alpha_8; mod v0_4_0_alpha_9; -pub type Current = v0_4_0_alpha_9::Version; // VERSION_BUMP +mod v0_4_0_alpha_10; + +pub type Current = v0_4_0_alpha_10::Version; // VERSION_BUMP impl Current { #[instrument(skip(self, db))] @@ -161,7 +163,8 @@ enum Version { V0_4_0_alpha_6(Wrapper), V0_4_0_alpha_7(Wrapper), V0_4_0_alpha_8(Wrapper), - V0_4_0_alpha_9(Wrapper), // VERSION_BUMP + V0_4_0_alpha_9(Wrapper), + V0_4_0_alpha_10(Wrapper), // VERSION_BUMP Other(exver::Version), } @@ -213,7 +216,8 @@ impl Version { Self::V0_4_0_alpha_6(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_7(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_8(v) => DynVersion(Box::new(v.0)), - Self::V0_4_0_alpha_9(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP + Self::V0_4_0_alpha_9(v) => DynVersion(Box::new(v.0)), + Self::V0_4_0_alpha_10(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP Self::Other(v) => { return Err(Error::new( eyre!("unknown version {v}"), @@ -257,7 +261,8 @@ impl Version { Version::V0_4_0_alpha_6(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_7(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_8(Wrapper(x)) => x.semver(), - Version::V0_4_0_alpha_9(Wrapper(x)) => x.semver(), // VERSION_BUMP + Version::V0_4_0_alpha_9(Wrapper(x)) => x.semver(), + Version::V0_4_0_alpha_10(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::Other(x) => x.clone(), } } diff --git a/core/startos/src/version/v0_3_6_alpha_10.rs b/core/startos/src/version/v0_3_6_alpha_10.rs index 6b7f1d73e..b198a93f1 100644 --- a/core/startos/src/version/v0_3_6_alpha_10.rs +++ b/core/startos/src/version/v0_3_6_alpha_10.rs @@ -72,8 +72,9 @@ impl VersionT for Version { } HostAddress::Domain { address } => { domains.insert( - address, + address.clone(), DomainConfig { + root: address, public: true, acme: None, }, diff --git a/core/startos/src/version/v0_4_0_alpha_10.rs b/core/startos/src/version/v0_4_0_alpha_10.rs new file mode 100644 index 000000000..a8ad7978a --- /dev/null +++ b/core/startos/src/version/v0_4_0_alpha_10.rs @@ -0,0 +1,108 @@ +use std::collections::BTreeSet; +use std::sync::Arc; + +use exver::{PreReleaseSegment, VersionRange}; +use imbl_value::json; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_4_0_alpha_9, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_4_0_alpha_10: exver::Version = exver::Version::new( + [0, 4, 0], + [PreReleaseSegment::String("alpha".into()), 10.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_4_0_alpha_9::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_4_0_alpha_10.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + #[instrument] + fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result { + let default_gateway = db["public"]["serverInfo"]["network"]["networkInterfaces"] + .as_object() + .into_iter() + .flatten() + .find(|(_, i)| i["ipInfo"]["wanIp"].is_string()) + .map(|(g, _)| g.clone()); + let mut roots = BTreeSet::new(); + for (_, package) in db["public"]["packageData"] + .as_object_mut() + .ok_or_else(|| { + Error::new( + eyre!("expected public.packageData to be an object"), + ErrorKind::Database, + ) + })? + .iter_mut() + { + for (_, host) in package["hosts"] + .as_object_mut() + .ok_or_else(|| { + Error::new( + eyre!("expected public.packageData[id].hosts to be an object"), + ErrorKind::Database, + ) + })? + .iter_mut() + { + if default_gateway.is_none() { + host["domains"] = json!({}); + continue; + } + for (domain, info) in host["domains"] + .as_object_mut() + .ok_or_else(|| { + Error::new( + eyre!( + "expected public.packageData[id].hosts[id].domains to be an object" + ), + ErrorKind::Database, + ) + })? + .iter_mut() + { + let Some(info) = info.as_object_mut() else { + continue; + }; + let root = domain.clone(); + info.insert("root".into(), Value::String(Arc::new((&*root).to_owned()))); + roots.insert(root); + } + } + } + let network = db["public"]["serverInfo"]["network"] + .as_object_mut() + .ok_or_else(|| { + Error::new( + eyre!("expected public.serverInfo.network to be an object"), + ErrorKind::Database, + ) + })?; + network["gateways"] = network["networkInterfaces"].clone(); + if let Some(gateway) = default_gateway { + for root in roots { + network["domains"][&*root] = json!({ "gateway": gateway }); + } + } + + Ok(Value::Null) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/sdk/base/lib/osBindings/DomainConfig.ts b/sdk/base/lib/osBindings/DomainConfig.ts index 433bc65f5..b68cbd068 100644 --- a/sdk/base/lib/osBindings/DomainConfig.ts +++ b/sdk/base/lib/osBindings/DomainConfig.ts @@ -1,4 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AcmeProvider } from "./AcmeProvider" -export type DomainConfig = { public: boolean; acme: AcmeProvider | null } +export type DomainConfig = { + root: string + public: boolean + acme: AcmeProvider | null +} diff --git a/sdk/base/lib/osBindings/DomainSettings.ts b/sdk/base/lib/osBindings/DomainSettings.ts new file mode 100644 index 000000000..276135926 --- /dev/null +++ b/sdk/base/lib/osBindings/DomainSettings.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { GatewayId } from "./GatewayId" + +export type DomainSettings = { gateway: GatewayId } diff --git a/sdk/base/lib/osBindings/NetworkInfo.ts b/sdk/base/lib/osBindings/NetworkInfo.ts index 28a8b5352..ba544b939 100644 --- a/sdk/base/lib/osBindings/NetworkInfo.ts +++ b/sdk/base/lib/osBindings/NetworkInfo.ts @@ -1,6 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AcmeProvider } from "./AcmeProvider" import type { AcmeSettings } from "./AcmeSettings" +import type { DomainSettings } from "./DomainSettings" import type { GatewayId } from "./GatewayId" import type { Host } from "./Host" import type { NetworkInterfaceInfo } from "./NetworkInterfaceInfo" @@ -9,6 +10,7 @@ import type { WifiInfo } from "./WifiInfo" export type NetworkInfo = { wifi: WifiInfo host: Host - networkInterfaces: { [key: GatewayId]: NetworkInterfaceInfo } + gateways: { [key: GatewayId]: NetworkInterfaceInfo } acme: { [key: AcmeProvider]: AcmeSettings } + domains: { [key: string]: DomainSettings } } diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index 374ad1efe..f3d6e9675 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -67,6 +67,7 @@ export { Description } from "./Description" export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams" export { DeviceFilter } from "./DeviceFilter" export { DomainConfig } from "./DomainConfig" +export { DomainSettings } from "./DomainSettings" export { Duration } from "./Duration" export { EchoParams } from "./EchoParams" export { EditSignerParams } from "./EditSignerParams" diff --git a/sdk/base/lib/util/ip.ts b/sdk/base/lib/util/ip.ts index 3125b248a..1dfcd2b8b 100644 --- a/sdk/base/lib/util/ip.ts +++ b/sdk/base/lib/util/ip.ts @@ -12,8 +12,9 @@ export class IpAddress { this.octets[octIdx++] = num & 255 idx += 1 } - if (idx < 7) { - idx = segs.length - 1 + const lastSegIdx = segs.length - 1 + if (idx < lastSegIdx) { + idx = lastSegIdx octIdx = 15 while (segs[idx]) { const num = parseInt(segs[idx], 16) diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 345b2f3a7..9cb32df4a 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -61,7 +61,7 @@ import { } from "../../base/lib/inits" import { DropGenerator } from "../../base/lib/util/Drop" -export const OSVersion = testTypeVersion("0.4.0-alpha.9") +export const OSVersion = testTypeVersion("0.4.0-alpha.10") // prettier-ignore type AnyNeverCond = diff --git a/web/package-lock.json b/web/package-lock.json index cc391703c..8eacd88cf 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "startos-ui", - "version": "0.4.0-alpha.9", + "version": "0.4.0-alpha.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "startos-ui", - "version": "0.4.0-alpha.9", + "version": "0.4.0-alpha.10", "license": "MIT", "dependencies": { "@angular/animations": "^20.1.0", diff --git a/web/package.json b/web/package.json index e6cba11fd..da35463a0 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.4.0-alpha.9", + "version": "0.4.0-alpha.10", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "license": "MIT", diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/domain.service.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/domain.service.ts index 649b3b3fa..88209a029 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/domain.service.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/domain.service.ts @@ -47,10 +47,8 @@ export class DomainService { readonly data = toSignal( this.patch.watch$('serverInfo', 'network').pipe( - map(({ networkInterfaces, domains, acme }) => ({ - gateways: Object.entries(networkInterfaces).reduce< - Record - >( + map(({ gateways, domains, acme }) => ({ + gateways: Object.entries(gateways).reduce>( (obj, [id, n]) => ({ ...obj, [id]: n.ipInfo?.name || '', @@ -64,7 +62,7 @@ export class DomainService { subdomain: parse(fqdn).subdomain, gateway: { id: gateway, - ipInfo: networkInterfaces[gateway]?.ipInfo || null, + ipInfo: gateways[gateway]?.ipInfo || null, }, authority: { url: acme, diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts index 7be07ade3..9bd5013a3 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts @@ -84,7 +84,7 @@ export default class GatewaysComponent { private readonly formDialog = inject(FormDialogService) readonly gateways$ = inject>(PatchDB) - .watch$('serverInfo', 'network', 'networkInterfaces') + .watch$('serverInfo', 'network', 'gateways') .pipe( map(gateways => Object.entries(gateways) 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 809e57591..ebf2f0a25 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -110,7 +110,7 @@ export namespace Mock { squashfs: { aarch64: { publishedAt: '2025-04-21T20:58:48.140749883Z', - url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.9/startos-0.4.0-alpha.9-33ae46f~dev_aarch64.squashfs', + url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_aarch64.squashfs', commitment: { hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=', size: 1343500288, @@ -122,7 +122,7 @@ export namespace Mock { }, 'aarch64-nonfree': { publishedAt: '2025-04-21T21:07:00.249285116Z', - url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.9/startos-0.4.0-alpha.9-33ae46f~dev_aarch64-nonfree.squashfs', + url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_aarch64-nonfree.squashfs', commitment: { hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=', size: 1653075968, @@ -134,7 +134,7 @@ export namespace Mock { }, raspberrypi: { publishedAt: '2025-04-21T21:16:12.933319237Z', - url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.9/startos-0.4.0-alpha.9-33ae46f~dev_raspberrypi.squashfs', + url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_raspberrypi.squashfs', commitment: { hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=', size: 1490731008, @@ -146,7 +146,7 @@ export namespace Mock { }, x86_64: { publishedAt: '2025-04-21T21:14:20.246908903Z', - url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.9/startos-0.4.0-alpha.9-33ae46f~dev_x86_64.squashfs', + url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_x86_64.squashfs', commitment: { hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=', size: 1411657728, @@ -158,7 +158,7 @@ export namespace Mock { }, 'x86_64-nonfree': { publishedAt: '2025-04-21T21:15:17.955265284Z', - url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.9/startos-0.4.0-alpha.9-33ae46f~dev_x86_64-nonfree.squashfs', + url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_x86_64-nonfree.squashfs', commitment: { hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=', size: 1731035136, diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 553b2f9f1..4bb4fe779 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -269,7 +269,7 @@ export namespace RR { export type UpdateTunnelReq = { id: string name: string - } // net.netwok-interface.set-name + } // net.gateway.set-name export type UpdateTunnelRes = null export type RemoveTunnelReq = { id: string } // net.tunnel.remove diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 3b4b270d9..9ba2061c5 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -351,7 +351,7 @@ export class LiveApiService extends ApiService { } async updateTunnel(params: RR.UpdateTunnelReq): Promise { - return this.rpcRequest({ method: 'net.netwok-interface.set-name', params }) + return this.rpcRequest({ method: 'net.gateway.set-name', params }) } async removeTunnel(params: RR.RemoveTunnelReq): Promise { diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 01c995dbb..84d19b383 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -553,7 +553,7 @@ export class MockApiService extends ApiService { const patch: AddOperation[] = [ { op: PatchOp.ADD, - path: `/serverInfo/network/networkInterfaces/${id}`, + path: `/serverInfo/network/gateways/${id}`, value: { public: params.public, secure: false, @@ -579,7 +579,7 @@ export class MockApiService extends ApiService { const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, - path: `/serverInfo/network/networkInterfaces/${params.id}/label`, + path: `/serverInfo/network/gateways/${params.id}/label`, value: params.name, }, ] @@ -593,7 +593,7 @@ export class MockApiService extends ApiService { const patch: RemoveOperation[] = [ { op: PatchOp.REMOVE, - path: `/serverInfo/network/networkInterfaces/${params.id}`, + path: `/serverInfo/network/gateways/${params.id}`, }, ] this.mockRevision(patch) 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 29c42ed91..4ecc40e09 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -145,7 +145,7 @@ export const mockPatchData: DataModel = { ], }, }, - networkInterfaces: { + gateways: { eth0: { public: null, secure: null, diff --git a/web/projects/ui/src/app/services/proxy.service.ts b/web/projects/ui/src/app/services/proxy.service.ts index c0185f17d..c504045cc 100644 --- a/web/projects/ui/src/app/services/proxy.service.ts +++ b/web/projects/ui/src/app/services/proxy.service.ts @@ -26,14 +26,14 @@ // ) {} // async presentModalSetOutboundProxy(current: string | null, pkgId?: string) { -// const networkInterfaces = await firstValueFrom( -// this.patch.watch$('serverInfo', 'network', 'networkInterfaces'), +// const gateways = await firstValueFrom( +// this.patch.watch$('serverInfo', 'network', 'gateways'), // ) // const config = ISB.InputSpec.of({ // proxyId: ISB.Value.select({ // name: 'Select Proxy', // default: current || '', -// values: Object.entries(networkInterfaces) +// values: Object.entries(gateways) // .filter( // ([_, n]) => n.outbound && n.ipInfo?.deviceType === 'wireguard', // )