Merge branch 'feat/preferred-port-design' into sdk-comments

This commit is contained in:
Aiden McClelland
2026-02-24 14:28:56 -07:00
committed by GitHub
43 changed files with 871 additions and 590 deletions

View File

@@ -1008,6 +1008,13 @@ hostname.invalid-character:
fr_FR: "Caractère invalide dans le nom d'hôte : %{char}" fr_FR: "Caractère invalide dans le nom d'hôte : %{char}"
pl_PL: "Nieprawidłowy znak w nazwie hosta: %{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.rs
init.running-preinit: init.running-preinit:
en_US: "Running preinit.sh" en_US: "Running preinit.sh"

View File

@@ -6,7 +6,7 @@ use openssl::pkey::{PKey, Private};
use openssl::x509::X509; use openssl::x509::X509;
use crate::db::model::DatabaseModel; 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::net::ssl::{gen_nistp256, make_root_cert};
use crate::prelude::*; use crate::prelude::*;
use crate::util::serde::Pem; use crate::util::serde::Pem;
@@ -23,7 +23,7 @@ fn hash_password(password: &str) -> Result<String, Error> {
#[derive(Clone)] #[derive(Clone)]
pub struct AccountInfo { pub struct AccountInfo {
pub server_id: String, pub server_id: String,
pub hostname: Hostname, pub hostname: ServerHostnameInfo,
pub password: String, pub password: String,
pub root_ca_key: PKey<Private>, pub root_ca_key: PKey<Private>,
pub root_ca_cert: X509, pub root_ca_cert: X509,
@@ -34,16 +34,16 @@ impl AccountInfo {
pub fn new( pub fn new(
password: &str, password: &str,
start_time: SystemTime, start_time: SystemTime,
hostname: Option<InternedString>, hostname: Option<ServerHostnameInfo>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let server_id = generate_id(); let server_id = generate_id();
let hostname = if let Some(h) = hostname { let hostname = if let Some(h) = hostname {
Hostname::validate(h)? h
} else { } else {
generate_hostname() ServerHostnameInfo::from_hostname(generate_hostname())
}; };
let root_ca_key = gen_nistp256()?; 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( let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random(
&mut ssh_key::rand_core::OsRng::default(), &mut ssh_key::rand_core::OsRng::default(),
)); ));
@@ -62,7 +62,7 @@ impl AccountInfo {
pub fn load(db: &DatabaseModel) -> Result<Self, Error> { pub fn load(db: &DatabaseModel) -> Result<Self, Error> {
let server_id = db.as_public().as_server_info().as_id().de()?; 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 password = db.as_private().as_password().de()?;
let key_store = db.as_private().as_key_store(); let key_store = db.as_private().as_key_store();
let cert_store = key_store.as_local_certs(); let cert_store = key_store.as_local_certs();
@@ -85,7 +85,7 @@ impl AccountInfo {
pub fn save(&self, db: &mut DatabaseModel) -> Result<(), Error> { pub fn save(&self, db: &mut DatabaseModel) -> Result<(), Error> {
let server_info = db.as_public_mut().as_server_info_mut(); let server_info = db.as_public_mut().as_server_info_mut();
server_info.as_id_mut().ser(&self.server_id)?; 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 server_info
.as_pubkey_mut() .as_pubkey_mut()
.ser(&self.ssh_key.public_key().to_openssh()?)?; .ser(&self.ssh_key.public_key().to_openssh()?)?;
@@ -123,8 +123,8 @@ impl AccountInfo {
pub fn hostnames(&self) -> impl IntoIterator<Item = InternedString> + Send + '_ { pub fn hostnames(&self) -> impl IntoIterator<Item = InternedString> + Send + '_ {
[ [
self.hostname.no_dot_host_name(), (*self.hostname.hostname).clone(),
self.hostname.local_domain_name(), self.hostname.hostname.local_domain_name(),
] ]
} }
} }

View File

@@ -338,7 +338,7 @@ async fn perform_backup(
let timestamp = Utc::now(); let timestamp = Utc::now();
backup_guard.unencrypted_metadata.version = crate::version::Current::default().semver().into(); 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.unencrypted_metadata.timestamp = timestamp.clone();
backup_guard.metadata.version = crate::version::Current::default().semver().into(); backup_guard.metadata.version = crate::version::Current::default().semver().into();
backup_guard.metadata.timestamp = Some(timestamp); backup_guard.metadata.timestamp = Some(timestamp);

View File

@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use ssh_key::private::Ed25519Keypair; use ssh_key::private::Ed25519Keypair;
use crate::account::AccountInfo; 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::prelude::*;
use crate::util::serde::{Base32, Base64, Pem}; use crate::util::serde::{Base32, Base64, Pem};
@@ -27,10 +27,12 @@ impl<'de> Deserialize<'de> for OsBackup {
.map_err(serde::de::Error::custom)?, .map_err(serde::de::Error::custom)?,
1 => patch_db::value::from_value::<OsBackupV1>(tagged.rest) 1 => patch_db::value::from_value::<OsBackupV1>(tagged.rest)
.map_err(serde::de::Error::custom)? .map_err(serde::de::Error::custom)?
.project(), .project()
.map_err(serde::de::Error::custom)?,
2 => patch_db::value::from_value::<OsBackupV2>(tagged.rest) 2 => patch_db::value::from_value::<OsBackupV2>(tagged.rest)
.map_err(serde::de::Error::custom)? .map_err(serde::de::Error::custom)?
.project(), .project()
.map_err(serde::de::Error::custom)?,
v => { v => {
return Err(serde::de::Error::custom(&format!( return Err(serde::de::Error::custom(&format!(
"Unknown backup version {v}" "Unknown backup version {v}"
@@ -75,7 +77,7 @@ impl OsBackupV0 {
Ok(OsBackup { Ok(OsBackup {
account: AccountInfo { account: AccountInfo {
server_id: generate_id(), server_id: generate_id(),
hostname: generate_hostname(), hostname: ServerHostnameInfo::from_hostname(generate_hostname()),
password: Default::default(), password: Default::default(),
root_ca_key: self.root_ca_key.0, root_ca_key: self.root_ca_key.0,
root_ca_cert: self.root_ca_cert.0, root_ca_cert: self.root_ca_cert.0,
@@ -104,11 +106,11 @@ struct OsBackupV1 {
ui: Value, // JSON Value ui: Value, // JSON Value
} }
impl OsBackupV1 { impl OsBackupV1 {
fn project(self) -> OsBackup { fn project(self) -> Result<OsBackup, Error> {
OsBackup { Ok(OsBackup {
account: AccountInfo { account: AccountInfo {
server_id: self.server_id, server_id: self.server_id,
hostname: Hostname(self.hostname), hostname: ServerHostnameInfo::from_hostname(ServerHostname::new(self.hostname)?),
password: Default::default(), password: Default::default(),
root_ca_key: self.root_ca_key.0, root_ca_key: self.root_ca_key.0,
root_ca_cert: self.root_ca_cert.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), developer_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key),
}, },
ui: self.ui, ui: self.ui,
} })
} }
} }
@@ -134,11 +136,11 @@ struct OsBackupV2 {
ui: Value, // JSON Value ui: Value, // JSON Value
} }
impl OsBackupV2 { impl OsBackupV2 {
fn project(self) -> OsBackup { fn project(self) -> Result<OsBackup, Error> {
OsBackup { Ok(OsBackup {
account: AccountInfo { account: AccountInfo {
server_id: self.server_id, server_id: self.server_id,
hostname: Hostname(self.hostname), hostname: ServerHostnameInfo::from_hostname(ServerHostname::new(self.hostname)?),
password: Default::default(), password: Default::default(),
root_ca_key: self.root_ca_key.0, root_ca_key: self.root_ca_key.0,
root_ca_cert: self.root_ca_cert.0, root_ca_cert: self.root_ca_cert.0,
@@ -146,12 +148,12 @@ impl OsBackupV2 {
developer_key: self.compat_s9pk_key.0, developer_key: self.compat_s9pk_key.0,
}, },
ui: self.ui, ui: self.ui,
} })
} }
fn unproject(backup: &OsBackup) -> Self { fn unproject(backup: &OsBackup) -> Self {
Self { Self {
server_id: backup.account.server_id.clone(), 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_key: Pem(backup.account.root_ca_key.clone()),
root_ca_cert: Pem(backup.account.root_ca_cert.clone()), root_ca_cert: Pem(backup.account.root_ca_cert.clone()),
ssh_key: Pem(backup.account.ssh_key.clone()), ssh_key: Pem(backup.account.ssh_key.clone()),

View File

@@ -17,7 +17,7 @@ use crate::db::model::Database;
use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::backup::BackupMountGuard;
use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
use crate::hostname::Hostname; use crate::hostname::ServerHostnameInfo;
use crate::init::init; use crate::init::init;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::ProgressUnits; use crate::progress::ProgressUnits;
@@ -91,7 +91,7 @@ pub async fn recover_full_server(
server_id: &str, server_id: &str,
recovery_password: &str, recovery_password: &str,
kiosk: Option<bool>, kiosk: Option<bool>,
hostname: Option<InternedString>, hostname: Option<ServerHostnameInfo>,
SetupExecuteProgress { SetupExecuteProgress {
init_phases, init_phases,
restore_phase, restore_phase,
@@ -118,7 +118,7 @@ pub async fn recover_full_server(
.with_kind(ErrorKind::PasswordHashGeneration)?; .with_kind(ErrorKind::PasswordHashGeneration)?;
if let Some(h) = hostname { 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"); let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
@@ -189,7 +189,7 @@ pub async fn recover_full_server(
Ok(( Ok((
SetupResult { SetupResult {
hostname: os_backup.account.hostname, hostname: os_backup.account.hostname.hostname,
root_ca: Pem(os_backup.account.root_ca_cert), root_ca: Pem(os_backup.account.root_ca_cert),
needs_restart: ctx.install_rootfs.peek(|a| a.is_some()), needs_restart: ctx.install_rootfs.peek(|a| a.is_some()),
}, },

View File

@@ -218,7 +218,10 @@ pub struct CifsRemoveParams {
pub id: BackupTargetId, 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 { let id = if let BackupTargetId::Cifs { id } = id {
id id
} else { } else {

View File

@@ -70,7 +70,8 @@ async fn inner_main(
}; };
let (rpc_ctx, shutdown) = async { 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(); let mut shutdown_recv = rpc_ctx.shutdown.subscribe();
@@ -147,10 +148,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
.build() .build()
.expect(&t!("bins.startd.failed-to-initialize-runtime")); .expect(&t!("bins.startd.failed-to-initialize-runtime"));
let res = rt.block_on(async { let res = rt.block_on(async {
let mut server = WebServer::new( let mut server = WebServer::new(Acceptor::new(WildcardListener::new(80)?), refresher());
Acceptor::new(WildcardListener::new(80)?),
refresher(),
);
match inner_main(&mut server, &config).await { match inner_main(&mut server, &config).await {
Ok(a) => { Ok(a) => {
server.shutdown().await; server.shutdown().await;

View File

@@ -7,13 +7,13 @@ use clap::Parser;
use futures::FutureExt; use futures::FutureExt;
use rpc_toolkit::CliApp; use rpc_toolkit::CliApp;
use rust_i18n::t; use rust_i18n::t;
use tokio::net::TcpListener;
use tokio::signal::unix::signal; use tokio::signal::unix::signal;
use tracing::instrument; use tracing::instrument;
use visit_rs::Visit; use visit_rs::Visit;
use crate::context::CliContext; use crate::context::CliContext;
use crate::context::config::ClientConfig; use crate::context::config::ClientConfig;
use tokio::net::TcpListener;
use crate::net::tls::TlsListener; use crate::net::tls::TlsListener;
use crate::net::web_server::{Accept, Acceptor, MetadataVisitor, WebServer}; use crate::net::web_server::{Accept, Acceptor, MetadataVisitor, WebServer};
use crate::prelude::*; use crate::prelude::*;

View File

@@ -19,7 +19,7 @@ use crate::MAIN_DATA;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::context::config::ServerConfig; use crate::context::config::ServerConfig;
use crate::disk::mount::guard::{MountGuard, TmpMountGuard}; use crate::disk::mount::guard::{MountGuard, TmpMountGuard};
use crate::hostname::Hostname; use crate::hostname::ServerHostname;
use crate::net::gateway::WildcardListener; use crate::net::gateway::WildcardListener;
use crate::net::web_server::{WebServer, WebServerAcceptorSetter}; use crate::net::web_server::{WebServer, WebServerAcceptorSetter};
use crate::prelude::*; use crate::prelude::*;
@@ -45,7 +45,7 @@ lazy_static::lazy_static! {
#[ts(export)] #[ts(export)]
pub struct SetupResult { pub struct SetupResult {
#[ts(type = "string")] #[ts(type = "string")]
pub hostname: Hostname, pub hostname: ServerHostname,
pub root_ca: Pem<X509>, pub root_ca: Pem<X509>,
pub needs_restart: bool, pub needs_restart: bool,
} }

View File

@@ -59,7 +59,8 @@ impl Public {
platform: get_platform(), platform: get_platform(),
id: account.server_id.clone(), id: account.server_id.clone(),
version: Current::default().semver(), version: Current::default().semver(),
hostname: account.hostname.no_dot_host_name(), name: account.hostname.name.clone(),
hostname: (*account.hostname.hostname).clone(),
last_backup: None, last_backup: None,
package_version_compat: Current::default().compat().clone(), package_version_compat: Current::default().compat().clone(),
post_init_migration_todos: BTreeMap::new(), post_init_migration_todos: BTreeMap::new(),
@@ -176,13 +177,11 @@ pub fn default_ifconfig_url() -> Url {
#[ts(export)] #[ts(export)]
pub struct ServerInfo { pub struct ServerInfo {
#[serde(default = "get_arch")] #[serde(default = "get_arch")]
#[ts(type = "string")]
pub arch: InternedString, pub arch: InternedString,
#[serde(default = "get_platform")] #[serde(default = "get_platform")]
#[ts(type = "string")]
pub platform: InternedString, pub platform: InternedString,
pub id: String, pub id: String,
#[ts(type = "string")] pub name: InternedString,
pub hostname: InternedString, pub hostname: InternedString,
#[ts(type = "string")] #[ts(type = "string")]
pub version: Version, pub version: Version,

View File

@@ -19,7 +19,7 @@ use super::mount::filesystem::block_dev::BlockDev;
use super::mount::guard::TmpMountGuard; use super::mount::guard::TmpMountGuard;
use crate::disk::OsPartitionInfo; use crate::disk::OsPartitionInfo;
use crate::disk::mount::guard::GenericMountGuard; use crate::disk::mount::guard::GenericMountGuard;
use crate::hostname::Hostname; use crate::hostname::ServerHostname;
use crate::prelude::*; use crate::prelude::*;
use crate::util::Invoke; use crate::util::Invoke;
use crate::util::serde::IoFormat; use crate::util::serde::IoFormat;
@@ -61,7 +61,7 @@ pub struct PartitionInfo {
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct StartOsRecoveryInfo { pub struct StartOsRecoveryInfo {
pub hostname: Hostname, pub hostname: ServerHostname,
#[ts(type = "string")] #[ts(type = "string")]
pub version: exver::Version, pub version: exver::Version,
#[ts(type = "string")] #[ts(type = "string")]

View File

@@ -1,39 +1,41 @@
use clap::Parser; use clap::Parser;
use imbl_value::InternedString; use imbl_value::InternedString;
use lazy_format::lazy_format; use lazy_format::lazy_format;
use rand::{Rng, rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::process::Command; use tokio::process::Command;
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::public::ServerInfo;
use crate::prelude::*; use crate::prelude::*;
use crate::util::Invoke; use crate::util::Invoke;
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, ts_rs::TS)] #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, ts_rs::TS)]
#[ts(type = "string")] #[ts(type = "string")]
pub struct Hostname(pub InternedString); pub struct ServerHostname(InternedString);
impl std::ops::Deref for ServerHostname {
lazy_static::lazy_static! { type Target = InternedString;
static ref ADJECTIVES: Vec<String> = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect(); fn deref(&self) -> &Self::Target {
static ref NOUNS: Vec<String> = include_str!("./assets/nouns.txt").lines().map(|x| x.to_string()).collect();
}
impl AsRef<str> for Hostname {
fn as_ref(&self) -> &str {
&self.0 &self.0
} }
} }
impl AsRef<str> for ServerHostname {
fn as_ref(&self) -> &str {
&***self
}
}
impl Hostname { impl ServerHostname {
pub fn validate(h: InternedString) -> Result<Self, Error> { fn validate(&self) -> Result<(), Error> {
if h.is_empty() { if self.0.is_empty() {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("hostname.empty")), eyre!("{}", t!("hostname.empty")),
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); ));
} }
if let Some(c) = h if let Some(c) = self
.0
.chars() .chars()
.find(|c| !(c.is_ascii_alphanumeric() || c == &'-') || c.is_ascii_uppercase()) .find(|c| !(c.is_ascii_alphanumeric() || c == &'-') || c.is_ascii_uppercase())
{ {
@@ -42,7 +44,13 @@ impl Hostname {
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); ));
} }
Ok(Self(h)) Ok(())
}
pub fn new(hostname: InternedString) -> Result<Self, Error> {
let res = Self(hostname);
res.validate()?;
Ok(res)
} }
pub fn lan_address(&self) -> InternedString { pub fn lan_address(&self) -> InternedString {
@@ -53,17 +61,135 @@ impl Hostname {
InternedString::from_display(&lazy_format!("{}.local", self.0)) InternedString::from_display(&lazy_format!("{}.local", self.0))
} }
pub fn no_dot_host_name(&self) -> InternedString { pub fn load(server_info: &Model<ServerInfo>) -> Result<Self, Error> {
self.0.clone() Ok(Self(server_info.as_hostname().de()?))
}
pub fn save(&self, server_info: &mut Model<ServerInfo>) -> Result<(), Error> {
server_info.as_hostname_mut().ser(&**self)
} }
} }
pub fn generate_hostname() -> Hostname { #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, ts_rs::TS)]
let mut rng = rng(); #[ts(type = "string")]
let adjective = &ADJECTIVES[rng.random_range(0..ADJECTIVES.len())]; pub struct ServerHostnameInfo {
let noun = &NOUNS[rng.random_range(0..NOUNS.len())]; pub name: InternedString,
Hostname(InternedString::from_display(&lazy_format!( pub hostname: ServerHostname,
"{adjective}-{noun}" }
lazy_static::lazy_static! {
static ref ADJECTIVES: Vec<String> = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect();
static ref NOUNS: Vec<String> = include_str!("./assets/nouns.txt").lines().map(|x| x.to_string()).collect();
}
impl AsRef<str> 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::<String>();
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::<String>()
.into()
}
impl ServerHostnameInfo {
pub fn new(
name: Option<InternedString>,
hostname: Option<InternedString>,
) -> Result<Self, Error> {
Self::new_opt(name, hostname)
.map(|h| h.unwrap_or_else(|| ServerHostnameInfo::from_hostname(generate_hostname())))
}
pub fn new_opt(
name: Option<InternedString>,
hostname: Option<InternedString>,
) -> Result<Option<Self>, 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<ServerInfo>) -> Result<Self, Error> {
Ok(Self {
name: server_info.as_name().de()?,
hostname: ServerHostname::load(server_info)?,
})
}
pub fn save(&self, server_info: &mut Model<ServerInfo>) -> Result<(), Error> {
server_info.as_name_mut().ser(&self.name)?;
self.hostname.save(server_info)
}
}
pub fn generate_hostname() -> ServerHostname {
let num = rand::random::<u16>();
ServerHostname(InternedString::from_display(&lazy_format!(
"startos-{num:04x}"
))) )))
} }
@@ -73,17 +199,17 @@ pub fn generate_id() -> String {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn get_current_hostname() -> Result<Hostname, Error> { pub async fn get_current_hostname() -> Result<InternedString, Error> {
let out = Command::new("hostname") let out = Command::new("hostname")
.invoke(ErrorKind::ParseSysInfo) .invoke(ErrorKind::ParseSysInfo)
.await?; .await?;
let out_string = String::from_utf8(out)?; let out_string = String::from_utf8(out)?;
Ok(Hostname(out_string.trim().into())) Ok(out_string.trim().into())
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> { pub async fn set_hostname(hostname: &ServerHostname) -> Result<(), Error> {
let hostname = &*hostname.0; let hostname = &***hostname;
Command::new("hostnamectl") Command::new("hostnamectl")
.arg("--static") .arg("--static")
.arg("set-hostname") .arg("set-hostname")
@@ -102,7 +228,7 @@ pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> {
} }
#[instrument(skip_all)] #[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?; set_hostname(hostname).await?;
Command::new("systemctl") Command::new("systemctl")
.arg("restart") .arg("restart")
@@ -117,25 +243,31 @@ pub async fn sync_hostname(hostname: &Hostname) -> Result<(), Error> {
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
#[ts(export)] #[ts(export)]
pub struct SetServerHostnameParams { pub struct SetServerHostnameParams {
hostname: InternedString, name: Option<InternedString>,
hostname: Option<InternedString>,
} }
pub async fn set_hostname_rpc( pub async fn set_hostname_rpc(
ctx: RpcContext, ctx: RpcContext,
SetServerHostnameParams { hostname }: SetServerHostnameParams, SetServerHostnameParams { name, hostname }: SetServerHostnameParams,
) -> Result<(), Error> { ) -> 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 ctx.db
.mutate(|db| { .mutate(|db| hostname.save(db.as_public_mut().as_server_info_mut()))
db.as_public_mut()
.as_server_info_mut()
.as_hostname_mut()
.ser(&hostname.0)
})
.await .await
.result?; .result?;
ctx.account.mutate(|a| a.hostname = hostname.clone()); ctx.account.mutate(|a| a.hostname = hostname.clone());
sync_hostname(&hostname).await?; sync_hostname(&hostname.hostname).await?;
Ok(()) Ok(())
} }
#[test]
fn test_generate_hostname() {
assert_eq!(dbg!(generate_hostname().0).len(), 12);
}

View File

@@ -18,7 +18,7 @@ use crate::context::{CliContext, InitContext, RpcContext};
use crate::db::model::Database; use crate::db::model::Database;
use crate::db::model::public::ServerStatus; use crate::db::model::public::ServerStatus;
use crate::developer::OS_DEVELOPER_KEY_PATH; use crate::developer::OS_DEVELOPER_KEY_PATH;
use crate::hostname::Hostname; use crate::hostname::ServerHostname;
use crate::middleware::auth::local::LocalAuthContext; use crate::middleware::auth::local::LocalAuthContext;
use crate::net::gateway::WildcardListener; use crate::net::gateway::WildcardListener;
use crate::net::net_controller::{NetController, NetService}; use crate::net::net_controller::{NetController, NetService};
@@ -191,15 +191,16 @@ pub async fn init(
.arg(OS_DEVELOPER_KEY_PATH) .arg(OS_DEVELOPER_KEY_PATH)
.invoke(ErrorKind::Filesystem) .invoke(ErrorKind::Filesystem)
.await?; .await?;
let hostname = ServerHostname::load(peek.as_public().as_server_info())?;
crate::ssh::sync_keys( 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_privkey().de()?,
&peek.as_private().as_ssh_pubkeys().de()?, &peek.as_private().as_ssh_pubkeys().de()?,
SSH_DIR, SSH_DIR,
) )
.await?; .await?;
crate::ssh::sync_keys( 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_privkey().de()?,
&Default::default(), &Default::default(),
"/root/.ssh", "/root/.ssh",

View File

@@ -279,9 +279,7 @@ impl Resolver {
let Some((ref config, ref opts)) = last_config else { let Some((ref config, ref opts)) = last_config else {
continue; continue;
}; };
let static_servers: Option< let static_servers: Option<std::collections::VecDeque<SocketAddr>> = db
std::collections::VecDeque<SocketAddr>,
> = db
.peek() .peek()
.await .await
.as_public() .as_public()
@@ -290,12 +288,9 @@ impl Resolver {
.as_dns() .as_dns()
.as_static_servers() .as_static_servers()
.de()?; .de()?;
let hash = let hash = crate::util::serde::hash_serializable::<sha2::Sha256, _>(
crate::util::serde::hash_serializable::<sha2::Sha256, _>(&( &(config, opts, &static_servers),
config, )?;
opts,
&static_servers,
))?;
if hash == prev { if hash == prev {
prev = hash; prev = hash;
continue; continue;
@@ -320,26 +315,25 @@ impl Resolver {
.await .await
.result?; .result?;
} }
let forward_servers = let forward_servers = if let Some(servers) = &static_servers {
if let Some(servers) = &static_servers { servers
servers .iter()
.iter() .flat_map(|addr| {
.flat_map(|addr| { [
[ NameServerConfig::new(*addr, Protocol::Udp),
NameServerConfig::new(*addr, Protocol::Udp), NameServerConfig::new(*addr, Protocol::Tcp),
NameServerConfig::new(*addr, Protocol::Tcp), ]
] })
}) .map(|n| to_value(&n))
.map(|n| to_value(&n)) .collect::<Result<_, Error>>()?
.collect::<Result<_, Error>>()? } else {
} else { config
config .name_servers()
.name_servers() .into_iter()
.into_iter() .skip(4)
.skip(4) .map(to_value)
.map(to_value) .collect::<Result<_, Error>>()?
.collect::<Result<_, Error>>()? };
};
let auth: Vec<Arc<dyn AuthorityObject>> = vec![Arc::new( let auth: Vec<Arc<dyn AuthorityObject>> = vec![Arc::new(
ForwardAuthority::builder_tokio(ForwardConfig { ForwardAuthority::builder_tokio(ForwardConfig {
name_servers: from_value(Value::Array(forward_servers))?, name_servers: from_value(Value::Array(forward_servers))?,
@@ -349,17 +343,15 @@ impl Resolver {
.map_err(|e| Error::new(eyre!("{e}"), ErrorKind::Network))?, .map_err(|e| Error::new(eyre!("{e}"), ErrorKind::Network))?,
)]; )];
{ {
let mut guard = tokio::time::timeout( let mut guard =
Duration::from_secs(10), tokio::time::timeout(Duration::from_secs(10), catalog.write())
catalog.write(), .await
) .map_err(|_| {
.await Error::new(
.map_err(|_| { eyre!("{}", t!("net.dns.timeout-updating-catalog")),
Error::new( ErrorKind::Timeout,
eyre!("{}", t!("net.dns.timeout-updating-catalog")), )
ErrorKind::Timeout, })?;
)
})?;
guard.upsert(Name::root().into(), auth); guard.upsert(Name::root().into(), auth);
drop(guard); drop(guard);
} }

View File

@@ -3,7 +3,7 @@ use std::future::Future;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::Arc; use std::sync::Arc;
use std::task::Poll; use std::task::Poll;
use std::time::Duration; use std::time::{Duration, Instant};
use clap::Parser; use clap::Parser;
use futures::{FutureExt, Stream, StreamExt, TryStreamExt}; use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
@@ -732,6 +732,57 @@ async fn get_wan_ipv4(iface: &str, base_url: &Url) -> Result<Option<Ipv4Addr>, E
Ok(Some(trimmed.parse()?)) 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))] #[instrument(skip(connection, device_proxy, write_to, db))]
async fn watch_ip( async fn watch_ip(
connection: &Connection, connection: &Connection,
@@ -758,12 +809,14 @@ async fn watch_ip(
.with_stream(device_proxy.receive_ip6_config_changed().await.stub()) .with_stream(device_proxy.receive_ip6_config_changed().await.stub())
.with_async_fn(|| { .with_async_fn(|| {
async { async {
tokio::time::sleep(Duration::from_secs(300)).await; tokio::time::sleep(Duration::from_secs(600)).await;
Ok(()) Ok(())
} }
.fuse() .fuse()
}); });
let mut prev_attempt: Option<Instant> = None;
loop { loop {
until until
.run(async { .run(async {
@@ -850,10 +903,7 @@ async fn watch_ip(
// Policy routing: track per-interface table for cleanup on scope exit // Policy routing: track per-interface table for cleanup on scope exit
let policy_table_id = if !matches!( let policy_table_id = if !matches!(
device_type, device_type,
Some( Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback)
NetworkInterfaceType::Bridge
| NetworkInterfaceType::Loopback
)
) { ) {
if_nametoindex(iface.as_str()) if_nametoindex(iface.as_str())
.map(|idx| 1000 + idx) .map(|idx| 1000 + idx)
@@ -861,44 +911,7 @@ async fn watch_ip(
} else { } else {
None None
}; };
struct PolicyRoutingCleanup { let policy_guard: Option<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<PolicyRoutingCleanup> =
policy_table_id.map(|t| PolicyRoutingCleanup { policy_table_id.map(|t| PolicyRoutingCleanup {
table_id: t, table_id: t,
iface: iface.as_str().to_owned(), iface: iface.as_str().to_owned(),
@@ -906,271 +919,18 @@ async fn watch_ip(
loop { loop {
until until
.run(async { .run(poll_ip_info(
let addresses = ip4_proxy &ip4_proxy,
.address_data() &ip6_proxy,
.await? &dhcp4_proxy,
.into_iter() &policy_guard,
.chain(ip6_proxy.address_data().await?) &iface,
.collect_vec(); &mut prev_attempt,
let lan_ip: OrdSet<IpAddr> = [ db,
Some(ip4_proxy.gateway().await?) write_to,
.filter(|g| !g.is_empty()) device_type,
.and_then(|g| g.parse::<IpAddr>().log_err()), &name,
Some(ip6_proxy.gateway().await?) ))
.filter(|g| !g.is_empty())
.and_then(|g| g.parse::<IpAddr>().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::<IpAddr>().log_err()
})
.collect::<Vec<_>>(),
);
}
}
let scope_id = if_nametoindex(iface.as_str())
.with_kind(ErrorKind::Network)?;
let subnets: OrdSet<IpNet> = 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<Ipv4Addr> =
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<GatewayId, NetworkInterfaceInfo>| {
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>(())
})
.await?; .await?;
} }
}) })
@@ -1181,6 +941,319 @@ async fn watch_ip(
} }
} }
async fn apply_policy_routing(
guard: &PolicyRoutingCleanup,
iface: &GatewayId,
lan_ip: &OrdSet<IpAddr>,
) -> Result<(), Error> {
let table_id = guard.table_id;
let table_str = table_id.to_string();
let ipv4_gateway: Option<Ipv4Addr> = 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<Dhcp4ConfigProxy<'_>>,
policy_guard: &Option<PolicyRoutingCleanup>,
iface: &GatewayId,
prev_attempt: &mut Option<Instant>,
db: Option<&TypedPatchDb<Database>>,
write_to: &Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
device_type: Option<NetworkInterfaceType>,
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<IpAddr> = [
Some(ip4_proxy.gateway().await?)
.filter(|g| !g.is_empty())
.and_then(|g| g.parse::<IpAddr>().log_err()),
Some(ip6_proxy.gateway().await?)
.filter(|g| !g.is_empty())
.and_then(|g| g.parse::<IpAddr>().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::<IpAddr>().log_err())
.collect::<Vec<_>>(),
);
}
}
let scope_id = if_nametoindex(iface.as_str()).with_kind(ErrorKind::Network)?;
let subnets: OrdSet<IpNet> = 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<GatewayId, NetworkInterfaceInfo>| {
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))] #[instrument(skip(_connection, device_proxy, watch_activation))]
async fn watch_activated( async fn watch_activated(
_connection: &Connection, _connection: &Connection,
@@ -1287,8 +1360,7 @@ impl NetworkInterfaceController {
.as_network_mut() .as_network_mut()
.as_gateways_mut() .as_gateways_mut()
.ser(info)?; .ser(info)?;
let hostname = let hostname = crate::hostname::ServerHostname::load(db.as_public().as_server_info())?;
crate::hostname::Hostname(db.as_public().as_server_info().as_hostname().de()?);
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
for host in all_hosts(db) { for host in all_hosts(db) {
host?.update_addresses(&hostname, info, &ports)?; host?.update_addresses(&hostname, info, &ports)?;

View File

@@ -10,7 +10,7 @@ use ts_rs::TS;
use crate::GatewayId; use crate::GatewayId;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::hostname::Hostname; use crate::hostname::ServerHostname;
use crate::net::acme::AcmeProvider; use crate::net::acme::AcmeProvider;
use crate::net::host::{HostApiKind, all_hosts}; use crate::net::host::{HostApiKind, all_hosts};
use crate::prelude::*; use crate::prelude::*;
@@ -197,7 +197,7 @@ pub async fn add_public_domain<Kind: HostApiKind>(
.as_public_domains_mut() .as_public_domains_mut()
.insert(&fqdn, &PublicDomainConfig { acme, gateway })?; .insert(&fqdn, &PublicDomainConfig { acme, gateway })?;
handle_duplicates(db)?; 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 gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports) Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports)
@@ -230,8 +230,13 @@ pub async fn remove_public_domain<Kind: HostApiKind>(
Kind::host_for(&inheritance, db)? Kind::host_for(&inheritance, db)?
.as_public_domains_mut() .as_public_domains_mut()
.remove(&fqdn)?; .remove(&fqdn)?;
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 gateways = db
.as_public()
.as_server_info()
.as_network()
.as_gateways()
.de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports) Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports)
}) })
@@ -262,8 +267,13 @@ pub async fn add_private_domain<Kind: HostApiKind>(
.upsert(&fqdn, || Ok(BTreeSet::new()))? .upsert(&fqdn, || Ok(BTreeSet::new()))?
.mutate(|d| Ok(d.insert(gateway)))?; .mutate(|d| Ok(d.insert(gateway)))?;
handle_duplicates(db)?; 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 gateways = db
.as_public()
.as_server_info()
.as_network()
.as_gateways()
.de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports) Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports)
}) })
@@ -284,8 +294,13 @@ pub async fn remove_private_domain<Kind: HostApiKind>(
Kind::host_for(&inheritance, db)? Kind::host_for(&inheritance, db)?
.as_private_domains_mut() .as_private_domains_mut()
.mutate(|d| Ok(d.remove(&domain)))?; .mutate(|d| Ok(d.remove(&domain)))?;
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 gateways = db
.as_public()
.as_server_info()
.as_network()
.as_gateways()
.de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports) Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports)
}) })

View File

@@ -15,7 +15,7 @@ use ts_rs::TS;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType}; use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType};
use crate::hostname::Hostname; use crate::hostname::ServerHostname;
use crate::net::forward::AvailablePorts; use crate::net::forward::AvailablePorts;
use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api}; use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api};
use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding}; use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding};
@@ -82,7 +82,7 @@ impl Host {
impl Model<Host> { impl Model<Host> {
pub fn update_addresses( pub fn update_addresses(
&mut self, &mut self,
mdns: &Hostname, mdns: &ServerHostname,
gateways: &OrdMap<GatewayId, NetworkInterfaceInfo>, gateways: &OrdMap<GatewayId, NetworkInterfaceInfo>,
available_ports: &AvailablePorts, available_ports: &AvailablePorts,
) -> Result<(), Error> { ) -> Result<(), Error> {

View File

@@ -13,7 +13,7 @@ use tokio_rustls::rustls::ClientConfig as TlsClientConfig;
use tracing::instrument; use tracing::instrument;
use crate::db::model::Database; use crate::db::model::Database;
use crate::hostname::Hostname; use crate::hostname::ServerHostname;
use crate::net::dns::DnsController; use crate::net::dns::DnsController;
use crate::net::forward::{ use crate::net::forward::{
ForwardRequirements, InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule, ForwardRequirements, InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule,
@@ -651,7 +651,7 @@ impl NetService {
.as_network() .as_network()
.as_gateways() .as_gateways()
.de()?; .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 mut ports = db.as_private().as_available_ports().de()?;
let host = host_for(db, pkg_id.as_ref(), &id)?; let host = host_for(db, pkg_id.as_ref(), &id)?;
host.add_binding(&mut ports, internal_port, options)?; host.add_binding(&mut ports, internal_port, options)?;
@@ -676,7 +676,7 @@ impl NetService {
.as_network() .as_network()
.as_gateways() .as_gateways()
.de()?; .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()?; let ports = db.as_private().as_available_ports().de()?;
if let Some(ref pkg_id) = pkg_id { if let Some(ref pkg_id) = pkg_id {
for (host_id, host) in db for (host_id, host) in db

View File

@@ -33,7 +33,7 @@ use crate::SOURCE_DATE;
use crate::account::AccountInfo; use crate::account::AccountInfo;
use crate::db::model::Database; use crate::db::model::Database;
use crate::db::{DbAccess, DbAccessMut}; use crate::db::{DbAccess, DbAccessMut};
use crate::hostname::Hostname; use crate::hostname::ServerHostname;
use crate::init::check_time_is_synchronized; use crate::init::check_time_is_synchronized;
use crate::net::gateway::GatewayInfo; use crate::net::gateway::GatewayInfo;
use crate::net::tls::TlsHandler; use crate::net::tls::TlsHandler;
@@ -283,7 +283,7 @@ pub fn gen_nistp256() -> Result<PKey<Private>, Error> {
#[instrument(skip_all)] #[instrument(skip_all)]
pub fn make_root_cert( pub fn make_root_cert(
root_key: &PKey<Private>, root_key: &PKey<Private>,
hostname: &Hostname, hostname: &ServerHostname,
start_time: SystemTime, start_time: SystemTime,
) -> Result<X509, Error> { ) -> Result<X509, Error> {
let mut builder = X509Builder::new()?; let mut builder = X509Builder::new()?;
@@ -300,7 +300,8 @@ pub fn make_root_cert(
builder.set_serial_number(&*rand_serial()?)?; builder.set_serial_number(&*rand_serial()?)?;
let mut subject_name_builder = X509NameBuilder::new()?; 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("O", "Start9")?;
subject_name_builder.append_entry_by_text("OU", "StartOS")?; subject_name_builder.append_entry_by_text("OU", "StartOS")?;
let subject_name = subject_name_builder.build(); let subject_name = subject_name_builder.build();

View File

@@ -31,7 +31,7 @@ use tokio_util::io::ReaderStream;
use url::Url; use url::Url;
use crate::context::{DiagnosticContext, InitContext, RpcContext, SetupContext}; use crate::context::{DiagnosticContext, InitContext, RpcContext, SetupContext};
use crate::hostname::Hostname; use crate::hostname::ServerHostname;
use crate::middleware::auth::Auth; use crate::middleware::auth::Auth;
use crate::middleware::auth::session::ValidSessionToken; use crate::middleware::auth::session::ValidSessionToken;
use crate::middleware::cors::Cors; use crate::middleware::cors::Cors;
@@ -105,8 +105,9 @@ impl UiContext for RpcContext {
get(move || { get(move || {
let ctx = self.clone(); let ctx = self.clone();
async move { async move {
ctx.account ctx.account.peek(|account| {
.peek(|account| cert_send(&account.root_ca_cert, &account.hostname)) cert_send(&account.root_ca_cert, &account.hostname.hostname)
})
} }
}), }),
) )
@@ -419,7 +420,7 @@ pub fn bad_request() -> Response {
.unwrap() .unwrap()
} }
fn cert_send(cert: &X509, hostname: &Hostname) -> Result<Response, Error> { fn cert_send(cert: &X509, hostname: &ServerHostname) -> Result<Response, Error> {
let pem = cert.to_pem()?; let pem = cert.to_pem()?;
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
@@ -435,7 +436,7 @@ fn cert_send(cert: &X509, hostname: &Hostname) -> Result<Response, Error> {
.header(http::header::CONTENT_LENGTH, pem.len()) .header(http::header::CONTENT_LENGTH, pem.len())
.header( .header(
http::header::CONTENT_DISPOSITION, http::header::CONTENT_DISPOSITION,
format!("attachment; filename={}.crt", &hostname.0), format!("attachment; filename={}.crt", hostname.as_ref()),
) )
.body(Body::from(pem)) .body(Body::from(pem))
.with_kind(ErrorKind::Network) .with_kind(ErrorKind::Network)

View File

@@ -171,20 +171,14 @@ where
let (metadata, stream) = ready!(self.accept.poll_accept(cx)?); let (metadata, stream) = ready!(self.accept.poll_accept(cx)?);
let mut tls_handler = self.tls_handler.clone(); let mut tls_handler = self.tls_handler.clone();
let mut fut = async move { let mut fut = async move {
let res = match tokio::time::timeout( let res = match tokio::time::timeout(Duration::from_secs(15), async {
Duration::from_secs(15), let mut acceptor =
async { LazyConfigAcceptor::new(Acceptor::default(), BackTrackingIO::new(stream));
let mut acceptor = LazyConfigAcceptor::new( let mut mid: tokio_rustls::StartHandshake<BackTrackingIO<AcceptStream>> =
Acceptor::default(), match (&mut acceptor).await {
BackTrackingIO::new(stream),
);
let mut mid: tokio_rustls::StartHandshake<
BackTrackingIO<AcceptStream>,
> = match (&mut acceptor).await {
Ok(a) => a, Ok(a) => a,
Err(e) => { Err(e) => {
let mut stream = let mut stream = acceptor.take_io().or_not_found("acceptor io")?;
acceptor.take_io().or_not_found("acceptor io")?;
let (_, buf) = stream.rewind(); let (_, buf) = stream.rewind();
if std::str::from_utf8(buf) if std::str::from_utf8(buf)
.ok() .ok()
@@ -208,42 +202,39 @@ where
} }
} }
}; };
let hello = mid.client_hello(); let hello = mid.client_hello();
if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await { if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await {
let buffered = mid.io.stop_buffering(); let buffered = mid.io.stop_buffering();
mid.io mid.io
.write_all(&buffered) .write_all(&buffered)
.await .await
.with_kind(ErrorKind::Network)?; .with_kind(ErrorKind::Network)?;
return Ok(match mid.into_stream(Arc::new(cfg)).await { return Ok(match mid.into_stream(Arc::new(cfg)).await {
Ok(stream) => { Ok(stream) => {
let s = stream.get_ref().1; let s = stream.get_ref().1;
Some(( Some((
TlsMetadata { TlsMetadata {
inner: metadata, inner: metadata,
tls_info: TlsHandshakeInfo { tls_info: TlsHandshakeInfo {
sni: s sni: s.server_name().map(InternedString::intern),
.server_name() alpn: s
.map(InternedString::intern), .alpn_protocol()
alpn: s .map(|a| MaybeUtf8String(a.to_vec())),
.alpn_protocol()
.map(|a| MaybeUtf8String(a.to_vec())),
},
}, },
Box::pin(stream) as AcceptStream, },
)) Box::pin(stream) as AcceptStream,
} ))
Err(e) => { }
tracing::trace!("Error completing TLS handshake: {e}"); Err(e) => {
tracing::trace!("{e:?}"); tracing::trace!("Error completing TLS handshake: {e}");
None tracing::trace!("{e:?}");
} None
}); }
} });
}
Ok(None) Ok(None)
}, })
)
.await .await
{ {
Ok(res) => res, Ok(res) => res,

View File

@@ -175,8 +175,13 @@ pub async fn remove_tunnel(
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
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 gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let gateways = db
.as_public()
.as_server_info()
.as_network()
.as_gateways()
.de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
for host in all_hosts(db) { for host in all_hosts(db) {
let host = host?; let host = host?;
@@ -194,8 +199,13 @@ pub async fn remove_tunnel(
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
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 gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let gateways = db
.as_public()
.as_server_info()
.as_network()
.as_gateways()
.de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
for host in all_hosts(db) { for host in all_hosts(db) {
let host = host?; let host = host?;

View File

@@ -161,7 +161,10 @@ pub struct WifiAddParams {
password: String, password: String,
} }
#[instrument(skip_all)] #[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(); let wifi_manager = ctx.wifi_manager.clone();
if !ssid.is_ascii() { if !ssid.is_ascii() {
return Err(Error::new( return Err(Error::new(
@@ -240,7 +243,10 @@ pub struct WifiSsidParams {
} }
#[instrument(skip_all)] #[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(); let wifi_manager = ctx.wifi_manager.clone();
if !ssid.is_ascii() { if !ssid.is_ascii() {
return Err(Error::new( return Err(Error::new(

View File

@@ -579,9 +579,8 @@ fn check_matching_info_short() {
use crate::s9pk::manifest::{Alerts, Description}; use crate::s9pk::manifest::{Alerts, Description};
use crate::util::DataUrl; use crate::util::DataUrl;
let lang_map = |s: &str| { let lang_map =
LocaleString::LanguageMap([("en".into(), s.into())].into_iter().collect()) |s: &str| LocaleString::LanguageMap([("en".into(), s.into())].into_iter().collect());
};
let info = PackageVersionInfo { let info = PackageVersionInfo {
metadata: PackageMetadata { metadata: PackageMetadata {

View File

@@ -10,7 +10,6 @@ use ts_rs::TS;
use url::Url; use url::Url;
use crate::PackageId; use crate::PackageId;
use crate::service::effects::plugin::PluginId;
use crate::prelude::*; use crate::prelude::*;
use crate::registry::asset::RegistryAsset; use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
@@ -22,6 +21,7 @@ use crate::s9pk::manifest::{
Alerts, Description, HardwareRequirements, LocaleString, current_version, Alerts, Description, HardwareRequirements, LocaleString, current_version,
}; };
use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::merkle_archive::source::FileSource;
use crate::service::effects::plugin::PluginId;
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::sign::{AnySignature, AnyVerifyingKey}; use crate::sign::{AnySignature, AnyVerifyingKey};
use crate::util::{DataUrl, VersionString}; use crate::util::{DataUrl, VersionString};

View File

@@ -72,7 +72,13 @@ impl Service {
return Ok(None); return Ok(None);
} }
self.actor self.actor
.send(id, GetActionInput { id: action_id, prefill }) .send(
id,
GetActionInput {
id: action_id,
prefill,
},
)
.await? .await?
} }
} }

View File

@@ -151,7 +151,9 @@ async fn get_action_input(
.get_action_input(procedure_id, action_id, prefill) .get_action_input(procedure_id, action_id, prefill)
.await .await
} else { } else {
context.get_action_input(procedure_id, action_id, prefill).await context
.get_action_input(procedure_id, action_id, prefill)
.await
} }
} }

View File

@@ -178,10 +178,7 @@ pub fn handler<C: Context>() -> ParentHandler<C> {
ParentHandler::<C>::new().subcommand( ParentHandler::<C>::new().subcommand(
"url", "url",
ParentHandler::<C>::new() ParentHandler::<C>::new()
.subcommand( .subcommand("register", from_fn_async(net::plugin::register).no_cli())
"register",
from_fn_async(net::plugin::register).no_cli(),
)
.subcommand( .subcommand(
"export-url", "export-url",
from_fn_async(net::plugin::export_url).no_cli(), from_fn_async(net::plugin::export_url).no_cli(),

View File

@@ -43,9 +43,8 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(),
for host in all_hosts(d) { for host in all_hosts(d) {
let host = host?; let host = host?;
for (_, bind) in host.as_bindings_mut().as_entries_mut()? { for (_, bind) in host.as_bindings_mut().as_entries_mut()? {
bind.as_addresses_mut() bind.as_addresses_mut().as_available_mut().mutate(
.as_available_mut() |available: &mut BTreeSet<HostnameInfo>| {
.mutate(|available: &mut BTreeSet<HostnameInfo>| {
available.retain(|h| { available.retain(|h| {
!matches!( !matches!(
&h.metadata, &h.metadata,
@@ -54,7 +53,8 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(),
) )
}); });
Ok(()) Ok(())
})?; },
)?;
} }
} }
Ok(Some(pde)) Ok(Some(pde))

View File

@@ -31,7 +31,7 @@ use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::filesystem::cifs::Cifs;
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
use crate::disk::util::{DiskInfo, StartOsRecoveryInfo, pvscan, recovery_info}; 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::init::{InitPhases, InitResult, init};
use crate::net::ssl::root_ca_start_time; use crate::net::ssl::root_ca_start_time;
use crate::prelude::*; use crate::prelude::*;
@@ -116,7 +116,7 @@ async fn setup_init(
ctx: &SetupContext, ctx: &SetupContext,
password: Option<String>, password: Option<String>,
kiosk: Option<bool>, kiosk: Option<bool>,
hostname: Option<InternedString>, hostname: Option<ServerHostnameInfo>,
init_phases: InitPhases, init_phases: InitPhases,
) -> Result<(AccountInfo, InitResult), Error> { ) -> Result<(AccountInfo, InitResult), Error> {
let init_result = init(&ctx.webserver, &ctx.config.peek(|c| c.clone()), init_phases).await?; 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)?; account.set_password(password)?;
} }
if let Some(hostname) = hostname { if let Some(hostname) = hostname {
account.hostname = Hostname::validate(hostname)?; account.hostname = hostname;
} }
account.save(m)?; account.save(m)?;
let info = m.as_public_mut().as_server_info_mut(); let info = m.as_public_mut().as_server_info_mut();
@@ -176,6 +176,7 @@ pub struct AttachParams {
pub guid: InternedString, pub guid: InternedString,
#[ts(optional)] #[ts(optional)]
pub kiosk: Option<bool>, pub kiosk: Option<bool>,
pub name: Option<InternedString>,
pub hostname: Option<InternedString>, pub hostname: Option<InternedString>,
} }
@@ -186,6 +187,7 @@ pub async fn attach(
password, password,
guid: disk_guid, guid: disk_guid,
kiosk, kiosk,
name,
hostname, hostname,
}: AttachParams, }: AttachParams,
) -> Result<SetupProgress, Error> { ) -> Result<SetupProgress, Error> {
@@ -240,14 +242,10 @@ pub async fn attach(
} }
disk_phase.complete(); disk_phase.complete();
let (account, net_ctrl) = setup_init( let hostname = ServerHostnameInfo::new_opt(name, hostname)?;
&setup_ctx,
password, let (account, net_ctrl) =
kiosk, setup_init(&setup_ctx, password, kiosk, hostname, init_phases).await?;
hostname.filter(|h| !h.is_empty()),
init_phases,
)
.await?;
let rpc_ctx = RpcContext::init( let rpc_ctx = RpcContext::init(
&setup_ctx.webserver, &setup_ctx.webserver,
@@ -260,7 +258,7 @@ pub async fn attach(
Ok(( Ok((
SetupResult { SetupResult {
hostname: account.hostname, hostname: account.hostname.hostname,
root_ca: Pem(account.root_ca_cert), root_ca: Pem(account.root_ca_cert),
needs_restart: setup_ctx.install_rootfs.peek(|a| a.is_some()), needs_restart: setup_ctx.install_rootfs.peek(|a| a.is_some()),
}, },
@@ -420,6 +418,7 @@ pub struct SetupExecuteParams {
recovery_source: Option<RecoverySource<EncryptedWire>>, recovery_source: Option<RecoverySource<EncryptedWire>>,
#[ts(optional)] #[ts(optional)]
kiosk: Option<bool>, kiosk: Option<bool>,
name: Option<InternedString>,
hostname: Option<InternedString>, hostname: Option<InternedString>,
} }
@@ -431,6 +430,7 @@ pub async fn execute(
password, password,
recovery_source, recovery_source,
kiosk, kiosk,
name,
hostname, hostname,
}: SetupExecuteParams, }: SetupExecuteParams,
) -> Result<SetupProgress, Error> { ) -> Result<SetupProgress, Error> {
@@ -462,17 +462,10 @@ pub async fn execute(
None => None, None => None,
}; };
let hostname = ServerHostnameInfo::new_opt(name, hostname)?;
let setup_ctx = ctx.clone(); let setup_ctx = ctx.clone();
ctx.run_setup(move || { ctx.run_setup(move || execute_inner(setup_ctx, guid, password, recovery, kiosk, hostname))?;
execute_inner(
setup_ctx,
guid,
password,
recovery,
kiosk,
hostname.filter(|h| !h.is_empty()),
)
})?;
Ok(ctx.progress().await) Ok(ctx.progress().await)
} }
@@ -487,7 +480,7 @@ pub async fn complete(ctx: SetupContext) -> Result<SetupResult, Error> {
guid_file.sync_all().await?; guid_file.sync_all().await?;
Command::new("systemd-firstboot") Command::new("systemd-firstboot")
.arg("--root=/media/startos/config/overlay/") .arg("--root=/media/startos/config/overlay/")
.arg(format!("--hostname={}", res.hostname.0)) .arg(format!("--hostname={}", res.hostname.as_ref()))
.invoke(ErrorKind::ParseSysInfo) .invoke(ErrorKind::ParseSysInfo)
.await?; .await?;
Command::new("sync").invoke(ErrorKind::Filesystem).await?; Command::new("sync").invoke(ErrorKind::Filesystem).await?;
@@ -561,7 +554,7 @@ pub async fn execute_inner(
password: String, password: String,
recovery_source: Option<RecoverySource<String>>, recovery_source: Option<RecoverySource<String>>,
kiosk: Option<bool>, kiosk: Option<bool>,
hostname: Option<InternedString>, hostname: Option<ServerHostnameInfo>,
) -> Result<(SetupResult, RpcContext), Error> { ) -> Result<(SetupResult, RpcContext), Error> {
let progress = &ctx.progress; let progress = &ctx.progress;
let restore_phase = match recovery_source.as_ref() { let restore_phase = match recovery_source.as_ref() {
@@ -619,7 +612,7 @@ async fn fresh_setup(
guid: InternedString, guid: InternedString,
password: &str, password: &str,
kiosk: Option<bool>, kiosk: Option<bool>,
hostname: Option<InternedString>, hostname: Option<ServerHostnameInfo>,
SetupExecuteProgress { SetupExecuteProgress {
init_phases, init_phases,
rpc_ctx_phases, rpc_ctx_phases,
@@ -663,7 +656,7 @@ async fn fresh_setup(
Ok(( Ok((
SetupResult { SetupResult {
hostname: account.hostname, hostname: account.hostname.hostname,
root_ca: Pem(account.root_ca_cert), root_ca: Pem(account.root_ca_cert),
needs_restart: ctx.install_rootfs.peek(|a| a.is_some()), needs_restart: ctx.install_rootfs.peek(|a| a.is_some()),
}, },
@@ -680,7 +673,7 @@ async fn recover(
server_id: String, server_id: String,
recovery_password: String, recovery_password: String,
kiosk: Option<bool>, kiosk: Option<bool>,
hostname: Option<InternedString>, hostname: Option<ServerHostnameInfo>,
progress: SetupExecuteProgress, progress: SetupExecuteProgress,
) -> Result<(SetupResult, RpcContext), Error> { ) -> Result<(SetupResult, RpcContext), Error> {
let recovery_source = TmpMountGuard::mount(&recovery_source, ReadWrite).await?; let recovery_source = TmpMountGuard::mount(&recovery_source, ReadWrite).await?;
@@ -705,7 +698,7 @@ async fn migrate(
old_guid: &str, old_guid: &str,
password: String, password: String,
kiosk: Option<bool>, kiosk: Option<bool>,
hostname: Option<InternedString>, hostname: Option<ServerHostnameInfo>,
SetupExecuteProgress { SetupExecuteProgress {
init_phases, init_phases,
restore_phase, restore_phase,
@@ -798,7 +791,7 @@ async fn migrate(
Ok(( Ok((
SetupResult { SetupResult {
hostname: account.hostname, hostname: account.hostname.hostname,
root_ca: Pem(account.root_ca_cert), root_ca: Pem(account.root_ca_cert),
needs_restart: ctx.install_rootfs.peek(|a| a.is_some()), needs_restart: ctx.install_rootfs.peek(|a| a.is_some()),
}, },

View File

@@ -12,7 +12,7 @@ use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::hostname::Hostname; use crate::hostname::ServerHostname;
use crate::prelude::*; use crate::prelude::*;
use crate::util::io::create_file; use crate::util::io::create_file;
use crate::util::serde::{HandlerExtSerde, Pem, WithIoFormat, display_serializable}; use crate::util::serde::{HandlerExtSerde, Pem, WithIoFormat, display_serializable};
@@ -125,7 +125,10 @@ pub struct SshAddParams {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn add(ctx: RpcContext, SshAddParams { key }: SshAddParams) -> Result<SshKeyResponse, Error> { pub async fn add(
ctx: RpcContext,
SshAddParams { key }: SshAddParams,
) -> Result<SshKeyResponse, Error> {
let mut key = WithTimeData::new(key); let mut key = WithTimeData::new(key);
let fingerprint = InternedString::intern(key.0.fingerprint_md5()); let fingerprint = InternedString::intern(key.0.fingerprint_md5());
let (keys, res) = ctx let (keys, res) = ctx
@@ -238,7 +241,7 @@ pub async fn list(ctx: RpcContext) -> Result<Vec<SshKeyResponse>, Error> {
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn sync_keys<P: AsRef<Path>>( pub async fn sync_keys<P: AsRef<Path>>(
hostname: &Hostname, hostname: &ServerHostname,
privkey: &Pem<ssh_key::PrivateKey>, privkey: &Pem<ssh_key::PrivateKey>,
pubkeys: &SshKeys, pubkeys: &SshKeys,
ssh_dir: P, ssh_dir: P,
@@ -284,8 +287,8 @@ pub async fn sync_keys<P: AsRef<Path>>(
.to_openssh() .to_openssh()
.with_kind(ErrorKind::OpenSsh)? .with_kind(ErrorKind::OpenSsh)?
+ " start9@" + " start9@"
+ &*hostname.0) + hostname.as_ref())
.as_bytes(), .as_bytes(),
) )
.await?; .await?;
f.write_all(b"\n").await?; f.write_all(b"\n").await?;

View File

@@ -474,7 +474,10 @@ pub async fn add_forward(
}) })
.map(|s| s.prefix_len()) .map(|s| s.prefix_len())
.unwrap_or(32); .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| { ctx.active_forwards.mutate(|m| {
m.insert(source, rc); m.insert(source, rc);
}); });

View File

@@ -18,7 +18,7 @@ use tokio_rustls::rustls::server::ClientHello;
use ts_rs::TS; use ts_rs::TS;
use crate::context::CliContext; use crate::context::CliContext;
use crate::hostname::Hostname; use crate::hostname::ServerHostname;
use crate::net::ssl::{SANInfo, root_ca_start_time}; use crate::net::ssl::{SANInfo, root_ca_start_time};
use crate::net::tls::TlsHandler; use crate::net::tls::TlsHandler;
use crate::net::web_server::Accept; 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_key = crate::net::ssl::gen_nistp256()?;
let root_cert = crate::net::ssl::make_root_cert( let root_cert = crate::net::ssl::make_root_cert(
&root_key, &root_key,
&Hostname("start-tunnel".into()), &ServerHostname::new("start-tunnel".into())?,
root_ca_start_time().await, root_ca_start_time().await,
)?; )?;
let int_key = crate::net::ssl::gen_nistp256()?; let int_key = crate::net::ssl::gen_nistp256()?;
@@ -523,27 +523,27 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
println!(concat!( println!(concat!(
"To access your Web URL securely, trust your Root CA (displayed above) on your client device(s):\n", "To access your Web URL securely, trust your Root CA (displayed above) on your client device(s):\n",
" - MacOS\n", " - MacOS\n",
" 1. Open the Terminal app\n", " 1. Open the Terminal app\n",
" 2. Paste the following command (**DO NOT** click Return): pbcopy < ~/Desktop/ca.crt\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", " 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", " 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", " 5. Complete by trusting your Root CA: https://docs.start9.com/device-guides/mac/ca.html\n",
" - Linux\n", " - Linux\n",
" 1. Open gedit, nano, or any editor\n", " 1. Open gedit, nano, or any editor\n",
" 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\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", " 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", " 4. Complete by trusting your Root CA: https://docs.start9.com/device-guides/linux/ca.html\n",
" - Windows\n", " - Windows\n",
" 1. Open the Notepad app\n", " 1. Open the Notepad app\n",
" 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\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", " 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", " 4. Complete by trusting your Root CA: https://docs.start9.com/device-guides/windows/ca.html\n",
" - Android/Graphene\n", " - Android/Graphene\n",
" 1. Send the ca.crt file (created above) to yourself\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", " 2. Complete by trusting your Root CA: https://docs.start9.com/device-guides/android/ca.html\n",
" - iOS\n", " - iOS\n",
" 1. Send the ca.crt file (created above) to yourself\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", " 2. Complete by trusting your Root CA: https://docs.start9.com/device-guides/ios/ca.html\n",
)); ));
return Ok(()); return Ok(());

View File

@@ -21,7 +21,7 @@ use crate::backup::target::cifs::CifsTargets;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::filesystem::cifs::Cifs;
use crate::disk::mount::util::unmount; use crate::disk::mount::util::unmount;
use crate::hostname::Hostname; use crate::hostname::{ServerHostname, ServerHostnameInfo};
use crate::net::forward::AvailablePorts; use crate::net::forward::AvailablePorts;
use crate::net::keys::KeyStore; use crate::net::keys::KeyStore;
use crate::notifications::Notifications; use crate::notifications::Notifications;
@@ -166,11 +166,7 @@ impl VersionT for Version {
Ok((account, ssh_keys, cifs)) Ok((account, ssh_keys, cifs))
} }
fn up( fn up(self, db: &mut Value, (account, ssh_keys, cifs): Self::PreUpRes) -> Result<Value, Error> {
self,
db: &mut Value,
(account, ssh_keys, cifs): Self::PreUpRes,
) -> Result<Value, Error> {
let prev_package_data = db["package-data"].clone(); let prev_package_data = db["package-data"].clone();
let wifi = json!({ let wifi = json!({
@@ -435,12 +431,12 @@ async fn previous_account_info(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<Accoun
server_id: account_query server_id: account_query
.try_get("server_id") .try_get("server_id")
.with_ctx(|_| (ErrorKind::Database, "server_id"))?, .with_ctx(|_| (ErrorKind::Database, "server_id"))?,
hostname: Hostname( hostname: ServerHostnameInfo::from_hostname(ServerHostname::new(
account_query account_query
.try_get::<String, _>("hostname") .try_get::<String, _>("hostname")
.with_ctx(|_| (ErrorKind::Database, "hostname"))? .with_ctx(|_| (ErrorKind::Database, "hostname"))?
.into(), .into(),
), )?),
root_ca_key: PKey::private_key_from_pem( root_ca_key: PKey::private_key_from_pem(
&account_query &account_query
.try_get::<String, _>("root_ca_key_pem") .try_get::<String, _>("root_ca_key_pem")
@@ -502,4 +498,3 @@ async fn previous_ssh_keys(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<SshKeys, E
}; };
Ok(ssh_keys) Ok(ssh_keys)
} }

View File

@@ -50,7 +50,10 @@ impl VersionT for Version {
async fn post_up(self, ctx: &RpcContext, _input: Value) -> Result<(), Error> { async fn post_up(self, ctx: &RpcContext, _input: Value) -> Result<(), Error> {
Command::new("systemd-firstboot") Command::new("systemd-firstboot")
.arg("--root=/media/startos/config/overlay/") .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) .invoke(ErrorKind::ParseSysInfo)
.await?; .await?;
Ok(()) Ok(())

View File

@@ -168,6 +168,33 @@ impl VersionT for Version {
// Migrate SMTP: rename server->host, login->username, add security field // Migrate SMTP: rename server->host, login->username, add security field
migrate_smtp(db); migrate_smtp(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) Ok(migration_data)
} }
@@ -264,6 +291,23 @@ fn migrate_smtp(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>) { fn migrate_host(host: Option<&mut Value>) {
let Some(host) = host.and_then(|h| h.as_object_mut()) else { let Some(host) = host.and_then(|h| h.as_object_mut()) else {
return; return;

View File

@@ -5,5 +5,6 @@ export type AttachParams = {
password: EncryptedWire | null password: EncryptedWire | null
guid: string guid: string
kiosk?: boolean kiosk?: boolean
name: string | null
hostname: string | null hostname: string | null
} }

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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

View File

@@ -10,6 +10,7 @@ export type ServerInfo = {
arch: string arch: string
platform: string platform: string
id: string id: string
name: string
hostname: string hostname: string
version: string version: string
packageVersionCompat: string packageVersionCompat: string

View File

@@ -1,3 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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
}

View File

@@ -7,5 +7,6 @@ export type SetupExecuteParams = {
password: EncryptedWire password: EncryptedWire
recoverySource: RecoverySource<EncryptedWire> | null recoverySource: RecoverySource<EncryptedWire> | null
kiosk?: boolean kiosk?: boolean
name: string | null
hostname: string | null hostname: string | null
} }

View File

@@ -1,8 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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 = { export type StartOsRecoveryInfo = {
hostname: Hostname hostname: ServerHostname
version: string version: string
timestamp: string timestamp: string
passwordHash: string | null passwordHash: string | null

View File

@@ -130,7 +130,6 @@ export { HealthCheckId } from './HealthCheckId'
export { HostId } from './HostId' export { HostId } from './HostId'
export { HostnameInfo } from './HostnameInfo' export { HostnameInfo } from './HostnameInfo'
export { HostnameMetadata } from './HostnameMetadata' export { HostnameMetadata } from './HostnameMetadata'
export { Hostname } from './Hostname'
export { Hosts } from './Hosts' export { Hosts } from './Hosts'
export { Host } from './Host' export { Host } from './Host'
export { IdMap } from './IdMap' export { IdMap } from './IdMap'
@@ -237,6 +236,7 @@ export { RestorePackageParams } from './RestorePackageParams'
export { RunActionParams } from './RunActionParams' export { RunActionParams } from './RunActionParams'
export { Security } from './Security' export { Security } from './Security'
export { ServerBackupReport } from './ServerBackupReport' export { ServerBackupReport } from './ServerBackupReport'
export { ServerHostname } from './ServerHostname'
export { ServerInfo } from './ServerInfo' export { ServerInfo } from './ServerInfo'
export { ServerSpecs } from './ServerSpecs' export { ServerSpecs } from './ServerSpecs'
export { ServerStatus } from './ServerStatus' export { ServerStatus } from './ServerStatus'