From 3974c09369d52c39c2e935108541f537ac530244 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 24 Feb 2026 14:18:53 -0700 Subject: [PATCH] feat(core): refactor hostname to ServerHostnameInfo with name/hostname pair - Rename Hostname to ServerHostnameInfo, add name + hostname fields - Add set_hostname_rpc for changing hostname at runtime - Migrate alpha_20: generate serverInfo.name from hostname, delete ui.name - Extract gateway.rs helpers to fix rustfmt nesting depth issue - Add i18n key for hostname validation error - Update SDK bindings --- core/locales/i18n.yaml | 7 + core/src/account.rs | 20 +- core/src/backup/backup_bulk.rs | 2 +- core/src/backup/os.rs | 28 +- core/src/backup/restore.rs | 8 +- core/src/backup/target/cifs.rs | 5 +- core/src/bins/startd.rs | 8 +- core/src/bins/tunnel.rs | 2 +- core/src/context/setup.rs | 4 +- core/src/db/model/public.rs | 7 +- core/src/disk/util.rs | 4 +- core/src/hostname.rs | 206 +++++- core/src/init.rs | 7 +- core/src/net/dns.rs | 72 +- core/src/net/gateway.rs | 694 ++++++++++-------- core/src/net/host/address.rs | 31 +- core/src/net/host/mod.rs | 4 +- core/src/net/net_controller.rs | 6 +- core/src/net/ssl.rs | 7 +- core/src/net/static_server.rs | 11 +- core/src/net/tls.rs | 83 +-- core/src/net/tunnel.rs | 18 +- core/src/net/wifi.rs | 10 +- core/src/registry/package/get.rs | 5 +- core/src/registry/package/index.rs | 2 +- core/src/service/action.rs | 8 +- core/src/service/effects/action.rs | 4 +- core/src/service/effects/mod.rs | 5 +- core/src/service/uninstall.rs | 8 +- core/src/setup.rs | 51 +- core/src/ssh.rs | 13 +- core/src/tunnel/api.rs | 5 +- core/src/tunnel/web.rs | 38 +- core/src/version/v0_3_6_alpha_0.rs | 13 +- core/src/version/v0_3_6_alpha_7.rs | 5 +- core/src/version/v0_4_0_alpha_20.rs | 44 ++ sdk/base/lib/osBindings/AttachParams.ts | 1 + .../{Hostname.ts => ServerHostname.ts} | 2 +- sdk/base/lib/osBindings/ServerInfo.ts | 1 + .../lib/osBindings/SetServerHostnameParams.ts | 5 +- sdk/base/lib/osBindings/SetupExecuteParams.ts | 1 + .../lib/osBindings/StartOsRecoveryInfo.ts | 4 +- sdk/base/lib/osBindings/index.ts | 2 +- 43 files changed, 871 insertions(+), 590 deletions(-) rename sdk/base/lib/osBindings/{Hostname.ts => ServerHostname.ts} (75%) diff --git a/core/locales/i18n.yaml b/core/locales/i18n.yaml index 9292bff7f..aaf2aad10 100644 --- a/core/locales/i18n.yaml +++ b/core/locales/i18n.yaml @@ -1008,6 +1008,13 @@ hostname.invalid-character: fr_FR: "Caractère invalide dans le nom d'hôte : %{char}" pl_PL: "Nieprawidłowy znak w nazwie hosta: %{char}" +hostname.must-provide-name-or-hostname: + en_US: "Must provide at least one of: name, hostname" + de_DE: "Es muss mindestens eines angegeben werden: name, hostname" + es_ES: "Se debe proporcionar al menos uno de: name, hostname" + fr_FR: "Vous devez fournir au moins l'un des éléments suivants : name, hostname" + pl_PL: "Należy podać co najmniej jedno z: name, hostname" + # init.rs init.running-preinit: en_US: "Running preinit.sh" diff --git a/core/src/account.rs b/core/src/account.rs index 78fb3ecf3..f80cc951c 100644 --- a/core/src/account.rs +++ b/core/src/account.rs @@ -6,7 +6,7 @@ use openssl::pkey::{PKey, Private}; use openssl::x509::X509; use crate::db::model::DatabaseModel; -use crate::hostname::{Hostname, generate_hostname, generate_id}; +use crate::hostname::{ServerHostnameInfo, generate_hostname, generate_id}; use crate::net::ssl::{gen_nistp256, make_root_cert}; use crate::prelude::*; use crate::util::serde::Pem; @@ -23,7 +23,7 @@ fn hash_password(password: &str) -> Result { #[derive(Clone)] pub struct AccountInfo { pub server_id: String, - pub hostname: Hostname, + pub hostname: ServerHostnameInfo, pub password: String, pub root_ca_key: PKey, pub root_ca_cert: X509, @@ -34,16 +34,16 @@ impl AccountInfo { pub fn new( password: &str, start_time: SystemTime, - hostname: Option, + hostname: Option, ) -> Result { let server_id = generate_id(); let hostname = if let Some(h) = hostname { - Hostname::validate(h)? + h } else { - generate_hostname() + ServerHostnameInfo::from_hostname(generate_hostname()) }; let root_ca_key = gen_nistp256()?; - let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?; + let root_ca_cert = make_root_cert(&root_ca_key, &hostname.hostname, start_time)?; let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random( &mut ssh_key::rand_core::OsRng::default(), )); @@ -62,7 +62,7 @@ impl AccountInfo { pub fn load(db: &DatabaseModel) -> Result { let server_id = db.as_public().as_server_info().as_id().de()?; - let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); + let hostname = ServerHostnameInfo::load(db.as_public().as_server_info())?; let password = db.as_private().as_password().de()?; let key_store = db.as_private().as_key_store(); let cert_store = key_store.as_local_certs(); @@ -85,7 +85,7 @@ impl AccountInfo { pub fn save(&self, db: &mut DatabaseModel) -> Result<(), Error> { let server_info = db.as_public_mut().as_server_info_mut(); server_info.as_id_mut().ser(&self.server_id)?; - server_info.as_hostname_mut().ser(&self.hostname.0)?; + self.hostname.save(server_info)?; server_info .as_pubkey_mut() .ser(&self.ssh_key.public_key().to_openssh()?)?; @@ -123,8 +123,8 @@ impl AccountInfo { pub fn hostnames(&self) -> impl IntoIterator + Send + '_ { [ - self.hostname.no_dot_host_name(), - self.hostname.local_domain_name(), + (*self.hostname.hostname).clone(), + self.hostname.hostname.local_domain_name(), ] } } diff --git a/core/src/backup/backup_bulk.rs b/core/src/backup/backup_bulk.rs index 0acc385ee..722498f3c 100644 --- a/core/src/backup/backup_bulk.rs +++ b/core/src/backup/backup_bulk.rs @@ -338,7 +338,7 @@ async fn perform_backup( let timestamp = Utc::now(); backup_guard.unencrypted_metadata.version = crate::version::Current::default().semver().into(); - backup_guard.unencrypted_metadata.hostname = ctx.account.peek(|a| a.hostname.clone()); + backup_guard.unencrypted_metadata.hostname = ctx.account.peek(|a| a.hostname.hostname.clone()); backup_guard.unencrypted_metadata.timestamp = timestamp.clone(); backup_guard.metadata.version = crate::version::Current::default().semver().into(); backup_guard.metadata.timestamp = Some(timestamp); diff --git a/core/src/backup/os.rs b/core/src/backup/os.rs index 8837f72f0..0d0f79a03 100644 --- a/core/src/backup/os.rs +++ b/core/src/backup/os.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use ssh_key::private::Ed25519Keypair; use crate::account::AccountInfo; -use crate::hostname::{Hostname, generate_hostname, generate_id}; +use crate::hostname::{ServerHostname, ServerHostnameInfo, generate_hostname, generate_id}; use crate::prelude::*; use crate::util::serde::{Base32, Base64, Pem}; @@ -27,10 +27,12 @@ impl<'de> Deserialize<'de> for OsBackup { .map_err(serde::de::Error::custom)?, 1 => patch_db::value::from_value::(tagged.rest) .map_err(serde::de::Error::custom)? - .project(), + .project() + .map_err(serde::de::Error::custom)?, 2 => patch_db::value::from_value::(tagged.rest) .map_err(serde::de::Error::custom)? - .project(), + .project() + .map_err(serde::de::Error::custom)?, v => { return Err(serde::de::Error::custom(&format!( "Unknown backup version {v}" @@ -75,7 +77,7 @@ impl OsBackupV0 { Ok(OsBackup { account: AccountInfo { server_id: generate_id(), - hostname: generate_hostname(), + hostname: ServerHostnameInfo::from_hostname(generate_hostname()), password: Default::default(), root_ca_key: self.root_ca_key.0, root_ca_cert: self.root_ca_cert.0, @@ -104,11 +106,11 @@ struct OsBackupV1 { ui: Value, // JSON Value } impl OsBackupV1 { - fn project(self) -> OsBackup { - OsBackup { + fn project(self) -> Result { + Ok(OsBackup { account: AccountInfo { server_id: self.server_id, - hostname: Hostname(self.hostname), + hostname: ServerHostnameInfo::from_hostname(ServerHostname::new(self.hostname)?), password: Default::default(), root_ca_key: self.root_ca_key.0, root_ca_cert: self.root_ca_cert.0, @@ -116,7 +118,7 @@ impl OsBackupV1 { developer_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key), }, ui: self.ui, - } + }) } } @@ -134,11 +136,11 @@ struct OsBackupV2 { ui: Value, // JSON Value } impl OsBackupV2 { - fn project(self) -> OsBackup { - OsBackup { + fn project(self) -> Result { + Ok(OsBackup { account: AccountInfo { server_id: self.server_id, - hostname: Hostname(self.hostname), + hostname: ServerHostnameInfo::from_hostname(ServerHostname::new(self.hostname)?), password: Default::default(), root_ca_key: self.root_ca_key.0, root_ca_cert: self.root_ca_cert.0, @@ -146,12 +148,12 @@ impl OsBackupV2 { developer_key: self.compat_s9pk_key.0, }, ui: self.ui, - } + }) } fn unproject(backup: &OsBackup) -> Self { Self { server_id: backup.account.server_id.clone(), - hostname: backup.account.hostname.0.clone(), + hostname: (*backup.account.hostname.hostname).clone(), root_ca_key: Pem(backup.account.root_ca_key.clone()), root_ca_cert: Pem(backup.account.root_ca_cert.clone()), ssh_key: Pem(backup.account.ssh_key.clone()), diff --git a/core/src/backup/restore.rs b/core/src/backup/restore.rs index 8e2d978bc..2defc2d80 100644 --- a/core/src/backup/restore.rs +++ b/core/src/backup/restore.rs @@ -17,7 +17,7 @@ use crate::db::model::Database; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; -use crate::hostname::Hostname; +use crate::hostname::ServerHostnameInfo; use crate::init::init; use crate::prelude::*; use crate::progress::ProgressUnits; @@ -91,7 +91,7 @@ pub async fn recover_full_server( server_id: &str, recovery_password: &str, kiosk: Option, - hostname: Option, + hostname: Option, SetupExecuteProgress { init_phases, restore_phase, @@ -118,7 +118,7 @@ pub async fn recover_full_server( .with_kind(ErrorKind::PasswordHashGeneration)?; if let Some(h) = hostname { - os_backup.account.hostname = Hostname::validate(h)?; + os_backup.account.hostname = h; } let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi"); @@ -189,7 +189,7 @@ pub async fn recover_full_server( Ok(( SetupResult { - hostname: os_backup.account.hostname, + hostname: os_backup.account.hostname.hostname, root_ca: Pem(os_backup.account.root_ca_cert), needs_restart: ctx.install_rootfs.peek(|a| a.is_some()), }, diff --git a/core/src/backup/target/cifs.rs b/core/src/backup/target/cifs.rs index e8a938fea..f5fa74862 100644 --- a/core/src/backup/target/cifs.rs +++ b/core/src/backup/target/cifs.rs @@ -218,7 +218,10 @@ pub struct CifsRemoveParams { pub id: BackupTargetId, } -pub async fn remove(ctx: RpcContext, CifsRemoveParams { id }: CifsRemoveParams) -> Result<(), Error> { +pub async fn remove( + ctx: RpcContext, + CifsRemoveParams { id }: CifsRemoveParams, +) -> Result<(), Error> { let id = if let BackupTargetId::Cifs { id } = id { id } else { diff --git a/core/src/bins/startd.rs b/core/src/bins/startd.rs index b88f622e5..314d3dc7a 100644 --- a/core/src/bins/startd.rs +++ b/core/src/bins/startd.rs @@ -70,7 +70,8 @@ async fn inner_main( }; let (rpc_ctx, shutdown) = async { - crate::hostname::sync_hostname(&rpc_ctx.account.peek(|a| a.hostname.clone())).await?; + crate::hostname::sync_hostname(&rpc_ctx.account.peek(|a| a.hostname.hostname.clone())) + .await?; let mut shutdown_recv = rpc_ctx.shutdown.subscribe(); @@ -147,10 +148,7 @@ pub fn main(args: impl IntoIterator) { .build() .expect(&t!("bins.startd.failed-to-initialize-runtime")); let res = rt.block_on(async { - let mut server = WebServer::new( - Acceptor::new(WildcardListener::new(80)?), - refresher(), - ); + let mut server = WebServer::new(Acceptor::new(WildcardListener::new(80)?), refresher()); match inner_main(&mut server, &config).await { Ok(a) => { server.shutdown().await; diff --git a/core/src/bins/tunnel.rs b/core/src/bins/tunnel.rs index 57615c00c..07db8f671 100644 --- a/core/src/bins/tunnel.rs +++ b/core/src/bins/tunnel.rs @@ -7,13 +7,13 @@ use clap::Parser; use futures::FutureExt; use rpc_toolkit::CliApp; use rust_i18n::t; +use tokio::net::TcpListener; use tokio::signal::unix::signal; use tracing::instrument; use visit_rs::Visit; use crate::context::CliContext; use crate::context::config::ClientConfig; -use tokio::net::TcpListener; use crate::net::tls::TlsListener; use crate::net::web_server::{Accept, Acceptor, MetadataVisitor, WebServer}; use crate::prelude::*; diff --git a/core/src/context/setup.rs b/core/src/context/setup.rs index 39a6b5fd3..d4d0bb9de 100644 --- a/core/src/context/setup.rs +++ b/core/src/context/setup.rs @@ -19,7 +19,7 @@ use crate::MAIN_DATA; use crate::context::RpcContext; use crate::context::config::ServerConfig; use crate::disk::mount::guard::{MountGuard, TmpMountGuard}; -use crate::hostname::Hostname; +use crate::hostname::ServerHostname; use crate::net::gateway::WildcardListener; use crate::net::web_server::{WebServer, WebServerAcceptorSetter}; use crate::prelude::*; @@ -45,7 +45,7 @@ lazy_static::lazy_static! { #[ts(export)] pub struct SetupResult { #[ts(type = "string")] - pub hostname: Hostname, + pub hostname: ServerHostname, pub root_ca: Pem, pub needs_restart: bool, } diff --git a/core/src/db/model/public.rs b/core/src/db/model/public.rs index c66ff8681..dac5faf11 100644 --- a/core/src/db/model/public.rs +++ b/core/src/db/model/public.rs @@ -59,7 +59,8 @@ impl Public { platform: get_platform(), id: account.server_id.clone(), version: Current::default().semver(), - hostname: account.hostname.no_dot_host_name(), + name: account.hostname.name.clone(), + hostname: (*account.hostname.hostname).clone(), last_backup: None, package_version_compat: Current::default().compat().clone(), post_init_migration_todos: BTreeMap::new(), @@ -176,13 +177,11 @@ pub fn default_ifconfig_url() -> Url { #[ts(export)] pub struct ServerInfo { #[serde(default = "get_arch")] - #[ts(type = "string")] pub arch: InternedString, #[serde(default = "get_platform")] - #[ts(type = "string")] pub platform: InternedString, pub id: String, - #[ts(type = "string")] + pub name: InternedString, pub hostname: InternedString, #[ts(type = "string")] pub version: Version, diff --git a/core/src/disk/util.rs b/core/src/disk/util.rs index 25dff7bf1..9cf2b6882 100644 --- a/core/src/disk/util.rs +++ b/core/src/disk/util.rs @@ -19,7 +19,7 @@ use super::mount::filesystem::block_dev::BlockDev; use super::mount::guard::TmpMountGuard; use crate::disk::OsPartitionInfo; use crate::disk::mount::guard::GenericMountGuard; -use crate::hostname::Hostname; +use crate::hostname::ServerHostname; use crate::prelude::*; use crate::util::Invoke; use crate::util::serde::IoFormat; @@ -61,7 +61,7 @@ pub struct PartitionInfo { #[ts(export)] #[serde(rename_all = "camelCase")] pub struct StartOsRecoveryInfo { - pub hostname: Hostname, + pub hostname: ServerHostname, #[ts(type = "string")] pub version: exver::Version, #[ts(type = "string")] diff --git a/core/src/hostname.rs b/core/src/hostname.rs index 0e528f03f..4eaae7e51 100644 --- a/core/src/hostname.rs +++ b/core/src/hostname.rs @@ -1,39 +1,41 @@ use clap::Parser; use imbl_value::InternedString; use lazy_format::lazy_format; -use rand::{Rng, rng}; use serde::{Deserialize, Serialize}; use tokio::process::Command; use tracing::instrument; use ts_rs::TS; use crate::context::RpcContext; +use crate::db::model::public::ServerInfo; use crate::prelude::*; use crate::util::Invoke; #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, ts_rs::TS)] #[ts(type = "string")] -pub struct Hostname(pub InternedString); - -lazy_static::lazy_static! { - static ref ADJECTIVES: Vec = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect(); - static ref NOUNS: Vec = include_str!("./assets/nouns.txt").lines().map(|x| x.to_string()).collect(); -} -impl AsRef for Hostname { - fn as_ref(&self) -> &str { +pub struct ServerHostname(InternedString); +impl std::ops::Deref for ServerHostname { + type Target = InternedString; + fn deref(&self) -> &Self::Target { &self.0 } } +impl AsRef for ServerHostname { + fn as_ref(&self) -> &str { + &***self + } +} -impl Hostname { - pub fn validate(h: InternedString) -> Result { - if h.is_empty() { +impl ServerHostname { + fn validate(&self) -> Result<(), Error> { + if self.0.is_empty() { return Err(Error::new( eyre!("{}", t!("hostname.empty")), ErrorKind::InvalidRequest, )); } - if let Some(c) = h + if let Some(c) = self + .0 .chars() .find(|c| !(c.is_ascii_alphanumeric() || c == &'-') || c.is_ascii_uppercase()) { @@ -42,7 +44,13 @@ impl Hostname { ErrorKind::InvalidRequest, )); } - Ok(Self(h)) + Ok(()) + } + + pub fn new(hostname: InternedString) -> Result { + let res = Self(hostname); + res.validate()?; + Ok(res) } pub fn lan_address(&self) -> InternedString { @@ -53,17 +61,135 @@ impl Hostname { InternedString::from_display(&lazy_format!("{}.local", self.0)) } - pub fn no_dot_host_name(&self) -> InternedString { - self.0.clone() + pub fn load(server_info: &Model) -> Result { + Ok(Self(server_info.as_hostname().de()?)) + } + + pub fn save(&self, server_info: &mut Model) -> Result<(), Error> { + server_info.as_hostname_mut().ser(&**self) } } -pub fn generate_hostname() -> Hostname { - let mut rng = rng(); - let adjective = &ADJECTIVES[rng.random_range(0..ADJECTIVES.len())]; - let noun = &NOUNS[rng.random_range(0..NOUNS.len())]; - Hostname(InternedString::from_display(&lazy_format!( - "{adjective}-{noun}" +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[ts(type = "string")] +pub struct ServerHostnameInfo { + pub name: InternedString, + pub hostname: ServerHostname, +} + +lazy_static::lazy_static! { + static ref ADJECTIVES: Vec = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect(); + static ref NOUNS: Vec = include_str!("./assets/nouns.txt").lines().map(|x| x.to_string()).collect(); +} +impl AsRef for ServerHostnameInfo { + fn as_ref(&self) -> &str { + &self.hostname + } +} + +fn normalize(s: &str) -> InternedString { + let mut prev_was_dash = true; + let mut normalized = s + .chars() + .filter_map(|c| { + if c.is_alphanumeric() { + prev_was_dash = false; + Some(c.to_ascii_lowercase()) + } else if (c == '-' || c.is_whitespace()) && !prev_was_dash { + prev_was_dash = true; + Some('-') + } else { + None + } + }) + .collect::(); + while normalized.ends_with('-') { + normalized.pop(); + } + if normalized.len() < 4 { + generate_hostname().0 + } else { + normalized.into() + } +} + +fn denormalize(s: &str) -> InternedString { + let mut cap = true; + s.chars() + .map(|c| { + if c == '-' { + cap = true; + ' ' + } else if cap { + cap = false; + c.to_ascii_uppercase() + } else { + c + } + }) + .collect::() + .into() +} + +impl ServerHostnameInfo { + pub fn new( + name: Option, + hostname: Option, + ) -> Result { + Self::new_opt(name, hostname) + .map(|h| h.unwrap_or_else(|| ServerHostnameInfo::from_hostname(generate_hostname()))) + } + + pub fn new_opt( + name: Option, + hostname: Option, + ) -> Result, Error> { + let name = name.filter(|n| !n.is_empty()); + let hostname = hostname.filter(|h| !h.is_empty()); + Ok(match (name, hostname) { + (Some(name), Some(hostname)) => Some(ServerHostnameInfo { + name, + hostname: ServerHostname::new(hostname)?, + }), + (Some(name), None) => Some(ServerHostnameInfo::from_name(name)), + (None, Some(hostname)) => Some(ServerHostnameInfo::from_hostname(ServerHostname::new( + hostname, + )?)), + (None, None) => None, + }) + } + + pub fn from_hostname(hostname: ServerHostname) -> Self { + Self { + name: denormalize(&**hostname), + hostname, + } + } + + pub fn from_name(name: InternedString) -> Self { + Self { + hostname: ServerHostname(normalize(&*name)), + name, + } + } + + pub fn load(server_info: &Model) -> Result { + Ok(Self { + name: server_info.as_name().de()?, + hostname: ServerHostname::load(server_info)?, + }) + } + + pub fn save(&self, server_info: &mut Model) -> Result<(), Error> { + server_info.as_name_mut().ser(&self.name)?; + self.hostname.save(server_info) + } +} + +pub fn generate_hostname() -> ServerHostname { + let num = rand::random::(); + ServerHostname(InternedString::from_display(&lazy_format!( + "startos-{num:04x}" ))) } @@ -73,17 +199,17 @@ pub fn generate_id() -> String { } #[instrument(skip_all)] -pub async fn get_current_hostname() -> Result { +pub async fn get_current_hostname() -> Result { let out = Command::new("hostname") .invoke(ErrorKind::ParseSysInfo) .await?; let out_string = String::from_utf8(out)?; - Ok(Hostname(out_string.trim().into())) + Ok(out_string.trim().into()) } #[instrument(skip_all)] -pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> { - let hostname = &*hostname.0; +pub async fn set_hostname(hostname: &ServerHostname) -> Result<(), Error> { + let hostname = &***hostname; Command::new("hostnamectl") .arg("--static") .arg("set-hostname") @@ -102,7 +228,7 @@ pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> { } #[instrument(skip_all)] -pub async fn sync_hostname(hostname: &Hostname) -> Result<(), Error> { +pub async fn sync_hostname(hostname: &ServerHostname) -> Result<(), Error> { set_hostname(hostname).await?; Command::new("systemctl") .arg("restart") @@ -117,25 +243,31 @@ pub async fn sync_hostname(hostname: &Hostname) -> Result<(), Error> { #[command(rename_all = "kebab-case")] #[ts(export)] pub struct SetServerHostnameParams { - hostname: InternedString, + name: Option, + hostname: Option, } pub async fn set_hostname_rpc( ctx: RpcContext, - SetServerHostnameParams { hostname }: SetServerHostnameParams, + SetServerHostnameParams { name, hostname }: SetServerHostnameParams, ) -> Result<(), Error> { - let hostname = Hostname::validate(hostname)?; + let Some(hostname) = ServerHostnameInfo::new_opt(name, hostname)? else { + return Err(Error::new( + eyre!("{}", t!("hostname.must-provide-name-or-hostname")), + ErrorKind::InvalidRequest, + )); + }; ctx.db - .mutate(|db| { - db.as_public_mut() - .as_server_info_mut() - .as_hostname_mut() - .ser(&hostname.0) - }) + .mutate(|db| hostname.save(db.as_public_mut().as_server_info_mut())) .await .result?; ctx.account.mutate(|a| a.hostname = hostname.clone()); - sync_hostname(&hostname).await?; + sync_hostname(&hostname.hostname).await?; Ok(()) } + +#[test] +fn test_generate_hostname() { + assert_eq!(dbg!(generate_hostname().0).len(), 12); +} diff --git a/core/src/init.rs b/core/src/init.rs index c6b38c3e2..8b6a91625 100644 --- a/core/src/init.rs +++ b/core/src/init.rs @@ -18,7 +18,7 @@ use crate::context::{CliContext, InitContext, RpcContext}; use crate::db::model::Database; use crate::db::model::public::ServerStatus; use crate::developer::OS_DEVELOPER_KEY_PATH; -use crate::hostname::Hostname; +use crate::hostname::ServerHostname; use crate::middleware::auth::local::LocalAuthContext; use crate::net::gateway::WildcardListener; use crate::net::net_controller::{NetController, NetService}; @@ -191,15 +191,16 @@ pub async fn init( .arg(OS_DEVELOPER_KEY_PATH) .invoke(ErrorKind::Filesystem) .await?; + let hostname = ServerHostname::load(peek.as_public().as_server_info())?; crate::ssh::sync_keys( - &Hostname(peek.as_public().as_server_info().as_hostname().de()?), + &hostname, &peek.as_private().as_ssh_privkey().de()?, &peek.as_private().as_ssh_pubkeys().de()?, SSH_DIR, ) .await?; crate::ssh::sync_keys( - &Hostname(peek.as_public().as_server_info().as_hostname().de()?), + &hostname, &peek.as_private().as_ssh_privkey().de()?, &Default::default(), "/root/.ssh", diff --git a/core/src/net/dns.rs b/core/src/net/dns.rs index d6e26e2f5..1083b74dc 100644 --- a/core/src/net/dns.rs +++ b/core/src/net/dns.rs @@ -279,9 +279,7 @@ impl Resolver { let Some((ref config, ref opts)) = last_config else { continue; }; - let static_servers: Option< - std::collections::VecDeque, - > = db + let static_servers: Option> = db .peek() .await .as_public() @@ -290,12 +288,9 @@ impl Resolver { .as_dns() .as_static_servers() .de()?; - let hash = - crate::util::serde::hash_serializable::(&( - config, - opts, - &static_servers, - ))?; + let hash = crate::util::serde::hash_serializable::( + &(config, opts, &static_servers), + )?; if hash == prev { prev = hash; continue; @@ -320,26 +315,25 @@ impl Resolver { .await .result?; } - let forward_servers = - if let Some(servers) = &static_servers { - servers - .iter() - .flat_map(|addr| { - [ - NameServerConfig::new(*addr, Protocol::Udp), - NameServerConfig::new(*addr, Protocol::Tcp), - ] - }) - .map(|n| to_value(&n)) - .collect::>()? - } else { - config - .name_servers() - .into_iter() - .skip(4) - .map(to_value) - .collect::>()? - }; + let forward_servers = if let Some(servers) = &static_servers { + servers + .iter() + .flat_map(|addr| { + [ + NameServerConfig::new(*addr, Protocol::Udp), + NameServerConfig::new(*addr, Protocol::Tcp), + ] + }) + .map(|n| to_value(&n)) + .collect::>()? + } else { + config + .name_servers() + .into_iter() + .skip(4) + .map(to_value) + .collect::>()? + }; let auth: Vec> = vec![Arc::new( ForwardAuthority::builder_tokio(ForwardConfig { name_servers: from_value(Value::Array(forward_servers))?, @@ -349,17 +343,15 @@ impl Resolver { .map_err(|e| Error::new(eyre!("{e}"), ErrorKind::Network))?, )]; { - let mut guard = tokio::time::timeout( - Duration::from_secs(10), - catalog.write(), - ) - .await - .map_err(|_| { - Error::new( - eyre!("{}", t!("net.dns.timeout-updating-catalog")), - ErrorKind::Timeout, - ) - })?; + let mut guard = + tokio::time::timeout(Duration::from_secs(10), catalog.write()) + .await + .map_err(|_| { + Error::new( + eyre!("{}", t!("net.dns.timeout-updating-catalog")), + ErrorKind::Timeout, + ) + })?; guard.upsert(Name::root().into(), auth); drop(guard); } diff --git a/core/src/net/gateway.rs b/core/src/net/gateway.rs index c1120fd89..e9f58881e 100644 --- a/core/src/net/gateway.rs +++ b/core/src/net/gateway.rs @@ -3,7 +3,7 @@ use std::future::Future; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; use std::task::Poll; -use std::time::Duration; +use std::time::{Duration, Instant}; use clap::Parser; use futures::{FutureExt, Stream, StreamExt, TryStreamExt}; @@ -732,6 +732,57 @@ async fn get_wan_ipv4(iface: &str, base_url: &Url) -> Result, E Ok(Some(trimmed.parse()?)) } +struct PolicyRoutingCleanup { + table_id: u32, + iface: String, +} +impl Drop for PolicyRoutingCleanup { + fn drop(&mut self) { + let table_str = self.table_id.to_string(); + let iface = std::mem::take(&mut self.iface); + tokio::spawn(async move { + Command::new("ip") + .arg("rule") + .arg("del") + .arg("fwmark") + .arg(&table_str) + .arg("lookup") + .arg(&table_str) + .arg("priority") + .arg("50") + .invoke(ErrorKind::Network) + .await + .log_err(); + Command::new("ip") + .arg("route") + .arg("flush") + .arg("table") + .arg(&table_str) + .invoke(ErrorKind::Network) + .await + .log_err(); + Command::new("iptables") + .arg("-t") + .arg("mangle") + .arg("-D") + .arg("PREROUTING") + .arg("-i") + .arg(&iface) + .arg("-m") + .arg("conntrack") + .arg("--ctstate") + .arg("NEW") + .arg("-j") + .arg("CONNMARK") + .arg("--set-mark") + .arg(&table_str) + .invoke(ErrorKind::Network) + .await + .log_err(); + }); + } +} + #[instrument(skip(connection, device_proxy, write_to, db))] async fn watch_ip( connection: &Connection, @@ -758,12 +809,14 @@ async fn watch_ip( .with_stream(device_proxy.receive_ip6_config_changed().await.stub()) .with_async_fn(|| { async { - tokio::time::sleep(Duration::from_secs(300)).await; + tokio::time::sleep(Duration::from_secs(600)).await; Ok(()) } .fuse() }); + let mut prev_attempt: Option = None; + loop { until .run(async { @@ -850,10 +903,7 @@ async fn watch_ip( // Policy routing: track per-interface table for cleanup on scope exit let policy_table_id = if !matches!( device_type, - Some( - NetworkInterfaceType::Bridge - | NetworkInterfaceType::Loopback - ) + Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback) ) { if_nametoindex(iface.as_str()) .map(|idx| 1000 + idx) @@ -861,44 +911,7 @@ async fn watch_ip( } else { None }; - struct PolicyRoutingCleanup { - table_id: u32, - iface: String, - } - impl Drop for PolicyRoutingCleanup { - fn drop(&mut self) { - let table_str = self.table_id.to_string(); - let iface = std::mem::take(&mut self.iface); - tokio::spawn(async move { - Command::new("ip") - .arg("rule").arg("del") - .arg("fwmark").arg(&table_str) - .arg("lookup").arg(&table_str) - .arg("priority").arg("50") - .invoke(ErrorKind::Network) - .await - .log_err(); - Command::new("ip") - .arg("route").arg("flush") - .arg("table").arg(&table_str) - .invoke(ErrorKind::Network) - .await - .log_err(); - Command::new("iptables") - .arg("-t").arg("mangle") - .arg("-D").arg("PREROUTING") - .arg("-i").arg(&iface) - .arg("-m").arg("conntrack") - .arg("--ctstate").arg("NEW") - .arg("-j").arg("CONNMARK") - .arg("--set-mark").arg(&table_str) - .invoke(ErrorKind::Network) - .await - .log_err(); - }); - } - } - let _policy_guard: Option = + let policy_guard: Option = policy_table_id.map(|t| PolicyRoutingCleanup { table_id: t, iface: iface.as_str().to_owned(), @@ -906,271 +919,18 @@ async fn watch_ip( loop { until - .run(async { - let addresses = ip4_proxy - .address_data() - .await? - .into_iter() - .chain(ip6_proxy.address_data().await?) - .collect_vec(); - let lan_ip: OrdSet = [ - Some(ip4_proxy.gateway().await?) - .filter(|g| !g.is_empty()) - .and_then(|g| g.parse::().log_err()), - Some(ip6_proxy.gateway().await?) - .filter(|g| !g.is_empty()) - .and_then(|g| g.parse::().log_err()), - ] - .into_iter() - .filter_map(|a| a) - .collect(); - let mut ntp_servers = OrdSet::new(); - let mut dns_servers = OrdSet::new(); - if let Some(dhcp4_proxy) = &dhcp4_proxy { - let dhcp = dhcp4_proxy.options().await?; - if let Some(ntp) = dhcp.ntp_servers { - ntp_servers.extend( - ntp.split_whitespace() - .map(InternedString::intern), - ); - } - if let Some(dns) = dhcp.domain_name_servers { - dns_servers.extend( - dns.split_ascii_whitespace() - .filter_map(|s| { - s.parse::().log_err() - }) - .collect::>(), - ); - } - } - let scope_id = if_nametoindex(iface.as_str()) - .with_kind(ErrorKind::Network)?; - let subnets: OrdSet = addresses - .into_iter() - .map(IpNet::try_from) - .try_collect()?; - // Policy routing: ensure replies exit the same interface - // they arrived on, eliminating the need for MASQUERADE. - if let Some(guard) = &_policy_guard { - let table_id = guard.table_id; - let table_str = table_id.to_string(); - - let ipv4_gateway: Option = - lan_ip.iter().find_map(|ip| match ip { - IpAddr::V4(v4) => Some(v4), - _ => None, - }).copied(); - - // Flush and rebuild per-interface routing table. - // Clone all non-default routes from the main table so - // that LAN IPs on other subnets remain reachable when - // the priority-75 catch-all overrides default routing, - // then replace the default route with this interface's. - Command::new("ip") - .arg("route").arg("flush") - .arg("table").arg(&table_str) - .invoke(ErrorKind::Network) - .await - .log_err(); - if let Ok(main_routes) = Command::new("ip") - .arg("route").arg("show") - .arg("table").arg("main") - .invoke(ErrorKind::Network) - .await - .and_then(|b| String::from_utf8(b).with_kind(ErrorKind::Utf8)) - { - for line in main_routes.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with("default") { - continue; - } - let mut cmd = Command::new("ip"); - cmd.arg("route").arg("add"); - for part in line.split_whitespace() { - // Skip status flags that appear in - // route output but are not valid for - // `ip route add`. - if part == "linkdown" || part == "dead" { - continue; - } - cmd.arg(part); - } - cmd.arg("table").arg(&table_str); - cmd.invoke(ErrorKind::Network) - .await - .log_err(); - } - } - // Add default route via this interface's gateway - { - let mut cmd = Command::new("ip"); - cmd.arg("route").arg("add").arg("default"); - if let Some(gw) = ipv4_gateway { - cmd.arg("via").arg(gw.to_string()); - } - cmd.arg("dev").arg(iface.as_str()) - .arg("table").arg(&table_str); - if ipv4_gateway.is_none() { - cmd.arg("scope").arg("link"); - } - cmd.invoke(ErrorKind::Network) - .await - .log_err(); - } - - // Ensure global CONNMARK restore rules in mangle - // PREROUTING (forwarded packets) and OUTPUT (locally-generated replies). - // Both are needed: PREROUTING handles DNAT-forwarded traffic, - // OUTPUT handles replies from locally-bound listeners (e.g. vhost). - // The `-m mark --mark 0` condition ensures we only restore - // when the packet has no existing fwmark, preserving marks - // set by WireGuard on encapsulation packets. - for chain in ["PREROUTING", "OUTPUT"] { - if Command::new("iptables") - .arg("-t").arg("mangle") - .arg("-C").arg(chain) - .arg("-m").arg("mark").arg("--mark").arg("0") - .arg("-j").arg("CONNMARK") - .arg("--restore-mark") - .invoke(ErrorKind::Network).await - .is_err() - { - Command::new("iptables") - .arg("-t").arg("mangle") - .arg("-I").arg(chain).arg("1") - .arg("-m").arg("mark").arg("--mark").arg("0") - .arg("-j").arg("CONNMARK") - .arg("--restore-mark") - .invoke(ErrorKind::Network) - .await - .log_err(); - } - } - - // Mark NEW connections arriving on this interface - // with its routing table ID via conntrack mark - if Command::new("iptables") - .arg("-t").arg("mangle") - .arg("-C").arg("PREROUTING") - .arg("-i").arg(iface.as_str()) - .arg("-m").arg("conntrack") - .arg("--ctstate").arg("NEW") - .arg("-j").arg("CONNMARK") - .arg("--set-mark").arg(&table_str) - .invoke(ErrorKind::Network).await - .is_err() - { - Command::new("iptables") - .arg("-t").arg("mangle") - .arg("-A").arg("PREROUTING") - .arg("-i").arg(iface.as_str()) - .arg("-m").arg("conntrack") - .arg("--ctstate").arg("NEW") - .arg("-j").arg("CONNMARK") - .arg("--set-mark").arg(&table_str) - .invoke(ErrorKind::Network) - .await - .log_err(); - } - - // Ensure fwmark-based ip rule for this interface's table - let rules_output = String::from_utf8( - Command::new("ip") - .arg("rule").arg("list") - .invoke(ErrorKind::Network) - .await?, - )?; - if !rules_output.lines().any(|l| { - l.contains("fwmark") - && l.contains(&format!("lookup {table_id}")) - }) { - Command::new("ip") - .arg("rule").arg("add") - .arg("fwmark").arg(&table_str) - .arg("lookup").arg(&table_str) - .arg("priority").arg("50") - .invoke(ErrorKind::Network) - .await - .log_err(); - } - } - let ifconfig_url = if let Some(db) = db { - db.peek() - .await - .as_public() - .as_server_info() - .as_ifconfig_url() - .de() - .unwrap_or_else(|_| crate::db::model::public::default_ifconfig_url()) - } else { - crate::db::model::public::default_ifconfig_url() - }; - let wan_ip = if !subnets.is_empty() - && !matches!( - device_type, - Some( - NetworkInterfaceType::Bridge - | NetworkInterfaceType::Loopback - ) - ) { - match get_wan_ipv4(iface.as_str(), &ifconfig_url).await { - Ok(a) => a, - Err(e) => { - tracing::error!( - "{}", - t!("net.gateway.failed-to-determine-wan-ip", iface = iface.to_string(), error = e.to_string()) - ); - tracing::debug!("{e:?}"); - None - } - } - } else { - None - }; - let mut ip_info = IpInfo { - name: name.clone(), - scope_id, - device_type, - subnets, - lan_ip, - wan_ip, - ntp_servers, - dns_servers, - }; - - write_to.send_if_modified( - |m: &mut OrdMap| { - let (name, secure, gateway_type, prev_wan_ip) = m - .get(&iface) - .map_or((None, None, None, None), |i| { - ( - i.name.clone(), - i.secure, - i.gateway_type, - i.ip_info - .as_ref() - .and_then(|i| i.wan_ip), - ) - }); - ip_info.wan_ip = ip_info.wan_ip.or(prev_wan_ip); - let ip_info = Arc::new(ip_info); - m.insert( - iface.clone(), - NetworkInterfaceInfo { - name, - secure, - ip_info: Some(ip_info.clone()), - gateway_type, - }, - ) - .filter(|old| &old.ip_info == &Some(ip_info)) - .is_none() - }, - ); - - Ok::<_, Error>(()) - }) + .run(poll_ip_info( + &ip4_proxy, + &ip6_proxy, + &dhcp4_proxy, + &policy_guard, + &iface, + &mut prev_attempt, + db, + write_to, + device_type, + &name, + )) .await?; } }) @@ -1181,6 +941,319 @@ async fn watch_ip( } } +async fn apply_policy_routing( + guard: &PolicyRoutingCleanup, + iface: &GatewayId, + lan_ip: &OrdSet, +) -> Result<(), Error> { + let table_id = guard.table_id; + let table_str = table_id.to_string(); + + let ipv4_gateway: Option = lan_ip + .iter() + .find_map(|ip| match ip { + IpAddr::V4(v4) => Some(v4), + _ => None, + }) + .copied(); + + // Flush and rebuild per-interface routing table. + // Clone all non-default routes from the main table so that LAN IPs on + // other subnets remain reachable when the priority-75 catch-all overrides + // default routing, then replace the default route with this interface's. + Command::new("ip") + .arg("route") + .arg("flush") + .arg("table") + .arg(&table_str) + .invoke(ErrorKind::Network) + .await + .log_err(); + if let Ok(main_routes) = Command::new("ip") + .arg("route") + .arg("show") + .arg("table") + .arg("main") + .invoke(ErrorKind::Network) + .await + .and_then(|b| String::from_utf8(b).with_kind(ErrorKind::Utf8)) + { + for line in main_routes.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with("default") { + continue; + } + let mut cmd = Command::new("ip"); + cmd.arg("route").arg("add"); + for part in line.split_whitespace() { + // Skip status flags that appear in route output but + // are not valid for `ip route add`. + if part == "linkdown" || part == "dead" { + continue; + } + cmd.arg(part); + } + cmd.arg("table").arg(&table_str); + cmd.invoke(ErrorKind::Network).await.log_err(); + } + } + // Add default route via this interface's gateway + { + let mut cmd = Command::new("ip"); + cmd.arg("route").arg("add").arg("default"); + if let Some(gw) = ipv4_gateway { + cmd.arg("via").arg(gw.to_string()); + } + cmd.arg("dev") + .arg(iface.as_str()) + .arg("table") + .arg(&table_str); + if ipv4_gateway.is_none() { + cmd.arg("scope").arg("link"); + } + cmd.invoke(ErrorKind::Network).await.log_err(); + } + + // Ensure global CONNMARK restore rules in mangle PREROUTING (forwarded + // packets) and OUTPUT (locally-generated replies). Both are needed: + // PREROUTING handles DNAT-forwarded traffic, OUTPUT handles replies from + // locally-bound listeners (e.g. vhost). The `-m mark --mark 0` condition + // ensures we only restore when the packet has no existing fwmark, + // preserving marks set by WireGuard on encapsulation packets. + for chain in ["PREROUTING", "OUTPUT"] { + if Command::new("iptables") + .arg("-t") + .arg("mangle") + .arg("-C") + .arg(chain) + .arg("-m") + .arg("mark") + .arg("--mark") + .arg("0") + .arg("-j") + .arg("CONNMARK") + .arg("--restore-mark") + .invoke(ErrorKind::Network) + .await + .is_err() + { + Command::new("iptables") + .arg("-t") + .arg("mangle") + .arg("-I") + .arg(chain) + .arg("1") + .arg("-m") + .arg("mark") + .arg("--mark") + .arg("0") + .arg("-j") + .arg("CONNMARK") + .arg("--restore-mark") + .invoke(ErrorKind::Network) + .await + .log_err(); + } + } + + // Mark NEW connections arriving on this interface with its routing + // table ID via conntrack mark + if Command::new("iptables") + .arg("-t") + .arg("mangle") + .arg("-C") + .arg("PREROUTING") + .arg("-i") + .arg(iface.as_str()) + .arg("-m") + .arg("conntrack") + .arg("--ctstate") + .arg("NEW") + .arg("-j") + .arg("CONNMARK") + .arg("--set-mark") + .arg(&table_str) + .invoke(ErrorKind::Network) + .await + .is_err() + { + Command::new("iptables") + .arg("-t") + .arg("mangle") + .arg("-A") + .arg("PREROUTING") + .arg("-i") + .arg(iface.as_str()) + .arg("-m") + .arg("conntrack") + .arg("--ctstate") + .arg("NEW") + .arg("-j") + .arg("CONNMARK") + .arg("--set-mark") + .arg(&table_str) + .invoke(ErrorKind::Network) + .await + .log_err(); + } + + // Ensure fwmark-based ip rule for this interface's table + let rules_output = String::from_utf8( + Command::new("ip") + .arg("rule") + .arg("list") + .invoke(ErrorKind::Network) + .await?, + )?; + if !rules_output + .lines() + .any(|l| l.contains("fwmark") && l.contains(&format!("lookup {table_id}"))) + { + Command::new("ip") + .arg("rule") + .arg("add") + .arg("fwmark") + .arg(&table_str) + .arg("lookup") + .arg(&table_str) + .arg("priority") + .arg("50") + .invoke(ErrorKind::Network) + .await + .log_err(); + } + + Ok(()) +} + +async fn poll_ip_info( + ip4_proxy: &Ip4ConfigProxy<'_>, + ip6_proxy: &Ip6ConfigProxy<'_>, + dhcp4_proxy: &Option>, + policy_guard: &Option, + iface: &GatewayId, + prev_attempt: &mut Option, + db: Option<&TypedPatchDb>, + write_to: &Watch>, + device_type: Option, + name: &InternedString, +) -> Result<(), Error> { + let addresses = ip4_proxy + .address_data() + .await? + .into_iter() + .chain(ip6_proxy.address_data().await?) + .collect_vec(); + let lan_ip: OrdSet = [ + Some(ip4_proxy.gateway().await?) + .filter(|g| !g.is_empty()) + .and_then(|g| g.parse::().log_err()), + Some(ip6_proxy.gateway().await?) + .filter(|g| !g.is_empty()) + .and_then(|g| g.parse::().log_err()), + ] + .into_iter() + .filter_map(|a| a) + .collect(); + let mut ntp_servers = OrdSet::new(); + let mut dns_servers = OrdSet::new(); + if let Some(dhcp4_proxy) = dhcp4_proxy { + let dhcp = dhcp4_proxy.options().await?; + if let Some(ntp) = dhcp.ntp_servers { + ntp_servers.extend(ntp.split_whitespace().map(InternedString::intern)); + } + if let Some(dns) = dhcp.domain_name_servers { + dns_servers.extend( + dns.split_ascii_whitespace() + .filter_map(|s| s.parse::().log_err()) + .collect::>(), + ); + } + } + let scope_id = if_nametoindex(iface.as_str()).with_kind(ErrorKind::Network)?; + let subnets: OrdSet = addresses.into_iter().map(IpNet::try_from).try_collect()?; + + // Policy routing: ensure replies exit the same interface they arrived on, + // eliminating the need for MASQUERADE. + if let Some(guard) = policy_guard { + apply_policy_routing(guard, iface, &lan_ip).await?; + } + + let ifconfig_url = if let Some(db) = db { + db.peek() + .await + .as_public() + .as_server_info() + .as_ifconfig_url() + .de() + .unwrap_or_else(|_| crate::db::model::public::default_ifconfig_url()) + } else { + crate::db::model::public::default_ifconfig_url() + }; + let wan_ip = if prev_attempt.map_or(true, |i| i.elapsed() > Duration::from_secs(300)) + && !subnets.is_empty() + && !matches!( + device_type, + Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback) + ) { + *prev_attempt = Some(Instant::now()); + match get_wan_ipv4(iface.as_str(), &ifconfig_url).await { + Ok(a) => a, + Err(e) => { + tracing::error!( + "{}", + t!( + "net.gateway.failed-to-determine-wan-ip", + iface = iface.to_string(), + error = e.to_string() + ) + ); + tracing::debug!("{e:?}"); + None + } + } + } else { + None + }; + let mut ip_info = IpInfo { + name: name.clone(), + scope_id, + device_type, + subnets, + lan_ip, + wan_ip, + ntp_servers, + dns_servers, + }; + + write_to.send_if_modified(|m: &mut OrdMap| { + let (name, secure, gateway_type, prev_wan_ip) = + m.get(iface).map_or((None, None, None, None), |i| { + ( + i.name.clone(), + i.secure, + i.gateway_type, + i.ip_info.as_ref().and_then(|i| i.wan_ip), + ) + }); + ip_info.wan_ip = ip_info.wan_ip.or(prev_wan_ip); + let ip_info = Arc::new(ip_info); + m.insert( + iface.clone(), + NetworkInterfaceInfo { + name, + secure, + ip_info: Some(ip_info.clone()), + gateway_type, + }, + ) + .filter(|old| &old.ip_info == &Some(ip_info)) + .is_none() + }); + + Ok(()) +} + #[instrument(skip(_connection, device_proxy, watch_activation))] async fn watch_activated( _connection: &Connection, @@ -1287,8 +1360,7 @@ 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 hostname = crate::hostname::ServerHostname::load(db.as_public().as_server_info())?; let ports = db.as_private().as_available_ports().de()?; for host in all_hosts(db) { host?.update_addresses(&hostname, info, &ports)?; diff --git a/core/src/net/host/address.rs b/core/src/net/host/address.rs index b2e30ce33..f7ee40469 100644 --- a/core/src/net/host/address.rs +++ b/core/src/net/host/address.rs @@ -10,7 +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::hostname::ServerHostname; use crate::net::acme::AcmeProvider; use crate::net::host::{HostApiKind, all_hosts}; use crate::prelude::*; @@ -197,7 +197,7 @@ 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 hostname = ServerHostname::load(db.as_public().as_server_info())?; 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(&hostname, &gateways, &ports) @@ -230,8 +230,13 @@ 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 hostname = ServerHostname::load(db.as_public().as_server_info())?; + 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(&hostname, &gateways, &ports) }) @@ -262,8 +267,13 @@ 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 hostname = ServerHostname::load(db.as_public().as_server_info())?; + 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(&hostname, &gateways, &ports) }) @@ -284,8 +294,13 @@ 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 hostname = ServerHostname::load(db.as_public().as_server_info())?; + 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(&hostname, &gateways, &ports) }) diff --git a/core/src/net/host/mod.rs b/core/src/net/host/mod.rs index bec7854de..b26355ff3 100644 --- a/core/src/net/host/mod.rs +++ b/core/src/net/host/mod.rs @@ -15,7 +15,7 @@ use ts_rs::TS; use crate::context::RpcContext; use crate::db::model::DatabaseModel; use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType}; -use crate::hostname::Hostname; +use crate::hostname::ServerHostname; use crate::net::forward::AvailablePorts; use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api}; use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding}; @@ -82,7 +82,7 @@ impl Host { impl Model { pub fn update_addresses( &mut self, - mdns: &Hostname, + mdns: &ServerHostname, gateways: &OrdMap, available_ports: &AvailablePorts, ) -> Result<(), Error> { diff --git a/core/src/net/net_controller.rs b/core/src/net/net_controller.rs index e35f193f9..608545e2f 100644 --- a/core/src/net/net_controller.rs +++ b/core/src/net/net_controller.rs @@ -13,7 +13,7 @@ use tokio_rustls::rustls::ClientConfig as TlsClientConfig; use tracing::instrument; use crate::db::model::Database; -use crate::hostname::Hostname; +use crate::hostname::ServerHostname; use crate::net::dns::DnsController; use crate::net::forward::{ ForwardRequirements, InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule, @@ -651,7 +651,7 @@ impl NetService { .as_network() .as_gateways() .de()?; - let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); + let hostname = ServerHostname::load(db.as_public().as_server_info())?; 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)?; @@ -676,7 +676,7 @@ impl NetService { .as_network() .as_gateways() .de()?; - let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); + let hostname = ServerHostname::load(db.as_public().as_server_info())?; let ports = db.as_private().as_available_ports().de()?; if let Some(ref pkg_id) = pkg_id { for (host_id, host) in db diff --git a/core/src/net/ssl.rs b/core/src/net/ssl.rs index 748abb493..284d224a2 100644 --- a/core/src/net/ssl.rs +++ b/core/src/net/ssl.rs @@ -33,7 +33,7 @@ use crate::SOURCE_DATE; use crate::account::AccountInfo; use crate::db::model::Database; use crate::db::{DbAccess, DbAccessMut}; -use crate::hostname::Hostname; +use crate::hostname::ServerHostname; use crate::init::check_time_is_synchronized; use crate::net::gateway::GatewayInfo; use crate::net::tls::TlsHandler; @@ -283,7 +283,7 @@ pub fn gen_nistp256() -> Result, Error> { #[instrument(skip_all)] pub fn make_root_cert( root_key: &PKey, - hostname: &Hostname, + hostname: &ServerHostname, start_time: SystemTime, ) -> Result { let mut builder = X509Builder::new()?; @@ -300,7 +300,8 @@ pub fn make_root_cert( builder.set_serial_number(&*rand_serial()?)?; let mut subject_name_builder = X509NameBuilder::new()?; - subject_name_builder.append_entry_by_text("CN", &format!("{} Local Root CA", &*hostname.0))?; + subject_name_builder + .append_entry_by_text("CN", &format!("{} Local Root CA", hostname.as_ref()))?; subject_name_builder.append_entry_by_text("O", "Start9")?; subject_name_builder.append_entry_by_text("OU", "StartOS")?; let subject_name = subject_name_builder.build(); diff --git a/core/src/net/static_server.rs b/core/src/net/static_server.rs index 1ec528828..d52014afa 100644 --- a/core/src/net/static_server.rs +++ b/core/src/net/static_server.rs @@ -31,7 +31,7 @@ use tokio_util::io::ReaderStream; use url::Url; use crate::context::{DiagnosticContext, InitContext, RpcContext, SetupContext}; -use crate::hostname::Hostname; +use crate::hostname::ServerHostname; use crate::middleware::auth::Auth; use crate::middleware::auth::session::ValidSessionToken; use crate::middleware::cors::Cors; @@ -105,8 +105,9 @@ impl UiContext for RpcContext { get(move || { let ctx = self.clone(); async move { - ctx.account - .peek(|account| cert_send(&account.root_ca_cert, &account.hostname)) + ctx.account.peek(|account| { + cert_send(&account.root_ca_cert, &account.hostname.hostname) + }) } }), ) @@ -419,7 +420,7 @@ pub fn bad_request() -> Response { .unwrap() } -fn cert_send(cert: &X509, hostname: &Hostname) -> Result { +fn cert_send(cert: &X509, hostname: &ServerHostname) -> Result { let pem = cert.to_pem()?; Response::builder() .status(StatusCode::OK) @@ -435,7 +436,7 @@ fn cert_send(cert: &X509, hostname: &Hostname) -> Result { .header(http::header::CONTENT_LENGTH, pem.len()) .header( http::header::CONTENT_DISPOSITION, - format!("attachment; filename={}.crt", &hostname.0), + format!("attachment; filename={}.crt", hostname.as_ref()), ) .body(Body::from(pem)) .with_kind(ErrorKind::Network) diff --git a/core/src/net/tls.rs b/core/src/net/tls.rs index 85897aec7..3d8c1b1a4 100644 --- a/core/src/net/tls.rs +++ b/core/src/net/tls.rs @@ -171,20 +171,14 @@ where let (metadata, stream) = ready!(self.accept.poll_accept(cx)?); let mut tls_handler = self.tls_handler.clone(); let mut fut = async move { - let res = match tokio::time::timeout( - Duration::from_secs(15), - async { - let mut acceptor = LazyConfigAcceptor::new( - Acceptor::default(), - BackTrackingIO::new(stream), - ); - let mut mid: tokio_rustls::StartHandshake< - BackTrackingIO, - > = match (&mut acceptor).await { + let res = match tokio::time::timeout(Duration::from_secs(15), async { + let mut acceptor = + LazyConfigAcceptor::new(Acceptor::default(), BackTrackingIO::new(stream)); + let mut mid: tokio_rustls::StartHandshake> = + match (&mut acceptor).await { Ok(a) => a, Err(e) => { - let mut stream = - acceptor.take_io().or_not_found("acceptor io")?; + let mut stream = acceptor.take_io().or_not_found("acceptor io")?; let (_, buf) = stream.rewind(); if std::str::from_utf8(buf) .ok() @@ -208,42 +202,39 @@ where } } }; - let hello = mid.client_hello(); - if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await { - let buffered = mid.io.stop_buffering(); - mid.io - .write_all(&buffered) - .await - .with_kind(ErrorKind::Network)?; - return Ok(match mid.into_stream(Arc::new(cfg)).await { - Ok(stream) => { - let s = stream.get_ref().1; - Some(( - TlsMetadata { - inner: metadata, - tls_info: TlsHandshakeInfo { - sni: s - .server_name() - .map(InternedString::intern), - alpn: s - .alpn_protocol() - .map(|a| MaybeUtf8String(a.to_vec())), - }, + let hello = mid.client_hello(); + if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await { + let buffered = mid.io.stop_buffering(); + mid.io + .write_all(&buffered) + .await + .with_kind(ErrorKind::Network)?; + return Ok(match mid.into_stream(Arc::new(cfg)).await { + Ok(stream) => { + let s = stream.get_ref().1; + Some(( + TlsMetadata { + inner: metadata, + tls_info: TlsHandshakeInfo { + sni: s.server_name().map(InternedString::intern), + alpn: s + .alpn_protocol() + .map(|a| MaybeUtf8String(a.to_vec())), }, - Box::pin(stream) as AcceptStream, - )) - } - Err(e) => { - tracing::trace!("Error completing TLS handshake: {e}"); - tracing::trace!("{e:?}"); - None - } - }); - } + }, + Box::pin(stream) as AcceptStream, + )) + } + Err(e) => { + tracing::trace!("Error completing TLS handshake: {e}"); + tracing::trace!("{e:?}"); + None + } + }); + } - Ok(None) - }, - ) + Ok(None) + }) .await { Ok(res) => res, diff --git a/core/src/net/tunnel.rs b/core/src/net/tunnel.rs index 117638851..da0f6d84c 100644 --- a/core/src/net/tunnel.rs +++ b/core/src/net/tunnel.rs @@ -175,8 +175,13 @@ 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 hostname = crate::hostname::ServerHostname::load(db.as_public().as_server_info())?; + 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?; @@ -194,8 +199,13 @@ 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 hostname = crate::hostname::ServerHostname::load(db.as_public().as_server_info())?; + 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?; diff --git a/core/src/net/wifi.rs b/core/src/net/wifi.rs index 9feb1bd59..57ccbd107 100644 --- a/core/src/net/wifi.rs +++ b/core/src/net/wifi.rs @@ -161,7 +161,10 @@ pub struct WifiAddParams { password: String, } #[instrument(skip_all)] -pub async fn add(ctx: RpcContext, WifiAddParams { ssid, password }: WifiAddParams) -> Result<(), Error> { +pub async fn add( + ctx: RpcContext, + WifiAddParams { ssid, password }: WifiAddParams, +) -> Result<(), Error> { let wifi_manager = ctx.wifi_manager.clone(); if !ssid.is_ascii() { return Err(Error::new( @@ -240,7 +243,10 @@ pub struct WifiSsidParams { } #[instrument(skip_all)] -pub async fn connect(ctx: RpcContext, WifiSsidParams { ssid }: WifiSsidParams) -> Result<(), Error> { +pub async fn connect( + ctx: RpcContext, + WifiSsidParams { ssid }: WifiSsidParams, +) -> Result<(), Error> { let wifi_manager = ctx.wifi_manager.clone(); if !ssid.is_ascii() { return Err(Error::new( diff --git a/core/src/registry/package/get.rs b/core/src/registry/package/get.rs index 3adf4431d..b18ee54c1 100644 --- a/core/src/registry/package/get.rs +++ b/core/src/registry/package/get.rs @@ -579,9 +579,8 @@ fn check_matching_info_short() { use crate::s9pk::manifest::{Alerts, Description}; use crate::util::DataUrl; - let lang_map = |s: &str| { - LocaleString::LanguageMap([("en".into(), s.into())].into_iter().collect()) - }; + let lang_map = + |s: &str| LocaleString::LanguageMap([("en".into(), s.into())].into_iter().collect()); let info = PackageVersionInfo { metadata: PackageMetadata { diff --git a/core/src/registry/package/index.rs b/core/src/registry/package/index.rs index bf88bede2..9ecd996db 100644 --- a/core/src/registry/package/index.rs +++ b/core/src/registry/package/index.rs @@ -10,7 +10,6 @@ use ts_rs::TS; use url::Url; use crate::PackageId; -use crate::service::effects::plugin::PluginId; use crate::prelude::*; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; @@ -22,6 +21,7 @@ use crate::s9pk::manifest::{ Alerts, Description, HardwareRequirements, LocaleString, current_version, }; use crate::s9pk::merkle_archive::source::FileSource; +use crate::service::effects::plugin::PluginId; use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; use crate::sign::{AnySignature, AnyVerifyingKey}; use crate::util::{DataUrl, VersionString}; diff --git a/core/src/service/action.rs b/core/src/service/action.rs index b00f654ad..e3367918f 100644 --- a/core/src/service/action.rs +++ b/core/src/service/action.rs @@ -72,7 +72,13 @@ impl Service { return Ok(None); } self.actor - .send(id, GetActionInput { id: action_id, prefill }) + .send( + id, + GetActionInput { + id: action_id, + prefill, + }, + ) .await? } } diff --git a/core/src/service/effects/action.rs b/core/src/service/effects/action.rs index 1aa0ab5bd..115e021b1 100644 --- a/core/src/service/effects/action.rs +++ b/core/src/service/effects/action.rs @@ -151,7 +151,9 @@ async fn get_action_input( .get_action_input(procedure_id, action_id, prefill) .await } else { - context.get_action_input(procedure_id, action_id, prefill).await + context + .get_action_input(procedure_id, action_id, prefill) + .await } } diff --git a/core/src/service/effects/mod.rs b/core/src/service/effects/mod.rs index e3116da13..73d06467f 100644 --- a/core/src/service/effects/mod.rs +++ b/core/src/service/effects/mod.rs @@ -178,10 +178,7 @@ pub fn handler() -> ParentHandler { ParentHandler::::new().subcommand( "url", ParentHandler::::new() - .subcommand( - "register", - from_fn_async(net::plugin::register).no_cli(), - ) + .subcommand("register", from_fn_async(net::plugin::register).no_cli()) .subcommand( "export-url", from_fn_async(net::plugin::export_url).no_cli(), diff --git a/core/src/service/uninstall.rs b/core/src/service/uninstall.rs index c979658e1..2f6515024 100644 --- a/core/src/service/uninstall.rs +++ b/core/src/service/uninstall.rs @@ -43,9 +43,8 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(), for host in all_hosts(d) { let host = host?; for (_, bind) in host.as_bindings_mut().as_entries_mut()? { - bind.as_addresses_mut() - .as_available_mut() - .mutate(|available: &mut BTreeSet| { + bind.as_addresses_mut().as_available_mut().mutate( + |available: &mut BTreeSet| { available.retain(|h| { !matches!( &h.metadata, @@ -54,7 +53,8 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(), ) }); Ok(()) - })?; + }, + )?; } } Ok(Some(pde)) diff --git a/core/src/setup.rs b/core/src/setup.rs index ea6a0fd79..9ffdb8377 100644 --- a/core/src/setup.rs +++ b/core/src/setup.rs @@ -31,7 +31,7 @@ use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::util::{DiskInfo, StartOsRecoveryInfo, pvscan, recovery_info}; -use crate::hostname::Hostname; +use crate::hostname::ServerHostnameInfo; use crate::init::{InitPhases, InitResult, init}; use crate::net::ssl::root_ca_start_time; use crate::prelude::*; @@ -116,7 +116,7 @@ async fn setup_init( ctx: &SetupContext, password: Option, kiosk: Option, - hostname: Option, + hostname: Option, init_phases: InitPhases, ) -> Result<(AccountInfo, InitResult), Error> { let init_result = init(&ctx.webserver, &ctx.config.peek(|c| c.clone()), init_phases).await?; @@ -132,7 +132,7 @@ async fn setup_init( account.set_password(password)?; } if let Some(hostname) = hostname { - account.hostname = Hostname::validate(hostname)?; + account.hostname = hostname; } account.save(m)?; let info = m.as_public_mut().as_server_info_mut(); @@ -176,6 +176,7 @@ pub struct AttachParams { pub guid: InternedString, #[ts(optional)] pub kiosk: Option, + pub name: Option, pub hostname: Option, } @@ -186,6 +187,7 @@ pub async fn attach( password, guid: disk_guid, kiosk, + name, hostname, }: AttachParams, ) -> Result { @@ -240,14 +242,10 @@ pub async fn attach( } disk_phase.complete(); - let (account, net_ctrl) = setup_init( - &setup_ctx, - password, - kiosk, - hostname.filter(|h| !h.is_empty()), - init_phases, - ) - .await?; + let hostname = ServerHostnameInfo::new_opt(name, hostname)?; + + let (account, net_ctrl) = + setup_init(&setup_ctx, password, kiosk, hostname, init_phases).await?; let rpc_ctx = RpcContext::init( &setup_ctx.webserver, @@ -260,7 +258,7 @@ pub async fn attach( Ok(( SetupResult { - hostname: account.hostname, + hostname: account.hostname.hostname, root_ca: Pem(account.root_ca_cert), needs_restart: setup_ctx.install_rootfs.peek(|a| a.is_some()), }, @@ -420,6 +418,7 @@ pub struct SetupExecuteParams { recovery_source: Option>, #[ts(optional)] kiosk: Option, + name: Option, hostname: Option, } @@ -431,6 +430,7 @@ pub async fn execute( password, recovery_source, kiosk, + name, hostname, }: SetupExecuteParams, ) -> Result { @@ -462,17 +462,10 @@ pub async fn execute( None => None, }; + let hostname = ServerHostnameInfo::new_opt(name, hostname)?; + let setup_ctx = ctx.clone(); - ctx.run_setup(move || { - execute_inner( - setup_ctx, - guid, - password, - recovery, - kiosk, - hostname.filter(|h| !h.is_empty()), - ) - })?; + ctx.run_setup(move || execute_inner(setup_ctx, guid, password, recovery, kiosk, hostname))?; Ok(ctx.progress().await) } @@ -487,7 +480,7 @@ pub async fn complete(ctx: SetupContext) -> Result { guid_file.sync_all().await?; Command::new("systemd-firstboot") .arg("--root=/media/startos/config/overlay/") - .arg(format!("--hostname={}", res.hostname.0)) + .arg(format!("--hostname={}", res.hostname.as_ref())) .invoke(ErrorKind::ParseSysInfo) .await?; Command::new("sync").invoke(ErrorKind::Filesystem).await?; @@ -561,7 +554,7 @@ pub async fn execute_inner( password: String, recovery_source: Option>, kiosk: Option, - hostname: Option, + hostname: Option, ) -> Result<(SetupResult, RpcContext), Error> { let progress = &ctx.progress; let restore_phase = match recovery_source.as_ref() { @@ -619,7 +612,7 @@ async fn fresh_setup( guid: InternedString, password: &str, kiosk: Option, - hostname: Option, + hostname: Option, SetupExecuteProgress { init_phases, rpc_ctx_phases, @@ -663,7 +656,7 @@ async fn fresh_setup( Ok(( SetupResult { - hostname: account.hostname, + hostname: account.hostname.hostname, root_ca: Pem(account.root_ca_cert), needs_restart: ctx.install_rootfs.peek(|a| a.is_some()), }, @@ -680,7 +673,7 @@ async fn recover( server_id: String, recovery_password: String, kiosk: Option, - hostname: Option, + hostname: Option, progress: SetupExecuteProgress, ) -> Result<(SetupResult, RpcContext), Error> { let recovery_source = TmpMountGuard::mount(&recovery_source, ReadWrite).await?; @@ -705,7 +698,7 @@ async fn migrate( old_guid: &str, password: String, kiosk: Option, - hostname: Option, + hostname: Option, SetupExecuteProgress { init_phases, restore_phase, @@ -798,7 +791,7 @@ async fn migrate( Ok(( SetupResult { - hostname: account.hostname, + hostname: account.hostname.hostname, root_ca: Pem(account.root_ca_cert), needs_restart: ctx.install_rootfs.peek(|a| a.is_some()), }, diff --git a/core/src/ssh.rs b/core/src/ssh.rs index a29bd76f8..69602e806 100644 --- a/core/src/ssh.rs +++ b/core/src/ssh.rs @@ -12,7 +12,7 @@ use tracing::instrument; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::hostname::Hostname; +use crate::hostname::ServerHostname; use crate::prelude::*; use crate::util::io::create_file; use crate::util::serde::{HandlerExtSerde, Pem, WithIoFormat, display_serializable}; @@ -125,7 +125,10 @@ pub struct SshAddParams { } #[instrument(skip_all)] -pub async fn add(ctx: RpcContext, SshAddParams { key }: SshAddParams) -> Result { +pub async fn add( + ctx: RpcContext, + SshAddParams { key }: SshAddParams, +) -> Result { let mut key = WithTimeData::new(key); let fingerprint = InternedString::intern(key.0.fingerprint_md5()); let (keys, res) = ctx @@ -238,7 +241,7 @@ pub async fn list(ctx: RpcContext) -> Result, Error> { #[instrument(skip_all)] pub async fn sync_keys>( - hostname: &Hostname, + hostname: &ServerHostname, privkey: &Pem, pubkeys: &SshKeys, ssh_dir: P, @@ -284,8 +287,8 @@ pub async fn sync_keys>( .to_openssh() .with_kind(ErrorKind::OpenSsh)? + " start9@" - + &*hostname.0) - .as_bytes(), + + hostname.as_ref()) + .as_bytes(), ) .await?; f.write_all(b"\n").await?; diff --git a/core/src/tunnel/api.rs b/core/src/tunnel/api.rs index 500c6c72d..10c2f21c2 100644 --- a/core/src/tunnel/api.rs +++ b/core/src/tunnel/api.rs @@ -474,7 +474,10 @@ pub async fn add_forward( }) .map(|s| s.prefix_len()) .unwrap_or(32); - let rc = ctx.forward.add_forward(source, target, prefix, None).await?; + let rc = ctx + .forward + .add_forward(source, target, prefix, None) + .await?; ctx.active_forwards.mutate(|m| { m.insert(source, rc); }); diff --git a/core/src/tunnel/web.rs b/core/src/tunnel/web.rs index e786671d5..598f05fa7 100644 --- a/core/src/tunnel/web.rs +++ b/core/src/tunnel/web.rs @@ -18,7 +18,7 @@ use tokio_rustls::rustls::server::ClientHello; use ts_rs::TS; use crate::context::CliContext; -use crate::hostname::Hostname; +use crate::hostname::ServerHostname; use crate::net::ssl::{SANInfo, root_ca_start_time}; use crate::net::tls::TlsHandler; use crate::net::web_server::Accept; @@ -292,7 +292,7 @@ pub async fn generate_certificate( let root_key = crate::net::ssl::gen_nistp256()?; let root_cert = crate::net::ssl::make_root_cert( &root_key, - &Hostname("start-tunnel".into()), + &ServerHostname::new("start-tunnel".into())?, root_ca_start_time().await, )?; let int_key = crate::net::ssl::gen_nistp256()?; @@ -523,27 +523,27 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { println!(concat!( "To access your Web URL securely, trust your Root CA (displayed above) on your client device(s):\n", " - MacOS\n", - " 1. Open the Terminal app\n", - " 2. Paste the following command (**DO NOT** click Return): pbcopy < ~/Desktop/ca.crt\n", - " 3. Copy your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n", - " 4. Back in Terminal, click Return. ca.crt is saved to your Desktop\n", - " 5. Complete by trusting your Root CA: https://docs.start9.com/device-guides/mac/ca.html\n", + " 1. Open the Terminal app\n", + " 2. Paste the following command (**DO NOT** click Return): pbcopy < ~/Desktop/ca.crt\n", + " 3. Copy your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n", + " 4. Back in Terminal, click Return. ca.crt is saved to your Desktop\n", + " 5. Complete by trusting your Root CA: https://docs.start9.com/device-guides/mac/ca.html\n", " - Linux\n", - " 1. Open gedit, nano, or any editor\n", - " 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n", - " 3. Name the file ca.crt and save as plaintext\n", - " 4. Complete by trusting your Root CA: https://docs.start9.com/device-guides/linux/ca.html\n", + " 1. Open gedit, nano, or any editor\n", + " 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n", + " 3. Name the file ca.crt and save as plaintext\n", + " 4. Complete by trusting your Root CA: https://docs.start9.com/device-guides/linux/ca.html\n", " - Windows\n", - " 1. Open the Notepad app\n", - " 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n", - " 3. Name the file ca.crt and save as plaintext\n", - " 4. Complete by trusting your Root CA: https://docs.start9.com/device-guides/windows/ca.html\n", + " 1. Open the Notepad app\n", + " 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n", + " 3. Name the file ca.crt and save as plaintext\n", + " 4. Complete by trusting your Root CA: https://docs.start9.com/device-guides/windows/ca.html\n", " - Android/Graphene\n", - " 1. Send the ca.crt file (created above) to yourself\n", - " 2. Complete by trusting your Root CA: https://docs.start9.com/device-guides/android/ca.html\n", + " 1. Send the ca.crt file (created above) to yourself\n", + " 2. Complete by trusting your Root CA: https://docs.start9.com/device-guides/android/ca.html\n", " - iOS\n", - " 1. Send the ca.crt file (created above) to yourself\n", - " 2. Complete by trusting your Root CA: https://docs.start9.com/device-guides/ios/ca.html\n", + " 1. Send the ca.crt file (created above) to yourself\n", + " 2. Complete by trusting your Root CA: https://docs.start9.com/device-guides/ios/ca.html\n", )); return Ok(()); diff --git a/core/src/version/v0_3_6_alpha_0.rs b/core/src/version/v0_3_6_alpha_0.rs index f7a8dc0bf..fbae2fc2f 100644 --- a/core/src/version/v0_3_6_alpha_0.rs +++ b/core/src/version/v0_3_6_alpha_0.rs @@ -21,7 +21,7 @@ use crate::backup::target::cifs::CifsTargets; use crate::context::RpcContext; use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::util::unmount; -use crate::hostname::Hostname; +use crate::hostname::{ServerHostname, ServerHostnameInfo}; use crate::net::forward::AvailablePorts; use crate::net::keys::KeyStore; use crate::notifications::Notifications; @@ -166,11 +166,7 @@ impl VersionT for Version { Ok((account, ssh_keys, cifs)) } - fn up( - self, - db: &mut Value, - (account, ssh_keys, cifs): Self::PreUpRes, - ) -> Result { + fn up(self, db: &mut Value, (account, ssh_keys, cifs): Self::PreUpRes) -> Result { let prev_package_data = db["package-data"].clone(); let wifi = json!({ @@ -435,12 +431,12 @@ async fn previous_account_info(pg: &sqlx::Pool) -> Result("hostname") .with_ctx(|_| (ErrorKind::Database, "hostname"))? .into(), - ), + )?), root_ca_key: PKey::private_key_from_pem( &account_query .try_get::("root_ca_key_pem") @@ -502,4 +498,3 @@ async fn previous_ssh_keys(pg: &sqlx::Pool) -> Result Result<(), Error> { Command::new("systemd-firstboot") .arg("--root=/media/startos/config/overlay/") - .arg(ctx.account.peek(|a| format!("--hostname={}", a.hostname.0))) + .arg( + ctx.account + .peek(|a| format!("--hostname={}", a.hostname.hostname.as_ref())), + ) .invoke(ErrorKind::ParseSysInfo) .await?; Ok(()) diff --git a/core/src/version/v0_4_0_alpha_20.rs b/core/src/version/v0_4_0_alpha_20.rs index 5f31223aa..1735707d3 100644 --- a/core/src/version/v0_4_0_alpha_20.rs +++ b/core/src/version/v0_4_0_alpha_20.rs @@ -166,6 +166,33 @@ impl VersionT for Version { // Rebuild from actual assigned ports in all bindings migrate_available_ports(db); + // Delete ui.name (moved to serverInfo.name) + if let Some(ui) = db + .get_mut("public") + .and_then(|p| p.get_mut("ui")) + .and_then(|u| u.as_object_mut()) + { + ui.remove("name"); + } + + // Generate serverInfo.name from serverInfo.hostname + if let Some(hostname) = db + .get("public") + .and_then(|p| p.get("serverInfo")) + .and_then(|s| s.get("hostname")) + .and_then(|h| h.as_str()) + .map(|s| s.to_owned()) + { + let name = denormalize_hostname(&hostname); + if let Some(server_info) = db + .get_mut("public") + .and_then(|p| p.get_mut("serverInfo")) + .and_then(|s| s.as_object_mut()) + { + server_info.insert("name".into(), Value::String(name.into())); + } + } + Ok(migration_data) } @@ -242,6 +269,23 @@ fn migrate_available_ports(db: &mut Value) { } } +fn denormalize_hostname(s: &str) -> String { + let mut cap = true; + s.chars() + .map(|c| { + if c == '-' { + cap = true; + ' ' + } else if cap { + cap = false; + c.to_ascii_uppercase() + } else { + c + } + }) + .collect() +} + fn migrate_host(host: Option<&mut Value>) { let Some(host) = host.and_then(|h| h.as_object_mut()) else { return; diff --git a/sdk/base/lib/osBindings/AttachParams.ts b/sdk/base/lib/osBindings/AttachParams.ts index 9b3d87422..08d6e5ac7 100644 --- a/sdk/base/lib/osBindings/AttachParams.ts +++ b/sdk/base/lib/osBindings/AttachParams.ts @@ -5,5 +5,6 @@ export type AttachParams = { password: EncryptedWire | null guid: string kiosk?: boolean + name: string | null hostname: string | null } diff --git a/sdk/base/lib/osBindings/Hostname.ts b/sdk/base/lib/osBindings/ServerHostname.ts similarity index 75% rename from sdk/base/lib/osBindings/Hostname.ts rename to sdk/base/lib/osBindings/ServerHostname.ts index 228fccca7..d73f2dc89 100644 --- a/sdk/base/lib/osBindings/Hostname.ts +++ b/sdk/base/lib/osBindings/ServerHostname.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Hostname = string +export type ServerHostname = string diff --git a/sdk/base/lib/osBindings/ServerInfo.ts b/sdk/base/lib/osBindings/ServerInfo.ts index d8d318016..a0eb98e0a 100644 --- a/sdk/base/lib/osBindings/ServerInfo.ts +++ b/sdk/base/lib/osBindings/ServerInfo.ts @@ -10,6 +10,7 @@ export type ServerInfo = { arch: string platform: string id: string + name: string hostname: string version: string packageVersionCompat: string diff --git a/sdk/base/lib/osBindings/SetServerHostnameParams.ts b/sdk/base/lib/osBindings/SetServerHostnameParams.ts index df42a6445..ac81ea3c4 100644 --- a/sdk/base/lib/osBindings/SetServerHostnameParams.ts +++ b/sdk/base/lib/osBindings/SetServerHostnameParams.ts @@ -1,3 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SetServerHostnameParams = { hostname: string } +export type SetServerHostnameParams = { + name: string | null + hostname: string | null +} diff --git a/sdk/base/lib/osBindings/SetupExecuteParams.ts b/sdk/base/lib/osBindings/SetupExecuteParams.ts index 330390060..4c3e41759 100644 --- a/sdk/base/lib/osBindings/SetupExecuteParams.ts +++ b/sdk/base/lib/osBindings/SetupExecuteParams.ts @@ -7,5 +7,6 @@ export type SetupExecuteParams = { password: EncryptedWire recoverySource: RecoverySource | null kiosk?: boolean + name: string | null hostname: string | null } diff --git a/sdk/base/lib/osBindings/StartOsRecoveryInfo.ts b/sdk/base/lib/osBindings/StartOsRecoveryInfo.ts index 0409fa2c4..7cb1aa5e7 100644 --- a/sdk/base/lib/osBindings/StartOsRecoveryInfo.ts +++ b/sdk/base/lib/osBindings/StartOsRecoveryInfo.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Hostname } from './Hostname' +import type { ServerHostname } from './ServerHostname' export type StartOsRecoveryInfo = { - hostname: Hostname + hostname: ServerHostname version: string timestamp: string passwordHash: string | null diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index 9af6c7399..1e7b61c75 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -130,7 +130,6 @@ export { HealthCheckId } from './HealthCheckId' export { HostId } from './HostId' export { HostnameInfo } from './HostnameInfo' export { HostnameMetadata } from './HostnameMetadata' -export { Hostname } from './Hostname' export { Hosts } from './Hosts' export { Host } from './Host' export { IdMap } from './IdMap' @@ -237,6 +236,7 @@ export { RestorePackageParams } from './RestorePackageParams' export { RunActionParams } from './RunActionParams' export { Security } from './Security' export { ServerBackupReport } from './ServerBackupReport' +export { ServerHostname } from './ServerHostname' export { ServerInfo } from './ServerInfo' export { ServerSpecs } from './ServerSpecs' export { ServerStatus } from './ServerStatus'