domains api + migration

This commit is contained in:
Aiden McClelland
2025-08-06 14:29:35 -06:00
parent ea12251a7e
commit d6dfaf8feb
39 changed files with 496 additions and 87 deletions

47
core/Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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;

View File

@@ -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::<GatewayId, NetworkInterfaceInfo>")]
#[serde(default)]
pub network_interfaces: OrdMap<GatewayId, NetworkInterfaceInfo>,
pub gateways: OrdMap<GatewayId, NetworkInterfaceInfo>,
#[serde(default)]
pub acme: BTreeMap<AcmeProvider, AcmeSettings>,
#[serde(default)]
#[ts(as = "BTreeMap::<String, DomainSettings>")]
pub domains: BTreeMap<InternedString, DomainSettings>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)]
@@ -303,6 +307,14 @@ pub struct AcmeSettings {
pub contact: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct DomainSettings {
pub gateway: GatewayId,
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[model = "Model<Self>"]
#[ts(export)]

View File

@@ -150,7 +150,7 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
)
.subcommand(
"net",
net::net::<C>().with_about("Network commands related to tor and dhcp"),
net::net_api::<C>().with_about("Network commands related to tor and dhcp"),
)
.subcommand(
"auth",

View File

@@ -174,7 +174,7 @@ impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> {
}
}
pub fn acme<C: Context>() -> ParentHandler<C> {
pub fn acme_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"init",

View File

@@ -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<C: Context>() -> ParentHandler<C> {
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::<CliContext>(),
)
.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::<CliContext>(),
)
.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::<CliContext>(),
)
.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<BTreeMap<InternedString, DomainSettings>, 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<TestDnsResult, Error> {
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::<UdpSocket>::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::<Name>().with_kind(ErrorKind::Network)?;
let wildcard = new_guid()
.parse::<Name>()
.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),
})
}

View File

@@ -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;

View File

@@ -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<C: Context>() -> ParentHandler<C> {
pub fn gateway_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"list",
@@ -88,7 +88,7 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
Ok(())
})
.with_about("Show network interfaces StartOS can listen on")
.with_about("Show gateways StartOS can listen on")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -96,26 +96,26 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
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::<CliContext>(),
).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::<CliContext>(),
).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::<CliContext>()
).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::<CliContext>()
)
}
@@ -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::<JsonPointer<_, _>>()
.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::<JsonPointer<_, _>>()
.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::<JsonPointer<_, _>>()
.with_kind(ErrorKind::Database)?
.join_end(interface.as_str())

View File

@@ -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<AcmeProvider>,
}
@@ -177,7 +179,7 @@ pub struct AddDomainParams {
pub async fn add_domain<Kind: HostApiKind>(
ctx: RpcContext,
AddDomainParams {
domain,
ref domain,
private,
acme,
}: AddDomainParams,
@@ -185,24 +187,41 @@ pub async fn add_domain<Kind: HostApiKind>(
) -> 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(())

View File

@@ -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};

View File

@@ -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(),

View File

@@ -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<C: Context>() -> ParentHandler<C> {
pub fn net_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"tor",
tor::tor::<C>().with_about("Tor commands such as list-services, logs, and reset"),
tor::tor_api::<C>().with_about("Tor commands such as list-services, logs, and reset"),
)
.subcommand(
"acme",
acme::acme::<C>().with_about("Setup automatic clearnet certificate acquisition"),
acme::acme_api::<C>().with_about("Setup automatic clearnet certificate acquisition"),
)
.subcommand(
"network-interface",
network_interface::network_interface_api::<C>()
.with_about("View and edit network interface configurations"),
"domain",
domain::domain_api::<C>().with_about("Setup clearnet domains"),
)
.subcommand(
"gateway",
gateway::gateway_api::<C>().with_about("View and edit gateway configurations"),
)
.subcommand(
"tunnel",

View File

@@ -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;

View File

@@ -84,7 +84,7 @@ lazy_static! {
static ref PROGRESS_REGEX: Regex = Regex::new("PROGRESS=([0-9]+)").unwrap();
}
pub fn tor<C: Context>() -> ParentHandler<C> {
pub fn tor_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"list-services",

View File

@@ -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 {

View File

@@ -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,
};

View File

@@ -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::{

View File

@@ -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())

View File

@@ -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;

View File

@@ -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_6::Version>),
V0_4_0_alpha_7(Wrapper<v0_4_0_alpha_7::Version>),
V0_4_0_alpha_8(Wrapper<v0_4_0_alpha_8::Version>),
V0_4_0_alpha_9(Wrapper<v0_4_0_alpha_9::Version>), // VERSION_BUMP
V0_4_0_alpha_9(Wrapper<v0_4_0_alpha_9::Version>),
V0_4_0_alpha_10(Wrapper<v0_4_0_alpha_10::Version>), // 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(),
}
}

View File

@@ -72,8 +72,9 @@ impl VersionT for Version {
}
HostAddress::Domain { address } => {
domains.insert(
address,
address.clone(),
DomainConfig {
root: address,
public: true,
acme: None,
},

View File

@@ -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<Self::PreUpRes, Error> {
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<Value, Error> {
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(())
}
}

View File

@@ -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
}

View File

@@ -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 }

View File

@@ -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 }
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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<T extends any[], Then, Else> =

4
web/package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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<string, string>
>(
map(({ gateways, domains, acme }) => ({
gateways: Object.entries(gateways).reduce<Record<string, string>>(
(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,

View File

@@ -84,7 +84,7 @@ export default class GatewaysComponent {
private readonly formDialog = inject(FormDialogService)
readonly gateways$ = inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'network', 'networkInterfaces')
.watch$('serverInfo', 'network', 'gateways')
.pipe(
map(gateways =>
Object.entries(gateways)

View File

@@ -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,

View File

@@ -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

View File

@@ -351,7 +351,7 @@ export class LiveApiService extends ApiService {
}
async updateTunnel(params: RR.UpdateTunnelReq): Promise<RR.UpdateTunnelRes> {
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<RR.RemoveTunnelRes> {

View File

@@ -553,7 +553,7 @@ export class MockApiService extends ApiService {
const patch: AddOperation<T.NetworkInterfaceInfo>[] = [
{
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<string>[] = [
{
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)

View File

@@ -145,7 +145,7 @@ export const mockPatchData: DataModel = {
],
},
},
networkInterfaces: {
gateways: {
eth0: {
public: null,
secure: null,

View File

@@ -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',
// )