wip: start-tunnel

This commit is contained in:
Aiden McClelland
2025-09-27 03:04:37 -06:00
parent 24521e3cac
commit 98f31d4891
54 changed files with 1432 additions and 819 deletions

View File

@@ -5,7 +5,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::{generate_hostname, generate_id, Hostname};
use crate::net::ssl::{generate_key, make_root_cert};
use crate::net::tor::TorSecretKey;
use crate::prelude::*;
@@ -107,6 +107,7 @@ impl AccountInfo {
.map(|tor_key| tor_key.onion_address())
.collect(),
)?;
server_info.as_password_hash_mut().ser(&self.password)?;
db.as_private_mut().as_password_mut().ser(&self.password)?;
db.as_private_mut()
.as_ssh_privkey_mut()

View File

@@ -3,11 +3,11 @@ use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use clap::Parser;
use color_eyre::eyre::eyre;
use imbl_value::{InternedString, json};
use imbl_value::{json, InternedString};
use itertools::Itertools;
use josekit::jwk::Jwk;
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{CallRemote, Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use rpc_toolkit::{from_fn_async, CallRemote, Context, HandlerArgs, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
use tracing::instrument;
@@ -20,8 +20,8 @@ use crate::middleware::auth::{
use crate::prelude::*;
use crate::util::crypto::EncryptedWire;
use crate::util::io::create_file_mod;
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
use crate::{Error, ResultExt, ensure_code};
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
use crate::{ensure_code, Error, ResultExt};
#[derive(Debug, Clone, Default, Deserialize, Serialize, TS)]
pub struct Sessions(pub BTreeMap<InternedString, Session>);
@@ -220,7 +220,7 @@ pub fn check_password(hash: &str, password: &str) -> Result<(), Error> {
pub struct LoginParams {
password: String,
#[ts(skip)]
#[serde(rename = "__auth_userAgent")] // from Auth middleware
#[serde(rename = "__Auth_userAgent")] // from Auth middleware
user_agent: Option<String>,
#[serde(default)]
ephemeral: bool,
@@ -279,7 +279,7 @@ pub async fn login_impl<C: AuthContext>(
#[command(rename_all = "kebab-case")]
pub struct LogoutParams {
#[ts(skip)]
#[serde(rename = "__auth_session")] // from Auth middleware
#[serde(rename = "__Auth_session")] // from Auth middleware
session: InternedString,
}
@@ -373,7 +373,7 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) -> Resul
pub struct ListParams {
#[arg(skip)]
#[ts(skip)]
#[serde(rename = "__auth_session")] // from Auth middleware
#[serde(rename = "__Auth_session")] // from Auth middleware
session: Option<InternedString>,
}
@@ -474,30 +474,19 @@ pub async fn reset_password_impl(
let old_password = old_password.unwrap_or_default().decrypt(&ctx)?;
let new_password = new_password.unwrap_or_default().decrypt(&ctx)?;
let mut account = ctx.account.write().await;
if !argon2::verify_encoded(&account.password, old_password.as_bytes())
.with_kind(crate::ErrorKind::IncorrectPassword)?
{
return Err(Error::new(
eyre!("Incorrect Password"),
crate::ErrorKind::IncorrectPassword,
));
}
account.set_password(&new_password)?;
let account_password = &account.password;
let account = account.clone();
ctx.db
.mutate(|d| {
d.as_public_mut()
.as_server_info_mut()
.as_password_hash_mut()
.ser(account_password)?;
account.save(d)?;
Ok(())
})
.await
.result
let account = ctx.account.mutate(|account| {
if !argon2::verify_encoded(&account.password, old_password.as_bytes())
.with_kind(crate::ErrorKind::IncorrectPassword)?
{
return Err(Error::new(
eyre!("Incorrect Password"),
crate::ErrorKind::IncorrectPassword,
));
}
account.set_password(&new_password)?;
Ok(account.clone())
})?;
ctx.db.mutate(|d| account.save(d)).await.result
}
#[instrument(skip_all)]

View File

@@ -13,8 +13,8 @@ use tokio::io::AsyncWriteExt;
use tracing::instrument;
use ts_rs::TS;
use super::PackageBackupReport;
use super::target::{BackupTargetId, PackageBackupInfo};
use super::PackageBackupReport;
use crate::backup::os::OsBackup;
use crate::backup::{BackupReport, ServerBackupReport};
use crate::context::RpcContext;
@@ -24,7 +24,7 @@ use crate::disk::mount::backup::BackupMountGuard;
use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
use crate::middleware::auth::AuthContext;
use crate::notifications::{NotificationLevel, notify};
use crate::notifications::{notify, NotificationLevel};
use crate::prelude::*;
use crate::util::io::dir_copy;
use crate::util::serde::IoFormat;
@@ -317,7 +317,7 @@ async fn perform_backup(
.with_kind(ErrorKind::Filesystem)?;
os_backup_file
.write_all(&IoFormat::Json.to_vec(&OsBackup {
account: ctx.account.read().await.clone(),
account: ctx.account.peek(|a| a.clone()),
ui,
})?)
.await?;
@@ -342,7 +342,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.read().await.hostname.clone();
backup_guard.unencrypted_metadata.hostname = ctx.account.peek(|a| a.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);

View File

@@ -3,8 +3,8 @@ use std::ffi::OsString;
use rpc_toolkit::CliApp;
use serde_json::Value;
use crate::context::CliContext;
use crate::context::config::ClientConfig;
use crate::context::CliContext;
use crate::util::logger::LOGGER;
use crate::version::{Current, VersionT};
@@ -17,7 +17,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
if let Err(e) = CliApp::new(
|cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?),
crate::expanded_api(),
crate::main_api(),
)
.run(args)
{

View File

@@ -6,9 +6,9 @@ use tracing::instrument;
use crate::context::config::ServerConfig;
use crate::context::rpc::InitRpcContextPhases;
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::disk::REPAIR_DISK_PATH;
use crate::disk::fsck::RepairStrategy;
use crate::disk::main::DEFAULT_PASSWORD;
use crate::disk::REPAIR_DISK_PATH;
use crate::firmware::{check_for_firmware_update, update_firmware};
use crate::init::{InitPhases, STANDBY_MODE_PATH};
use crate::net::web_server::{UpgradableListener, WebServer};
@@ -37,7 +37,7 @@ async fn setup_or_init(
let mut update_phase = handle.add_phase("Updating Firmware".into(), Some(10));
let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1));
server.serve_init(init_ctx);
server.serve_ui_for(init_ctx);
update_phase.start();
if let Err(e) = update_firmware(firmware).await {
@@ -93,7 +93,7 @@ async fn setup_or_init(
let ctx = InstallContext::init().await?;
server.serve_install(ctx.clone());
server.serve_ui_for(ctx.clone());
ctx.shutdown
.subscribe()
@@ -113,7 +113,7 @@ async fn setup_or_init(
{
let ctx = SetupContext::init(server, config)?;
server.serve_setup(ctx.clone());
server.serve_ui_for(ctx.clone());
let mut shutdown = ctx.shutdown.subscribe();
if let Some(shutdown) = shutdown.recv().await.expect("context dropped") {
@@ -149,7 +149,7 @@ async fn setup_or_init(
let init_phases = InitPhases::new(&handle);
let rpc_ctx_phases = InitRpcContextPhases::new(&handle);
server.serve_init(init_ctx);
server.serve_ui_for(init_ctx);
async {
disk_phase.start();
@@ -247,7 +247,7 @@ pub async fn main(
e,
)?;
server.serve_diagnostic(ctx.clone());
server.serve_ui_for(ctx.clone());
let shutdown = ctx.shutdown.subscribe().recv().await.unwrap();

View File

@@ -38,7 +38,7 @@ async fn inner_main(
};
tokio::fs::write("/run/startos/initialized", "").await?;
server.serve_main(ctx.clone());
server.serve_ui_for(ctx.clone());
LOGGER.set_logfile(None);
handle.complete();
@@ -47,7 +47,7 @@ async fn inner_main(
let init_ctx = InitContext::init(config).await?;
let handle = init_ctx.progress.clone();
let rpc_ctx_phases = InitRpcContextPhases::new(&handle);
server.serve_init(init_ctx);
server.serve_ui_for(init_ctx);
let ctx = RpcContext::init(
&server.acceptor_setter(),
@@ -63,14 +63,14 @@ async fn inner_main(
)
.await?;
server.serve_main(ctx.clone());
server.serve_ui_for(ctx.clone());
handle.complete();
ctx
};
let (rpc_ctx, shutdown) = async {
crate::hostname::sync_hostname(&rpc_ctx.account.read().await.hostname).await?;
crate::hostname::sync_hostname(&rpc_ctx.account.peek(|a| a.hostname.clone())).await?;
let mut shutdown_recv = rpc_ctx.shutdown.subscribe();
@@ -177,7 +177,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
e,
)?;
server.serve_diagnostic(ctx.clone());
server.serve_ui_for(ctx.clone());
let mut shutdown = ctx.shutdown.subscribe();

View File

@@ -6,6 +6,7 @@ use std::sync::Arc;
use cookie::{Cookie, Expiration, SameSite};
use cookie_store::CookieStore;
use http::HeaderMap;
use imbl_value::InternedString;
use josekit::jwk::Jwk;
use once_cell::sync::OnceCell;
@@ -20,13 +21,13 @@ use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
use tracing::instrument;
use super::setup::CURRENT_SECRET;
use crate::context::config::{ClientConfig, local_config_path};
use crate::context::config::{local_config_path, ClientConfig};
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::developer::{OS_DEVELOPER_KEY_PATH, default_developer_key_path};
use crate::developer::{default_developer_key_path, OS_DEVELOPER_KEY_PATH};
use crate::middleware::auth::AuthContext;
use crate::prelude::*;
use crate::rpc_continuations::Guid;
use crate::tunnel::context::TunnelContext;
use crate::util::io::read_file_to_string;
#[derive(Debug)]
pub struct CliContextSeed {
@@ -159,7 +160,7 @@ impl CliContext {
continue;
}
let pair = <ed25519::KeypairBytes as ed25519::pkcs8::DecodePrivateKey>::from_pkcs8_pem(
&std::fs::read_to_string(&self.developer_key_path)?,
&std::fs::read_to_string(path)?,
)
.with_kind(crate::ErrorKind::Pem)?;
let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| {
@@ -279,9 +280,15 @@ impl Context for CliContext {
)
}
}
impl AsRef<Client> for CliContext {
fn as_ref(&self) -> &Client {
&self.client
}
}
impl CallRemote<RpcContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
if let Ok(local) = std::fs::read_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH) {
if let Ok(local) = read_file_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH).await {
self.cookie_store
.lock()
.unwrap()
@@ -298,7 +305,8 @@ impl CallRemote<RpcContext> for CliContext {
crate::middleware::signature::call_remote(
self,
self.rpc_url.clone(),
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
@@ -307,24 +315,11 @@ impl CallRemote<RpcContext> for CliContext {
}
impl CallRemote<DiagnosticContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
if let Ok(local) = std::fs::read_to_string(TunnelContext::LOCAL_AUTH_COOKIE_PATH) {
self.cookie_store
.lock()
.unwrap()
.insert_raw(
&Cookie::build(("local", local))
.domain("localhost")
.expires(Expiration::Session)
.same_site(SameSite::Strict)
.build(),
&"http://localhost".parse()?,
)
.with_kind(crate::ErrorKind::Network)?;
}
crate::middleware::signature::call_remote(
self,
self.rpc_url.clone(),
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
@@ -336,7 +331,8 @@ impl CallRemote<InitContext> for CliContext {
crate::middleware::signature::call_remote(
self,
self.rpc_url.clone(),
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
@@ -348,7 +344,8 @@ impl CallRemote<SetupContext> for CliContext {
crate::middleware::signature::call_remote(
self,
self.rpc_url.clone(),
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
@@ -360,7 +357,8 @@ impl CallRemote<InstallContext> for CliContext {
crate::middleware::signature::call_remote(
self,
self.rpc_url.clone(),
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)

View File

@@ -1,7 +1,6 @@
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsStr;
use std::future::Future;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
@@ -46,7 +45,7 @@ use crate::service::ServiceMap;
use crate::shutdown::Shutdown;
use crate::util::io::delete_file;
use crate::util::lshw::LshwDevice;
use crate::util::sync::{SyncMutex, Watch};
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
use crate::{DATA_DIR, HOST_IP};
pub struct RpcContextSeed {
@@ -58,7 +57,7 @@ pub struct RpcContextSeed {
pub ephemeral_sessions: SyncMutex<Sessions>,
pub db: TypedPatchDb<Database>,
pub sync_db: watch::Sender<u64>,
pub account: RwLock<AccountInfo>,
pub account: SyncRwLock<AccountInfo>,
pub net_controller: Arc<NetController>,
pub os_net_service: NetService,
pub s9pk_arch: Option<&'static str>,
@@ -225,7 +224,7 @@ impl RpcContext {
ephemeral_sessions: SyncMutex::new(Sessions::new()),
sync_db: watch::Sender::new(db.sequence().await),
db,
account: RwLock::new(account),
account: SyncRwLock::new(account),
callbacks: net_controller.callbacks.clone(),
net_controller,
os_net_service,
@@ -483,6 +482,11 @@ impl RpcContext {
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra).await
}
}
impl AsRef<Client> for RpcContext {
fn as_ref(&self) -> &Client {
&self.client
}
}
impl AsRef<Jwk> for RpcContext {
fn as_ref(&self) -> &Jwk {
&CURRENT_SECRET

View File

@@ -127,7 +127,7 @@ pub struct SubscribeParams {
#[ts(type = "string | null")]
pointer: Option<JsonPointer>,
#[ts(skip)]
#[serde(rename = "__auth_session")]
#[serde(rename = "__Auth_session")]
session: Option<InternedString>,
}

View File

@@ -216,13 +216,18 @@ impl<T: Map> Model<T>
where
T::Value: Serialize,
{
pub fn insert(&mut self, key: &T::Key, value: &T::Value) -> Result<(), Error> {
pub fn insert_model(
&mut self,
key: &T::Key,
value: Model<T::Value>,
) -> Result<Option<Model<T::Value>>, Error> {
use patch_db::ModelExt;
use serde::ser::Error;
let v = patch_db::value::to_value(value)?;
let v = value.into_value();
match &mut self.value {
Value::Object(o) => {
o.insert(T::key_string(key)?, v);
Ok(())
let prev = o.insert(T::key_string(key)?, v);
Ok(prev.map(|v| Model::from_value(v)))
}
v => Err(patch_db::value::Error {
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
@@ -231,6 +236,13 @@ where
.into()),
}
}
pub fn insert(
&mut self,
key: &T::Key,
value: &T::Value,
) -> Result<Option<Model<T::Value>>, Error> {
self.insert_model(key, Model::new(value)?)
}
pub fn upsert<F>(&mut self, key: &T::Key, value: F) -> Result<&mut Model<T::Value>, Error>
where
F: FnOnce() -> Result<T::Value, Error>,
@@ -257,22 +269,6 @@ where
.into()),
}
}
pub fn insert_model(&mut self, key: &T::Key, value: Model<T::Value>) -> Result<(), Error> {
use patch_db::ModelExt;
use serde::ser::Error;
let v = value.into_value();
match &mut self.value {
Value::Object(o) => {
o.insert(T::key_string(key)?, v);
Ok(())
}
v => Err(patch_db::value::Error {
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
kind: patch_db::value::ErrorKind::Serialization,
}
.into()),
}
}
}
impl<T: Map> Model<T>

View File

@@ -175,7 +175,7 @@ pub async fn install(
#[serde(rename_all = "camelCase")]
pub struct SideloadParams {
#[ts(skip)]
#[serde(rename = "__auth_session")]
#[serde(rename = "__Auth_session")]
session: Option<InternedString>,
}

View File

@@ -79,19 +79,18 @@ pub use error::{Error, ErrorKind, ResultExt};
use imbl_value::Value;
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{
CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler, from_fn, from_fn_async,
from_fn_blocking,
from_fn, from_fn_async, from_fn_blocking, CallRemoteHandler, Context, Empty, HandlerExt,
ParentHandler,
};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::context::{
CliContext, DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext,
};
use crate::context::{CliContext, DiagnosticContext, InitContext, RpcContext};
use crate::disk::fsck::RequiresReboot;
use crate::registry::context::{RegistryContext, RegistryUrlParams};
use crate::system::kiosk;
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
use crate::tunnel::context::TunnelUrlParams;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
@@ -139,6 +138,20 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
.with_about("Display the API that is currently serving")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: InitContext| Ok::<_, Error>(ApiState::Initializing))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: DiagnosticContext| Ok::<_, Error>(ApiState::Error))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_call_remote::<CliContext>(),
)
.subcommand(
"server",
server::<C>()
@@ -191,6 +204,19 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
)
.no_cli(),
)
.subcommand(
"registry",
registry::registry_api::<CliContext>().with_about("Commands related to the registry"),
)
.subcommand(
"tunnel",
CallRemoteHandler::<RpcContext, _, _, TunnelUrlParams>::new(tunnel::api::tunnel_api())
.no_cli(),
)
.subcommand(
"tunnel",
tunnel::api::tunnel_api::<CliContext>().with_about("Commands related to StartTunnel"),
)
.subcommand(
"s9pk",
s9pk::rpc::s9pk().with_about("Commands for interacting with s9pk files"),
@@ -198,6 +224,28 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
.subcommand(
"util",
util::rpc::util::<C>().with_about("Command for calculating the blake3 hash of a file"),
)
.subcommand(
"init",
from_fn_async(developer::init)
.no_display()
.with_about("Create developer key if it doesn't exist"),
)
.subcommand(
"pubkey",
from_fn_blocking(developer::pubkey)
.with_about("Get public key for developer private key"),
)
.subcommand(
"diagnostic",
diagnostic::diagnostic::<C>()
.with_about("Commands to display logs, restart the server, etc"),
)
.subcommand("setup", setup::setup::<C>())
.subcommand(
"install",
os_install::install::<C>()
.with_about("Commands to list disk info, install StartOS, and reboot"),
);
if &*PLATFORM != "raspberrypi" {
api = api.subcommand("kiosk", kiosk::<C>());
@@ -484,127 +532,3 @@ pub fn package<C: Context>() -> ParentHandler<C> {
net::host::host_api::<C>().with_about("Manage network hosts for a package"),
)
}
pub fn diagnostic_api() -> ParentHandler<DiagnosticContext> {
ParentHandler::new()
.subcommand(
"git-info",
from_fn(|_: DiagnosticContext| version::git_info())
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the githash of StartOS CLI"),
)
.subcommand(
"echo",
from_fn(echo::<DiagnosticContext>)
.with_about("Echo a message")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: DiagnosticContext| Ok::<_, Error>(ApiState::Error))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_call_remote::<CliContext>(),
)
.subcommand(
"diagnostic",
diagnostic::diagnostic::<DiagnosticContext>()
.with_about("Diagnostic commands i.e. logs, restart, rebuild"),
)
}
pub fn init_api() -> ParentHandler<InitContext> {
ParentHandler::new()
.subcommand(
"git-info",
from_fn(|_: InitContext| version::git_info())
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the githash of StartOS CLI"),
)
.subcommand(
"echo",
from_fn(echo::<InitContext>)
.with_about("Echo a message")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: InitContext| Ok::<_, Error>(ApiState::Initializing))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_call_remote::<CliContext>(),
)
.subcommand(
"init",
init::init_api::<InitContext>()
.with_about("Commands to get logs or initialization progress"),
)
}
pub fn setup_api() -> ParentHandler<SetupContext> {
ParentHandler::new()
.subcommand(
"git-info",
from_fn(|_: SetupContext| version::git_info())
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the githash of StartOS CLI"),
)
.subcommand(
"echo",
from_fn(echo::<SetupContext>)
.with_about("Echo a message")
.with_call_remote::<CliContext>(),
)
.subcommand("setup", setup::setup::<SetupContext>())
}
pub fn install_api() -> ParentHandler<InstallContext> {
ParentHandler::new()
.subcommand(
"git-info",
from_fn(|_: InstallContext| version::git_info())
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the githash of StartOS CLI"),
)
.subcommand(
"echo",
from_fn(echo::<InstallContext>)
.with_about("Echo a message")
.with_call_remote::<CliContext>(),
)
.subcommand(
"install",
os_install::install::<InstallContext>()
.with_about("Commands to list disk info, install StartOS, and reboot"),
)
}
pub fn expanded_api() -> ParentHandler<CliContext> {
main_api()
.subcommand(
"init",
from_fn_async(developer::init)
.no_display()
.with_about("Create developer key if it doesn't exist"),
)
.subcommand(
"pubkey",
from_fn_blocking(developer::pubkey)
.with_about("Get public key for developer private key"),
)
.subcommand(
"diagnostic",
diagnostic::diagnostic::<CliContext>()
.with_about("Commands to display logs, restart the server, etc"),
)
.subcommand("setup", setup::setup::<CliContext>())
.subcommand(
"install",
os_install::install::<CliContext>()
.with_about("Commands to list disk info, install StartOS, and reboot"),
)
.subcommand(
"registry",
registry::registry_api::<CliContext>().with_about("Commands related to the registry"),
)
}

View File

@@ -13,9 +13,9 @@ use chrono::Utc;
use color_eyre::eyre::eyre;
use digest::Digest;
use helpers::const_true;
use http::HeaderValue;
use http::header::{COOKIE, USER_AGENT};
use imbl_value::{InternedString, json};
use http::HeaderValue;
use imbl_value::{json, InternedString};
use rand::random;
use rpc_toolkit::yajrc::INTERNAL_ERROR;
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
@@ -25,18 +25,15 @@ use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tokio::sync::Mutex;
use crate::auth::{Sessions, check_password, write_shadow};
use crate::auth::{check_password, write_shadow, Sessions};
use crate::context::RpcContext;
use crate::db::model::Database;
use crate::middleware::signature::{SignatureAuth, SignatureAuthContext};
use crate::prelude::*;
use crate::rpc_continuations::OpenAuthedContinuations;
use crate::sign::AnyVerifyingKey;
use crate::util::Invoke;
use crate::util::io::{create_file_mod, read_file_to_string};
use crate::util::iter::TransposeResultIterExt;
use crate::util::serde::BASE64;
use crate::util::sync::SyncMutex;
use crate::util::Invoke;
pub trait AuthContext: SignatureAuthContext {
const LOCAL_AUTH_COOKIE_PATH: &str;
@@ -66,65 +63,6 @@ pub trait AuthContext: SignatureAuthContext {
}
}
impl SignatureAuthContext for RpcContext {
type Database = Database;
type AdditionalMetadata = ();
type CheckPubkeyRes = ();
fn db(&self) -> &TypedPatchDb<Self::Database> {
&self.db
}
async fn sig_context(
&self,
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
let peek = self.db.peek().await;
self.account
.read()
.await
.hostnames()
.into_iter()
.map(Ok)
.chain(
peek.as_public()
.as_server_info()
.as_network()
.as_host()
.as_public_domains()
.keys()
.map(|k| k.into_iter())
.transpose(),
)
.chain(
peek.as_public()
.as_server_info()
.as_network()
.as_host()
.as_private_domains()
.de()
.map(|k| k.into_iter())
.transpose(),
)
.collect::<Vec<_>>()
}
fn check_pubkey(
db: &Model<Self::Database>,
pubkey: Option<&AnyVerifyingKey>,
_: Self::AdditionalMetadata,
) -> Result<Self::CheckPubkeyRes, Error> {
if let Some(pubkey) = pubkey {
if db.as_private().as_auth_pubkeys().de()?.contains(pubkey) {
return Ok(());
}
}
Err(Error::new(
eyre!("Developer Key is not authorized"),
ErrorKind::IncorrectPassword,
))
}
async fn post_auth_hook(&self, _: Self::CheckPubkeyRes, _: &RpcRequest) -> Result<(), Error> {
Ok(())
}
}
impl AuthContext for RpcContext {
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/rpc.authcookie";
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:startos";
@@ -439,7 +377,7 @@ impl<C: AuthContext> Middleware<C> for Auth {
));
}
if let Some(user_agent) = self.user_agent.as_ref().and_then(|h| h.to_str().ok()) {
request.params["__auth_userAgent"] =
request.params["__Auth_userAgent"] =
Value::String(Arc::new(user_agent.to_owned()))
// TODO: will this panic?
}
@@ -458,7 +396,7 @@ impl<C: AuthContext> Middleware<C> for Auth {
{
match HasValidSession::from_header(self.cookie.as_ref(), context).await? {
HasValidSession(SessionType::Session(s)) if metadata.get_session => {
request.params["__auth_session"] =
request.params["__Auth_session"] =
Value::String(Arc::new(s.hashed().deref().to_owned()));
}
_ => (),

View File

@@ -0,0 +1,55 @@
use std::net::SocketAddr;
use axum::extract::Request;
use axum::response::Response;
use imbl_value::json;
use rpc_toolkit::Middleware;
use serde::Deserialize;
#[derive(Clone, Default)]
pub struct ConnectInfo {
peer_addr: Option<SocketAddr>,
local_addr: Option<SocketAddr>,
}
impl ConnectInfo {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Deserialize)]
pub struct Metadata {
get_connect_info: bool,
}
impl<Context: Send + Sync + 'static> Middleware<Context> for ConnectInfo {
type Metadata = Metadata;
async fn process_http_request(
&mut self,
_: &Context,
request: &mut Request,
) -> Result<(), Response> {
if let Some(axum::extract::ConnectInfo((peer, local))) = request.extensions().get().cloned()
{
self.peer_addr = Some(peer);
self.local_addr = Some(local);
}
Ok(())
}
async fn process_rpc_request(
&mut self,
_: &Context,
metadata: Self::Metadata,
request: &mut rpc_toolkit::RpcRequest,
) -> Result<(), rpc_toolkit::RpcResponse> {
if metadata.get_connect_info {
if let Some(peer_addr) = self.peer_addr {
request.params["__ConnectInfo_peer_addr"] = json!(peer_addr);
}
if let Some(local_addr) = self.local_addr {
request.params["__ConnectInfo_local_addr"] = json!(local_addr);
}
}
Ok(())
}
}

View File

@@ -1,4 +1,5 @@
pub mod auth;
pub mod connect_info;
pub mod cors;
pub mod db;
pub mod signature;

View File

@@ -5,21 +5,26 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use axum::body::Body;
use axum::extract::Request;
use http::HeaderValue;
use http::{HeaderMap, HeaderValue};
use reqwest::Client;
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{Context, Middleware, RpcRequest, RpcResponse};
use serde::Deserialize;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use tokio::sync::Mutex;
use url::Url;
use crate::context::CliContext;
use crate::context::{CliContext, RpcContext};
use crate::db::model::Database;
use crate::prelude::*;
use crate::sign::commitment::Commitment;
use crate::sign::commitment::request::RequestCommitment;
use crate::sign::commitment::Commitment;
use crate::sign::{AnySignature, AnySigningKey, AnyVerifyingKey, SignatureScheme};
use crate::util::iter::TransposeResultIterExt;
use crate::util::serde::Base64;
pub const AUTH_SIG_HEADER: &str = "X-StartOS-Auth-Sig";
pub trait SignatureAuthContext: Context {
type Database: HasModel<Model = Model<Self::Database>> + Send + Sync;
type AdditionalMetadata: DeserializeOwned + Send;
@@ -28,7 +33,7 @@ pub trait SignatureAuthContext: Context {
fn sig_context(
&self,
) -> impl Future<Output = impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send>
+ Send;
+ Send;
fn check_pubkey(
db: &Model<Self::Database>,
pubkey: Option<&AnyVerifyingKey>,
@@ -41,7 +46,82 @@ pub trait SignatureAuthContext: Context {
) -> impl Future<Output = Result<(), Error>> + Send;
}
pub const AUTH_SIG_HEADER: &str = "X-StartOS-Auth-Sig";
impl SignatureAuthContext for RpcContext {
type Database = Database;
type AdditionalMetadata = ();
type CheckPubkeyRes = ();
fn db(&self) -> &TypedPatchDb<Self::Database> {
&self.db
}
async fn sig_context(
&self,
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
let peek = self.db.peek().await;
self.account.peek(|a| {
a.hostnames()
.into_iter()
.map(Ok)
.chain(
peek.as_public()
.as_server_info()
.as_network()
.as_host()
.as_public_domains()
.keys()
.map(|k| k.into_iter())
.transpose(),
)
.chain(
peek.as_public()
.as_server_info()
.as_network()
.as_host()
.as_private_domains()
.de()
.map(|k| k.into_iter())
.transpose(),
)
.collect::<Vec<_>>()
})
}
fn check_pubkey(
db: &Model<Self::Database>,
pubkey: Option<&AnyVerifyingKey>,
_: Self::AdditionalMetadata,
) -> Result<Self::CheckPubkeyRes, Error> {
if let Some(pubkey) = pubkey {
if db.as_private().as_auth_pubkeys().de()?.contains(pubkey) {
return Ok(());
}
}
Err(Error::new(
eyre!("Developer Key is not authorized"),
ErrorKind::IncorrectPassword,
))
}
async fn post_auth_hook(&self, _: Self::CheckPubkeyRes, _: &RpcRequest) -> Result<(), Error> {
Ok(())
}
}
pub trait SigningContext {
fn signing_key(&self) -> Result<AnySigningKey, Error>;
}
impl SigningContext for CliContext {
fn signing_key(&self) -> Result<AnySigningKey, Error> {
Ok(AnySigningKey::Ed25519(self.developer_key()?.clone()))
}
}
impl SigningContext for RpcContext {
fn signing_key(&self) -> Result<AnySigningKey, Error> {
Ok(AnySigningKey::Ed25519(
self.account.peek(|a| a.developer_key.clone()),
))
}
}
#[derive(Deserialize)]
pub struct Metadata<Additional> {
@@ -203,7 +283,7 @@ impl<C: SignatureAuthContext> Middleware<C> for SignatureAuth {
let signer = self.signer.take().transpose()?;
if metadata.get_signer {
if let Some(signer) = &signer {
request.params["__auth_signer"] = to_value(signer)?;
request.params["__Auth_signer"] = to_value(signer)?;
}
}
let db = context.db().peek().await;
@@ -216,17 +296,18 @@ impl<C: SignatureAuthContext> Middleware<C> for SignatureAuth {
}
}
pub async fn call_remote(
ctx: &CliContext,
pub async fn call_remote<Ctx: SigningContext + AsRef<Client>>(
ctx: &Ctx,
url: Url,
sig_context: &str,
headers: HeaderMap,
sig_context: Option<&str>,
method: &str,
params: Value,
) -> Result<Value, RpcError> {
use reqwest::Method;
use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE};
use rpc_toolkit::RpcResponse;
use reqwest::Method;
use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest};
use rpc_toolkit::RpcResponse;
let rpc_req = RpcRequest {
id: Some(Id::Number(0.into())),
@@ -235,16 +316,16 @@ pub async fn call_remote(
};
let body = serde_json::to_vec(&rpc_req)?;
let mut req = ctx
.client
.as_ref()
.request(Method::POST, url)
.header(CONTENT_TYPE, "application/json")
.header(ACCEPT, "application/json")
.header(CONTENT_LENGTH, body.len());
if let Ok(key) = ctx.developer_key() {
.header(CONTENT_LENGTH, body.len())
.headers(headers);
if let (Some(sig_ctx), Ok(key)) = (sig_context, ctx.signing_key()) {
req = req.header(
AUTH_SIG_HEADER,
SignatureHeader::sign(&AnySigningKey::Ed25519(key.clone()), &body, sig_context)?
.to_header(),
SignatureHeader::sign(&key, &body, sig_ctx)?.to_header(),
);
}
let res = req.body(body).send().await?;

View File

@@ -244,6 +244,8 @@ mod active_connection {
default_service = "org.freedesktop.NetworkManager"
)]
trait ConnectionSettings {
fn delete(&self) -> Result<(), Error>;
fn get_settings(&self) -> Result<HashMap<String, HashMap<String, OwnedValue>>, Error>;
fn update2(
@@ -1095,7 +1097,7 @@ impl NetworkInterfaceController {
.ip_info
.peek(|ifaces| ifaces.get(interface).map(|i| i.ip_info.is_some()))
else {
return Ok(());
return self.forget(interface).await;
};
if has_ip_info {
@@ -1115,7 +1117,21 @@ impl NetworkInterfaceController {
let device_proxy = DeviceProxy::new(&connection, device).await?;
device_proxy.delete().await?;
let ac = device_proxy.active_connection().await?;
if &*ac == "/" {
return Err(Error::new(
eyre!("Cannot delete device without active connection"),
ErrorKind::InvalidRequest,
));
}
let ac_proxy = active_connection::ActiveConnectionProxy::new(&connection, ac).await?;
let settings =
ConnectionSettingsProxy::new(&connection, ac_proxy.connection().await?).await?;
settings.delete().await?;
ip_info
.wait_for(|ifaces| ifaces.get(interface).map_or(true, |i| i.ip_info.is_none()))

View File

@@ -6,11 +6,11 @@ use std::sync::Arc;
use std::time::UNIX_EPOCH;
use async_compression::tokio::bufread::GzipEncoder;
use axum::Router;
use axum::body::Body;
use axum::extract::{self as x, Request};
use axum::response::{Redirect, Response};
use axum::routing::{any, get};
use axum::Router;
use base64::display::Base64Display;
use digest::Digest;
use futures::future::ready;
@@ -33,20 +33,20 @@ use url::Url;
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::hostname::Hostname;
use crate::main_api;
use crate::middleware::auth::{Auth, HasValidSession};
use crate::middleware::cors::Cors;
use crate::middleware::db::SyncDb;
use crate::prelude::*;
use crate::rpc_continuations::{Guid, RpcContinuations};
use crate::s9pk::S9pk;
use crate::s9pk::merkle_archive::source::FileSource;
use crate::s9pk::merkle_archive::source::http::HttpSource;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::merkle_archive::source::FileSource;
use crate::s9pk::S9pk;
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::util::io::open_file;
use crate::util::net::SyncBody;
use crate::util::serde::BASE64;
use crate::{diagnostic_api, init_api, install_api, main_api, setup_api};
const NOT_FOUND: &[u8] = b"Not Found";
const METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed";
@@ -61,20 +61,84 @@ const EMBEDDED_UIS: Dir<'_> =
#[cfg(not(all(feature = "startd", not(feature = "test"))))]
const EMBEDDED_UIS: Dir<'_> = Dir::new("", &[]);
#[derive(Clone)]
pub enum UiMode {
Setup,
Install,
Main,
pub trait UiContext: Context + AsRef<RpcContinuations> + Clone + Sized {
fn path(path: &str) -> PathBuf;
fn middleware(server: Server<Self>) -> HttpServer<Self>;
fn extend_router(self, router: Router) -> Router {
router
}
}
impl UiMode {
fn path(&self, path: &str) -> PathBuf {
match self {
Self::Setup => Path::new("setup-wizard").join(path),
Self::Install => Path::new("install-wizard").join(path),
Self::Main => Path::new("ui").join(path),
}
impl UiContext for RpcContext {
fn path(path: &str) -> PathBuf {
Path::new("ui").join(path)
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server
.middleware(Cors::new())
.middleware(Auth::new())
.middleware(SyncDb::new())
}
fn extend_router(self, router: Router) -> Router {
router
.route("/proxy/{url}", {
let ctx = self.clone();
any(move |x::Path(url): x::Path<String>, request: Request| {
let ctx = ctx.clone();
async move {
proxy_request(ctx, request, url)
.await
.unwrap_or_else(server_error)
}
})
})
.nest("/s9pk", s9pk_router(self.clone()))
.route(
"/static/local-root-ca.crt",
get(move || {
let ctx = self.clone();
async move {
ctx.account
.peek(|account| cert_send(&account.root_ca_cert, &account.hostname))
}
}),
)
}
}
impl UiContext for InitContext {
fn path(path: &str) -> PathBuf {
Path::new("ui").join(path)
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server.middleware(Cors::new())
}
}
impl UiContext for DiagnosticContext {
fn path(path: &str) -> PathBuf {
Path::new("ui").join(path)
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server.middleware(Cors::new())
}
}
impl UiContext for SetupContext {
fn path(path: &str) -> PathBuf {
Path::new("setup-wizard").join(path)
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server.middleware(Cors::new())
}
}
impl UiContext for InstallContext {
fn path(path: &str) -> PathBuf {
Path::new("install-wizard").join(path)
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server.middleware(Cors::new())
}
}
@@ -111,11 +175,11 @@ pub fn rpc_router<C: Context + Clone + AsRef<RpcContinuations>>(
)
}
fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> {
fn serve_ui<C: UiContext>(req: Request) -> Result<Response, Error> {
let (request_parts, _body) = req.into_parts();
match &request_parts.method {
&Method::GET | &Method::HEAD => {
let uri_path = ui_mode.path(
let uri_path = C::path(
request_parts
.uri
.path()
@@ -125,7 +189,7 @@ fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> {
let file = EMBEDDED_UIS
.get_file(&*uri_path)
.or_else(|| EMBEDDED_UIS.get_file(&*ui_mode.path("index.html")));
.or_else(|| EMBEDDED_UIS.get_file(&*C::path("index.html")));
if let Some(file) = file {
FileData::from_embedded(&request_parts, file)?.into_response(&request_parts)
@@ -137,79 +201,15 @@ fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> {
}
}
pub fn setup_ui_router(ctx: SetupContext) -> Router {
rpc_router(
ctx.clone(),
Server::new(move || ready(Ok(ctx.clone())), setup_api()).middleware(Cors::new()),
)
.fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Setup).unwrap_or_else(server_error)
}))
}
pub fn diagnostic_ui_router(ctx: DiagnosticContext) -> Router {
rpc_router(
ctx.clone(),
Server::new(move || ready(Ok(ctx.clone())), diagnostic_api()).middleware(Cors::new()),
)
.fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Main).unwrap_or_else(server_error)
}))
}
pub fn install_ui_router(ctx: InstallContext) -> Router {
rpc_router(
ctx.clone(),
Server::new(move || ready(Ok(ctx.clone())), install_api()).middleware(Cors::new()),
)
.fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Install).unwrap_or_else(server_error)
}))
}
pub fn init_ui_router(ctx: InitContext) -> Router {
rpc_router(
ctx.clone(),
Server::new(move || ready(Ok(ctx.clone())), init_api()).middleware(Cors::new()),
)
.fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Main).unwrap_or_else(server_error)
}))
}
pub fn main_ui_router(ctx: RpcContext) -> Router {
rpc_router(ctx.clone(), {
let ctx = ctx.clone();
Server::new(move || ready(Ok(ctx.clone())), main_api::<RpcContext>())
.middleware(Cors::new())
.middleware(Auth::new())
.middleware(SyncDb::new())
})
.route("/proxy/{url}", {
let ctx = ctx.clone();
any(move |x::Path(url): x::Path<String>, request: Request| {
let ctx = ctx.clone();
async move {
proxy_request(ctx, request, url)
.await
.unwrap_or_else(server_error)
}
})
})
.nest("/s9pk", s9pk_router(ctx.clone()))
.route(
"/static/local-root-ca.crt",
get(move || {
let ctx = ctx.clone();
async move {
let account = ctx.account.read().await;
cert_send(&account.root_ca_cert, &account.hostname)
}
}),
)
.fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Main).unwrap_or_else(server_error)
}))
pub fn ui_router<C: UiContext>(ctx: C) -> Router {
ctx.clone()
.extend_router(rpc_router(
ctx.clone(),
C::middleware(Server::new(move || ready(Ok(ctx.clone())), main_api())),
))
.fallback(any(|request: Request| async move {
serve_ui::<C>(request).unwrap_or_else(server_error)
}))
}
pub fn refresher() -> Router {

View File

@@ -191,7 +191,8 @@ impl Model<OnionStore> {
Ok(key)
}
pub fn insert_key(&mut self, key: &TorSecretKey) -> Result<(), Error> {
self.insert(&key.onion_address(), &key)
self.insert(&key.onion_address(), &key)?;
Ok(())
}
pub fn get_key(&self, address: &OnionAddress) -> Result<TorSecretKey, Error> {
self.as_idx(address)

View File

@@ -5,6 +5,7 @@ use std::sync::Arc;
use std::task::Poll;
use std::time::Duration;
use axum::extract::ConnectInfo;
use axum::Router;
use futures::future::Either;
use futures::FutureExt;
@@ -13,19 +14,17 @@ use hyper_util::rt::{TokioIo, TokioTimer};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::oneshot;
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::net::gateway::{
lookup_info_by_addr, NetworkInterfaceListener, SelfContainedNetworkInterfaceListener,
};
use crate::net::static_server::{
diagnostic_ui_router, init_ui_router, install_ui_router, main_ui_router, redirecter, refresher,
setup_ui_router,
};
use crate::net::static_server::{redirecter, refresher, ui_router, UiContext};
use crate::prelude::*;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::sync::{SyncRwLock, Watch};
pub struct Accepted {
pub peer_addr: SocketAddr,
pub local_addr: SocketAddr,
pub https_redirect: bool,
pub stream: TcpStream,
}
@@ -37,8 +36,10 @@ pub trait Accept {
impl Accept for Vec<TcpListener> {
fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<Accepted, Error>> {
for listener in &*self {
if let Poll::Ready((stream, _)) = listener.poll_accept(cx)? {
if let Poll::Ready((stream, peer_addr)) = listener.poll_accept(cx)? {
return Poll::Ready(Ok(Accepted {
local_addr: listener.local_addr()?,
peer_addr,
https_redirect: false,
stream,
}));
@@ -55,6 +56,8 @@ impl Accept for NetworkInterfaceListener {
.ip_info
.peek(|i| lookup_info_by_addr(i, a.bind).map_or(true, |(_, i)| i.public()));
Accepted {
peer_addr: a.peer,
local_addr: a.bind,
https_redirect: public,
stream: a.stream,
}
@@ -187,7 +190,12 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
}
}
struct SwappableRouter(Watch<Option<Router>>, bool);
struct SwappableRouter {
router: Watch<Option<Router>>,
redirect: bool,
local_addr: SocketAddr,
peer_addr: SocketAddr,
}
impl hyper::service::Service<hyper::Request<hyper::body::Incoming>> for SwappableRouter {
type Response = <Router as tower_service::Service<
hyper::Request<hyper::body::Incoming>,
@@ -199,13 +207,16 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
hyper::Request<hyper::body::Incoming>,
>>::Future;
fn call(&self, req: hyper::Request<hyper::body::Incoming>) -> Self::Future {
fn call(&self, mut req: hyper::Request<hyper::body::Incoming>) -> Self::Future {
use tower_service::Service;
if self.1 {
req.extensions_mut()
.insert(ConnectInfo((self.peer_addr, self.local_addr)));
if self.redirect {
redirecter().call(req)
} else {
let router = self.0.read();
let router = self.router.read();
if let Some(mut router) = router {
router.call(req)
} else {
@@ -239,15 +250,18 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
for _ in 0..5 {
if let Err(e) = async {
let accepted = acceptor.accept().await?;
let src = accepted.stream.peer_addr().ok();
queue.add_job(
graceful.watch(
server
.serve_connection_with_upgrades(
TokioIo::new(accepted.stream),
SwappableRouter(
service.clone(),
accepted.https_redirect,
),
SwappableRouter {
router: service.clone(),
redirect: accepted.https_redirect,
peer_addr: accepted.peer_addr,
local_addr: accepted.local_addr,
},
)
.into_owned(),
),
@@ -303,23 +317,7 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
self.router.send(Some(router))
}
pub fn serve_main(&mut self, ctx: RpcContext) {
self.serve_router(main_ui_router(ctx))
}
pub fn serve_setup(&mut self, ctx: SetupContext) {
self.serve_router(setup_ui_router(ctx))
}
pub fn serve_diagnostic(&mut self, ctx: DiagnosticContext) {
self.serve_router(diagnostic_ui_router(ctx))
}
pub fn serve_install(&mut self, ctx: InstallContext) {
self.serve_router(install_ui_router(ctx))
}
pub fn serve_init(&mut self, ctx: InitContext) {
self.serve_router(init_ui_router(ctx))
pub fn serve_ui_for<C: UiContext>(&mut self, ctx: C) {
self.serve_router(ui_router(ctx))
}
}

View File

@@ -3,12 +3,12 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use clap::Parser;
use clap::builder::TypedValueParser;
use clap::Parser;
use isocountry::CountryCode;
use lazy_static::lazy_static;
use regex::Regex;
use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async};
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use tokio::sync::RwLock;
@@ -16,11 +16,11 @@ use tracing::instrument;
use ts_rs::TS;
use crate::context::{CliContext, RpcContext};
use crate::db::model::Database;
use crate::db::model::public::WifiInfo;
use crate::db::model::Database;
use crate::prelude::*;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
use crate::util::Invoke;
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
use crate::{Error, ErrorKind};
type WifiManager = Arc<RwLock<Option<WpaCli>>>;
@@ -1017,6 +1017,31 @@ pub async fn synchronize_network_manager<P: AsRef<Path>>(
.await?;
}
Command::new("ip")
.arg("rule")
.arg("add")
.arg("pref")
.arg("1000")
.arg("from")
.arg("all")
.arg("lookup")
.arg("main")
.invoke(ErrorKind::Network)
.await
.log_err();
Command::new("ip")
.arg("rule")
.arg("add")
.arg("pref")
.arg("1100")
.arg("from")
.arg("all")
.arg("lookup")
.arg("default")
.invoke(ErrorKind::Network)
.await
.log_err();
Command::new("systemctl")
.arg("restart")
.arg("NetworkManager")

View File

@@ -463,7 +463,8 @@ pub fn notify<T: NotificationType>(
data,
seen: false,
},
)
)?;
Ok(())
}
#[test]

View File

@@ -5,6 +5,7 @@ use std::sync::Arc;
use chrono::Utc;
use clap::Parser;
use http::HeaderMap;
use imbl_value::InternedString;
use patch_db::PatchDb;
use reqwest::{Client, Proxy};
@@ -17,13 +18,13 @@ use tracing::instrument;
use ts_rs::TS;
use url::Url;
use crate::context::config::{CONFIG_PATH, ContextConfig};
use crate::context::config::{ContextConfig, CONFIG_PATH};
use crate::context::{CliContext, RpcContext};
use crate::middleware::signature::SignatureAuthContext;
use crate::prelude::*;
use crate::registry::RegistryDatabase;
use crate::registry::device_info::{DEVICE_INFO_HEADER, DeviceInfo};
use crate::registry::device_info::{DeviceInfo, DEVICE_INFO_HEADER};
use crate::registry::signer::SignerInfo;
use crate::registry::RegistryDatabase;
use crate::rpc_continuations::RpcContinuations;
use crate::sign::AnyVerifyingKey;
use crate::util::io::append_file;
@@ -183,10 +184,17 @@ impl CallRemote<RegistryContext> for CliContext {
let sig_context = self
.registry_hostname
.clone()
.or(url.host().as_ref().map(InternedString::from_display))
.or_not_found("registry hostname")?;
.or_else(|| url.host().as_ref().map(InternedString::from_display));
crate::middleware::signature::call_remote(self, url, &sig_context, method, params).await
crate::middleware::signature::call_remote(
self,
url,
HeaderMap::new(),
sig_context.as_deref(),
method,
params,
)
.await
}
}
@@ -197,59 +205,24 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
params: Value,
RegistryUrlParams { registry }: RegistryUrlParams,
) -> Result<Value, RpcError> {
use reqwest::Method;
use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE};
use rpc_toolkit::RpcResponse;
use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest};
let mut headers = HeaderMap::new();
headers.insert(
DEVICE_INFO_HEADER,
DeviceInfo::load(self).await?.to_header_value(),
);
let url = registry.join("rpc/v0")?;
method = method.strip_prefix("registry.").unwrap_or(method);
let sig_context = registry.host_str().map(InternedString::from);
let rpc_req = RpcRequest {
id: Some(Id::Number(0.into())),
method: GenericRpcMethod::<_, _, Value>::new(method),
crate::middleware::signature::call_remote(
self,
registry,
headers,
sig_context.as_deref(),
method,
params,
};
let body = serde_json::to_vec(&rpc_req)?;
let res = self
.client
.request(Method::POST, url)
.header(CONTENT_TYPE, "application/json")
.header(ACCEPT, "application/json")
.header(CONTENT_LENGTH, body.len())
.header(
DEVICE_INFO_HEADER,
DeviceInfo::load(self).await?.to_header_value(),
)
.body(body)
.send()
.await?;
if !res.status().is_success() {
let status = res.status();
let txt = res.text().await?;
let mut res = Err(Error::new(
eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())),
ErrorKind::Network,
));
if !txt.is_empty() {
res = res.with_ctx(|_| (ErrorKind::Network, txt));
}
return res.map_err(From::from);
}
match res
.headers()
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
{
Some("application/json") => {
serde_json::from_slice::<RpcResponse>(&*res.bytes().await?)
.with_kind(ErrorKind::Deserialization)?
.result
}
_ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()),
}
)
.await
}
}

View File

@@ -15,8 +15,8 @@ use url::Url;
use crate::context::RpcContext;
use crate::prelude::*;
use crate::registry::context::RegistryContext;
use crate::util::VersionString;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
use crate::util::VersionString;
use crate::version::VersionT;
pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info";
@@ -175,7 +175,7 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
async move {
if metadata.get_device_info {
if let Some(device_info) = &self.device_info {
request.params["__device_info"] =
request.params["__DeviceInfo_device_info"] =
to_value(&DeviceInfo::from_header_value(device_info)?)?;
}
}

View File

@@ -83,7 +83,7 @@ pub struct AddAssetParams {
pub platform: InternedString,
#[ts(type = "string")]
pub url: Url,
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
#[ts(skip)]
pub signer: AnyVerifyingKey,
pub signature: AnySignature,
@@ -289,7 +289,7 @@ pub struct RemoveAssetParams {
pub version: Version,
#[ts(type = "string")]
pub platform: InternedString,
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
#[ts(skip)]
pub signer: AnyVerifyingKey,
}

View File

@@ -56,7 +56,7 @@ pub struct SignAssetParams {
#[ts(type = "string")]
platform: InternedString,
#[ts(skip)]
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
signer: AnyVerifyingKey,
signature: AnySignature,
}

View File

@@ -68,7 +68,7 @@ pub struct AddVersionParams {
pub source_version: VersionRange,
#[arg(skip)]
#[ts(skip)]
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
pub signer: Option<AnyVerifyingKey>,
}
@@ -146,7 +146,7 @@ pub struct GetOsVersionParams {
platform: Option<InternedString>,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__device_info")]
#[serde(rename = "__DeviceInfo_device_info")]
pub device_info: Option<DeviceInfo>,
}

View File

@@ -31,7 +31,7 @@ pub struct AddPackageParams {
#[ts(type = "string")]
pub url: Url,
#[ts(skip)]
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
pub uploader: AnyVerifyingKey,
pub commitment: MerkleArchiveCommitment,
pub signature: AnySignature,
@@ -169,7 +169,7 @@ pub struct RemovePackageParams {
pub version: VersionString,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
pub signer: Option<AnyVerifyingKey>,
}

View File

@@ -12,8 +12,8 @@ use crate::prelude::*;
use crate::registry::context::RegistryContext;
use crate::registry::device_info::DeviceInfo;
use crate::registry::package::index::{PackageIndex, PackageVersionInfo};
use crate::util::serde::{display_serializable, WithIoFormat};
use crate::util::VersionString;
use crate::util::serde::{WithIoFormat, display_serializable};
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum,
@@ -51,7 +51,7 @@ pub struct GetPackageParams {
pub source_version: Option<VersionString>,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__device_info")]
#[serde(rename = "__DeviceInfo_device_info")]
pub device_info: Option<DeviceInfo>,
#[serde(default)]
#[arg(default_value = "none")]

View File

@@ -768,7 +768,7 @@ pub struct AttachParams {
pub stderr_tty: bool,
pub pty_size: Option<TermSize>,
#[ts(skip)]
#[serde(rename = "__auth_session")]
#[serde(rename = "__Auth_session")]
session: Option<InternedString>,
#[ts(type = "string | null")]
subcontainer: Option<InternedString>,

View File

@@ -1,146 +1,179 @@
use std::sync::Arc;
use std::time::Duration;
use futures::future::{BoxFuture, Either};
use futures::FutureExt;
use imbl::vector;
use super::ServiceActorSeed;
use super::start_stop::StartStop;
use super::ServiceActorSeed;
use crate::prelude::*;
use crate::service::SYNC_RETRY_COOLDOWN_SECONDS;
use crate::service::persistent_container::ServiceStateKinds;
use crate::service::transition::TransitionKind;
use crate::service::SYNC_RETRY_COOLDOWN_SECONDS;
use crate::status::MainStatus;
use crate::util::actor::Actor;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::Actor;
#[derive(Clone)]
pub(super) struct ServiceActor(pub(super) Arc<ServiceActorSeed>);
enum ServiceActorLoopNext {
Wait,
DontWait,
}
impl Actor for ServiceActor {
fn init(&mut self, jobs: &BackgroundJobQueue) {
let seed = self.0.clone();
let mut current = seed.persistent_container.state.subscribe();
jobs.add_job(async move {
let _ = current.wait_for(|s| s.rt_initialized).await;
let mut start_stop_task: Option<Either<_, _>> = None;
loop {
match service_actor_loop(&current, &seed).await {
ServiceActorLoopNext::Wait => tokio::select! {
_ = current.changed() => (),
},
ServiceActorLoopNext::DontWait => (),
}
let wait = match service_actor_loop(&current, &seed, &mut start_stop_task).await {
Ok(()) => Either::Right(current.changed().then(|res| async move {
match res {
Ok(()) => (),
Err(_) => futures::future::pending().await,
}
})),
Err(e) => {
tracing::error!("error synchronizing state of service: {e}");
tracing::debug!("{e:?}");
seed.synchronized.notify_waiters();
tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS);
Either::Left(tokio::time::sleep(Duration::from_secs(
SYNC_RETRY_COOLDOWN_SECONDS,
)))
}
};
tokio::pin!(wait);
let start_stop_handler = async {
match &mut start_stop_task {
Some(task) => {
let err = task.await.log_err().is_none(); // TODO: ideally this error should be sent to service logs
start_stop_task.take();
if err {
tokio::time::sleep(Duration::from_secs(
SYNC_RETRY_COOLDOWN_SECONDS,
))
.await;
}
}
_ => futures::future::pending().await,
}
};
tokio::pin!(start_stop_handler);
futures::future::select(wait, start_stop_handler).await;
}
});
}
}
async fn service_actor_loop(
async fn service_actor_loop<'a>(
current: &tokio::sync::watch::Receiver<super::persistent_container::ServiceState>,
seed: &Arc<ServiceActorSeed>,
) -> ServiceActorLoopNext {
seed: &'a Arc<ServiceActorSeed>,
start_stop_task: &mut Option<
Either<BoxFuture<'a, Result<(), Error>>, BoxFuture<'a, Result<(), Error>>>,
>,
) -> Result<(), Error> {
let id = &seed.id;
let kinds = current.borrow().kinds();
if let Err(e) = async {
let major_changes_state = seed
.ctx
.db
.mutate(|d| {
if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) {
let previous = i.as_status().de()?;
let main_status = match &kinds {
ServiceStateKinds {
transition_state: Some(TransitionKind::Restarting),
..
} => MainStatus::Restarting,
ServiceStateKinds {
transition_state: Some(TransitionKind::BackingUp),
..
} => previous.backing_up(),
ServiceStateKinds {
running_status: Some(status),
desired_state: StartStop::Start,
..
} => MainStatus::Running {
started: status.started,
health: previous.health().cloned().unwrap_or_default(),
},
ServiceStateKinds {
running_status: None,
desired_state: StartStop::Start,
..
} => MainStatus::Starting {
health: previous.health().cloned().unwrap_or_default(),
},
ServiceStateKinds {
running_status: Some(_),
desired_state: StartStop::Stop,
..
} => MainStatus::Stopping,
ServiceStateKinds {
running_status: None,
desired_state: StartStop::Stop,
..
} => MainStatus::Stopped,
};
i.as_status_mut().ser(&main_status)?;
return Ok(previous
.major_changes(&main_status)
.then_some((previous, main_status)));
}
Ok(None)
})
.await
.result?;
if let Some((previous, new_state)) = major_changes_state {
if let Some(callbacks) = seed.ctx.callbacks.get_status(id) {
callbacks
.call(vector![to_value(&previous)?, to_value(&new_state)?])
.await?;
let major_changes_state = seed
.ctx
.db
.mutate(|d| {
if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) {
let previous = i.as_status().de()?;
let main_status = match &kinds {
ServiceStateKinds {
transition_state: Some(TransitionKind::Restarting),
..
} => MainStatus::Restarting,
ServiceStateKinds {
transition_state: Some(TransitionKind::BackingUp),
..
} => previous.backing_up(),
ServiceStateKinds {
running_status: Some(status),
desired_state: StartStop::Start,
..
} => MainStatus::Running {
started: status.started,
health: previous.health().cloned().unwrap_or_default(),
},
ServiceStateKinds {
running_status: None,
desired_state: StartStop::Start,
..
} => MainStatus::Starting {
health: previous.health().cloned().unwrap_or_default(),
},
ServiceStateKinds {
running_status: Some(_),
desired_state: StartStop::Stop,
..
} => MainStatus::Stopping,
ServiceStateKinds {
running_status: None,
desired_state: StartStop::Stop,
..
} => MainStatus::Stopped,
};
i.as_status_mut().ser(&main_status)?;
return Ok(previous
.major_changes(&main_status)
.then_some((previous, main_status)));
}
Ok(None)
})
.await
.result?;
if let Some((previous, new_state)) = major_changes_state {
if let Some(callbacks) = seed.ctx.callbacks.get_status(id) {
callbacks
.call(vector![to_value(&previous)?, to_value(&new_state)?])
.await?;
}
seed.synchronized.notify_waiters();
match kinds {
ServiceStateKinds {
running_status: None,
desired_state: StartStop::Start,
..
} => {
seed.persistent_container.start().await?;
}
ServiceStateKinds {
running_status: Some(_),
desired_state: StartStop::Stop,
..
} => {
seed.persistent_container.stop().await?;
seed.persistent_container
.state
.send_if_modified(|s| s.running_status.take().is_some());
}
_ => (),
};
Ok::<_, Error>(())
}
.await
{
tracing::error!("error synchronizing state of service: {e}");
tracing::debug!("{e:?}");
seed.synchronized.notify_waiters();
tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS);
tokio::time::sleep(Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS)).await;
return ServiceActorLoopNext::DontWait;
}
seed.synchronized.notify_waiters();
ServiceActorLoopNext::Wait
match kinds {
ServiceStateKinds {
running_status: None,
desired_state: StartStop::Start,
..
} => {
let task = start_stop_task
.take()
.filter(|task| matches!(task, Either::Right(_)));
*start_stop_task = Some(
task.unwrap_or_else(|| Either::Right(seed.persistent_container.start().boxed())),
);
}
ServiceStateKinds {
running_status: Some(_),
desired_state: StartStop::Stop,
..
} => {
let task = start_stop_task
.take()
.filter(|task| matches!(task, Either::Left(_)));
*start_stop_task = Some(task.unwrap_or_else(|| {
Either::Left(
async {
seed.persistent_container.stop().await?;
seed.persistent_container
.state
.send_if_modified(|s| s.running_status.take().is_some());
Ok::<_, Error>(())
}
.boxed(),
)
}));
}
_ => (),
};
Ok(())
}

View File

@@ -9,7 +9,7 @@ use color_eyre::eyre::eyre;
use futures::{FutureExt, TryStreamExt};
use imbl::vector;
use imbl_value::InternedString;
use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async};
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use rustls::RootCertStore;
use rustls_pki_types::CertificateDer;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -24,12 +24,12 @@ use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
use crate::prelude::*;
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
use crate::shutdown::Shutdown;
use crate::util::Invoke;
use crate::util::cpupower::{Governor, get_available_governors, set_governor};
use crate::util::cpupower::{get_available_governors, set_governor, Governor};
use crate::util::io::open_file;
use crate::util::net::WebSocketExt;
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
use crate::util::sync::Watch;
use crate::util::Invoke;
use crate::{MAIN_DATA, PACKAGE_DATA};
pub fn experimental<C: Context>() -> ParentHandler<C> {
@@ -498,7 +498,7 @@ pub struct MetricsFollowResponse {
#[command(rename_all = "kebab-case")]
pub struct MetricsFollowParams {
#[ts(skip)]
#[serde(rename = "__auth_session")] // from Auth middleware
#[serde(rename = "__Auth_session")] // from Auth middleware
session: Option<InternedString>,
}
@@ -1039,8 +1039,8 @@ pub async fn test_smtp(
) -> Result<(), Error> {
#[cfg(feature = "mail-send")]
{
use mail_send::SmtpClientBuilder;
use mail_send::mail_builder::{self, MessageBuilder};
use mail_send::SmtpClientBuilder;
use rustls_pki_types::pem::PemObject;
let Some(pass_val) = password else {

View File

@@ -1,14 +1,16 @@
use std::net::Ipv4Addr;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use clap::Parser;
use imbl_value::InternedString;
use ipnet::Ipv4Net;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use crate::context::CliContext;
use crate::prelude::*;
use crate::tunnel::context::TunnelContext;
use crate::tunnel::wg::WgSubnetConfig;
use crate::tunnel::wg::{ClientConfig, WgConfig, WgSubnetClients, WgSubnetConfig};
use crate::util::serde::{display_serializable, HandlerExtSerde};
pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
@@ -17,6 +19,10 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
super::db::db_api::<C>()
.with_about("Commands to interact with the db i.e. dump and apply"),
)
.subcommand(
"auth",
super::auth::auth_api::<C>().with_about("Add or remove authorized clients"),
)
.subcommand(
"subnet",
subnet_api::<C>().with_about("Add, remove, or modify subnets"),
@@ -44,6 +50,7 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
}
#[derive(Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")]
pub struct SubnetParams {
subnet: Ipv4Net,
}
@@ -68,43 +75,94 @@ pub fn subnet_api<C: Context>() -> ParentHandler<C, SubnetParams> {
.with_about("Remove a subnet")
.with_call_remote::<CliContext>(),
)
// .subcommand(
// "set-default-forward-target",
// from_fn_async(set_default_forward_target)
// .with_metadata("sync_db", Value::Bool(true))
// .no_display()
// .with_about("Set the default target for port forwarding")
// .with_call_remote::<CliContext>(),
// )
// .subcommand(
// "add-device",
// from_fn_async(add_device)
// .with_metadata("sync_db", Value::Bool(true))
// .no_display()
// .with_about("Add a device to a subnet")
// .with_call_remote::<CliContext>(),
// )
// .subcommand(
// "remove-device",
// from_fn_async(remove_device)
// .with_metadata("sync_db", Value::Bool(true))
// .no_display()
// .with_about("Remove a device from a subnet")
// .with_call_remote::<CliContext>(),
// )
.subcommand(
"add-device",
from_fn_async(add_device)
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(|a, _| a)
.no_display()
.with_about("Add a device to a subnet")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove-device",
from_fn_async(remove_device)
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(|a, _| a)
.no_display()
.with_about("Remove a device from a subnet")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list-devices",
from_fn_async(list_devices)
.with_inherited(|a, _| a)
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, res);
}
let mut table = Table::new();
table.add_row(row![bc => "NAME", "IP", "PUBLIC KEY"]);
for (ip, config) in res.clients.0 {
table.add_row(row![config.name, ip, config.key.verifying_key()]);
}
table.print_tty(false)?;
Ok(())
})
.with_about("List devices in a subnet")
.with_call_remote::<CliContext>(),
)
.subcommand(
"show-config",
from_fn_async(show_config)
.with_inherited(|a, _| a)
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
if let Some(format) = params.format {
return display_serializable(format, res);
}
println!("{}", res);
Ok(())
})
.with_about("Show the WireGuard configuration for a subnet")
.with_call_remote::<CliContext>(),
)
}
#[derive(Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")]
pub struct AddSubnetParams {
name: InternedString,
}
pub async fn add_subnet(
ctx: TunnelContext,
_: Empty,
AddSubnetParams { name }: AddSubnetParams,
SubnetParams { subnet }: SubnetParams,
) -> Result<(), Error> {
if subnet.addr().octets()[3] == 0
|| subnet.addr().octets()[3] == 255
|| subnet.prefix_len() > 24
{
return Err(Error::new(
eyre!("invalid subnet"),
ErrorKind::InvalidRequest,
));
}
let server = ctx
.db
.mutate(|db| {
let map = db.as_wg_mut().as_subnets_mut();
if !map.contains_key(&subnet)? {
map.insert(&subnet, &WgSubnetConfig::new())?;
map.insert(&subnet, &WgSubnetConfig::new(name))?;
}
db.as_wg().de()
})
@@ -128,3 +186,162 @@ pub async fn remove_subnet(
.result?;
server.sync().await
}
#[derive(Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")]
pub struct AddDeviceParams {
name: InternedString,
ip: Option<Ipv4Addr>,
}
pub async fn add_device(
ctx: TunnelContext,
AddDeviceParams { name, ip }: AddDeviceParams,
SubnetParams { subnet }: SubnetParams,
) -> Result<(), Error> {
let config = WgConfig::generate(name);
let server = ctx
.db
.mutate(|db| {
db.as_wg_mut()
.as_subnets_mut()
.as_idx_mut(&subnet)
.or_not_found(&subnet)?
.as_clients_mut()
.mutate(|WgSubnetClients(clients)| {
let ip = if let Some(ip) = ip {
ip
} else {
subnet
.hosts()
.find(|ip| !clients.contains_key(ip) && *ip != subnet.addr())
.ok_or_else(|| {
Error::new(
eyre!("no available ips in subnet"),
ErrorKind::InvalidRequest,
)
})?
};
if ip.octets()[3] == 0 || ip.octets()[3] == 255 {
return Err(Error::new(eyre!("invalid ip"), ErrorKind::InvalidRequest));
}
if !subnet.contains(&ip) {
return Err(Error::new(
eyre!("ip not in subnet"),
ErrorKind::InvalidRequest,
));
}
clients.insert(ip, config).map_or(Ok(()), |_| {
Err(Error::new(
eyre!("ip already in use"),
ErrorKind::InvalidRequest,
))
})
})?;
db.as_wg().de()
})
.await
.result?;
server.sync().await
}
#[derive(Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")]
pub struct RemoveDeviceParams {
device: Ipv4Addr,
}
pub async fn remove_device(
ctx: TunnelContext,
RemoveDeviceParams { device }: RemoveDeviceParams,
SubnetParams { subnet }: SubnetParams,
) -> Result<(), Error> {
let server = ctx
.db
.mutate(|db| {
db.as_wg_mut()
.as_subnets_mut()
.as_idx_mut(&subnet)
.or_not_found(&subnet)?
.as_clients_mut()
.remove(&device)?
.or_not_found(&device)?;
db.as_wg().de()
})
.await
.result?;
server.sync().await
}
pub async fn list_devices(
ctx: TunnelContext,
_: Empty,
SubnetParams { subnet }: SubnetParams,
) -> Result<WgSubnetConfig, Error> {
ctx.db
.peek()
.await
.as_wg()
.as_subnets()
.as_idx(&subnet)
.or_not_found(&subnet)?
.de()
}
#[derive(Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")]
pub struct ShowConfigParams {
device: Ipv4Addr,
wan_addr: Option<IpAddr>,
#[serde(rename = "__ConnectInfo_local_addr")]
#[arg(skip)]
local_addr: Option<SocketAddr>,
}
pub async fn show_config(
ctx: TunnelContext,
ShowConfigParams {
device,
wan_addr,
local_addr,
}: ShowConfigParams,
SubnetParams { subnet }: SubnetParams,
) -> Result<ClientConfig, Error> {
let wg = ctx.db.peek().await.into_wg();
let client = wg
.as_subnets()
.as_idx(&subnet)
.or_not_found(&subnet)?
.as_clients()
.as_idx(&device)
.or_not_found(&device)?
.de()?;
let wan_addr = if let Some(wan_addr) = wan_addr.or(local_addr.map(|a| a.ip())).filter(|ip| {
!ip.is_loopback()
&& !match ip {
IpAddr::V4(ipv4) => ipv4.is_private() || ipv4.is_link_local(),
IpAddr::V6(ipv6) => ipv6.is_unique_local() || ipv6.is_unicast_link_local(),
}
}) {
wan_addr
} else {
ctx.net_iface
.ip_info()
.into_iter()
.find_map(|(_, info)| {
info.public()
.then_some(info.ip_info)
.flatten()
.into_iter()
.find_map(|info| info.subnets.into_iter().next())
})
.or_not_found("a public IP address")?
.addr()
};
Ok(client.client_config(
device,
wg.as_key().de()?.verifying_key(),
(wan_addr, wg.as_port().de()?).into(),
))
}

View File

@@ -0,0 +1,183 @@
use std::net::IpAddr;
use clap::Parser;
use imbl::HashMap;
use imbl_value::InternedString;
use patch_db::HasModel;
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::auth::{check_password, Sessions};
use crate::context::CliContext;
use crate::middleware::auth::AuthContext;
use crate::middleware::signature::SignatureAuthContext;
use crate::prelude::*;
use crate::rpc_continuations::OpenAuthedContinuations;
use crate::sign::AnyVerifyingKey;
use crate::tunnel::context::TunnelContext;
use crate::tunnel::db::TunnelDatabase;
use crate::util::serde::{display_serializable, HandlerExtSerde};
use crate::util::sync::SyncMutex;
impl SignatureAuthContext for TunnelContext {
type Database = TunnelDatabase;
type AdditionalMetadata = ();
type CheckPubkeyRes = ();
fn db(&self) -> &TypedPatchDb<Self::Database> {
&self.db
}
async fn sig_context(
&self,
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
self.addrs
.iter()
.filter(|a| !match a {
IpAddr::V4(a) => a.is_loopback() || a.is_unspecified(),
IpAddr::V6(a) => a.is_loopback() || a.is_unspecified(),
})
.map(|a| InternedString::from_display(&a))
.map(Ok)
}
fn check_pubkey(
db: &Model<Self::Database>,
pubkey: Option<&crate::sign::AnyVerifyingKey>,
_: Self::AdditionalMetadata,
) -> Result<Self::CheckPubkeyRes, Error> {
if let Some(pubkey) = pubkey {
if db.as_auth_pubkeys().de()?.contains_key(pubkey) {
return Ok(());
}
}
Err(Error::new(
eyre!("Key is not authorized"),
ErrorKind::IncorrectPassword,
))
}
async fn post_auth_hook(
&self,
_: Self::CheckPubkeyRes,
_: &rpc_toolkit::RpcRequest,
) -> Result<(), Error> {
Ok(())
}
}
impl AuthContext for TunnelContext {
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/start-tunnel/rpc.authcookie";
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:root";
fn access_sessions(db: &mut Model<Self::Database>) -> &mut Model<crate::auth::Sessions> {
db.as_sessions_mut()
}
fn ephemeral_sessions(&self) -> &SyncMutex<Sessions> {
&self.ephemeral_sessions
}
fn open_authed_continuations(&self) -> &OpenAuthedContinuations<Option<InternedString>> {
&self.open_authed_continuations
}
fn check_password(db: &Model<Self::Database>, password: &str) -> Result<(), Error> {
check_password(&db.as_password().de()?, password)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS, Parser)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct SignerInfo {
pub name: InternedString,
}
pub fn auth_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new().subcommand(
"key",
ParentHandler::<C>::new()
.subcommand(
"add",
from_fn_async(add_key)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Add a new authorized key")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove_key)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Remove an authorized key")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_keys)
.with_metadata("sync_db", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, res);
}
let mut table = Table::new();
table.add_row(row![bc => "NAME", "KEY"]);
for (key, info) in res {
table.add_row(row![info.name, key]);
}
table.print_tty(false)?;
Ok(())
})
.with_about("List authorized keys")
.with_call_remote::<CliContext>(),
),
)
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")]
pub struct AddKeyParams {
pub name: InternedString,
pub key: AnyVerifyingKey,
}
pub async fn add_key(
ctx: TunnelContext,
AddKeyParams { name, key }: AddKeyParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_auth_pubkeys_mut().mutate(|auth_pubkeys| {
auth_pubkeys.insert(key, SignerInfo { name });
Ok(())
})
})
.await
.result
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")]
pub struct RemoveKeyParams {
pub key: AnyVerifyingKey,
}
pub async fn remove_key(
ctx: TunnelContext,
RemoveKeyParams { key }: RemoveKeyParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_auth_pubkeys_mut()
.mutate(|auth_pubkeys| Ok(auth_pubkeys.remove(&key)))
})
.await
.result?;
Ok(())
}
pub async fn list_keys(ctx: TunnelContext) -> Result<HashMap<AnyVerifyingKey, SignerInfo>, Error> {
ctx.db.peek().await.into_auth_pubkeys().de()
}

View File

@@ -1,3 +1,5 @@
# StartTunnel config for {name}
[Interface]
Address = {addr}/24
PrivateKey = {privkey}
@@ -5,6 +7,6 @@ PrivateKey = {privkey}
[Peer]
PublicKey = {server_pubkey}
PresharedKey = {psk}
AllowedIPs = 0.0.0.0/0, ::/0
AllowedIPs = 0.0.0.0/0,::/0
Endpoint = {server_addr}
PersistentKeepalive = 25

View File

@@ -5,27 +5,33 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use clap::Parser;
use cookie::{Cookie, Expiration, SameSite};
use helpers::NonDetachingJoinHandle;
use http::HeaderMap;
use imbl::OrdMap;
use imbl_value::InternedString;
use patch_db::PatchDb;
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{CallRemote, Context, Empty};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use tokio::sync::broadcast::Sender;
use tracing::instrument;
use url::Url;
use crate::auth::{Sessions, check_password};
use crate::context::CliContext;
use crate::auth::Sessions;
use crate::context::config::ContextConfig;
use crate::context::{CliContext, RpcContext};
use crate::middleware::auth::AuthContext;
use crate::middleware::signature::SignatureAuthContext;
use crate::net::forward::PortForwardController;
use crate::net::gateway::NetworkInterfaceWatcher;
use crate::prelude::*;
use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations};
use crate::tunnel::TUNNEL_DEFAULT_PORT;
use crate::tunnel::db::TunnelDatabase;
use crate::tunnel::TUNNEL_DEFAULT_PORT;
use crate::util::io::read_file_to_string;
use crate::util::sync::SyncMutex;
use crate::util::Invoke;
#[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)]
#[serde(rename_all = "kebab-case")]
@@ -67,6 +73,7 @@ pub struct TunnelContextSeed {
pub ephemeral_sessions: SyncMutex<Sessions>,
pub net_iface: NetworkInterfaceWatcher,
pub forward: PortForwardController,
pub masquerade_thread: NonDetachingJoinHandle<()>,
pub shutdown: Sender<()>,
}
@@ -75,6 +82,7 @@ pub struct TunnelContext(Arc<TunnelContextSeed>);
impl TunnelContext {
#[instrument(skip_all)]
pub async fn init(config: &TunnelConfig) -> Result<Self, Error> {
Self::init_auth_cookie().await?;
let (shutdown, _) = tokio::sync::broadcast::channel(1);
let datadir = config
.datadir
@@ -96,6 +104,52 @@ impl TunnelContext {
));
let net_iface = NetworkInterfaceWatcher::new(async { OrdMap::new() }, []);
let forward = PortForwardController::new(net_iface.subscribe());
Command::new("sysctl")
.arg("-w")
.arg("net.ipv4.ip_forward=1")
.invoke(ErrorKind::Network)
.await?;
let mut masquerade_net_iface = net_iface.subscribe();
let masquerade_thread = tokio::spawn(async move {
loop {
for iface in masquerade_net_iface.peek(|i| i.keys().cloned().collect::<Vec<_>>()) {
if Command::new("iptables")
.arg("-t")
.arg("nat")
.arg("-C")
.arg("POSTROUTING")
.arg("-o")
.arg(iface.as_str())
.arg("-j")
.arg("MASQUERADE")
.invoke(ErrorKind::Network)
.await
.is_err()
{
Command::new("iptables")
.arg("-t")
.arg("nat")
.arg("-A")
.arg("POSTROUTING")
.arg("-o")
.arg(iface.as_str())
.arg("-j")
.arg("MASQUERADE")
.invoke(ErrorKind::Network)
.await
.log_err();
}
}
masquerade_net_iface.changed().await;
}
})
.into();
db.peek().await.into_wg().de()?.sync().await?;
Ok(Self(Arc::new(TunnelContextSeed {
listen,
addrs: crate::net::utils::all_socket_addrs_for(listen.port())
@@ -110,6 +164,7 @@ impl TunnelContext {
ephemeral_sessions: SyncMutex::new(Sessions::new()),
net_iface,
forward,
masquerade_thread,
shutdown,
})))
}
@@ -133,66 +188,6 @@ pub struct TunnelAddrParams {
pub tunnel: IpAddr,
}
impl SignatureAuthContext for TunnelContext {
type Database = TunnelDatabase;
type AdditionalMetadata = ();
type CheckPubkeyRes = ();
fn db(&self) -> &TypedPatchDb<Self::Database> {
&self.db
}
async fn sig_context(
&self,
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
self.addrs
.iter()
.filter(|a| !match a {
IpAddr::V4(a) => a.is_loopback() || a.is_unspecified(),
IpAddr::V6(a) => a.is_loopback() || a.is_unspecified(),
})
.map(|a| InternedString::from_display(&a))
.map(Ok)
}
fn check_pubkey(
db: &Model<Self::Database>,
pubkey: Option<&crate::sign::AnyVerifyingKey>,
_: Self::AdditionalMetadata,
) -> Result<Self::CheckPubkeyRes, Error> {
if let Some(pubkey) = pubkey {
if db.as_auth_pubkeys().de()?.contains(pubkey) {
return Ok(());
}
}
Err(Error::new(
eyre!("Developer Key is not authorized"),
ErrorKind::IncorrectPassword,
))
}
async fn post_auth_hook(
&self,
_: Self::CheckPubkeyRes,
_: &rpc_toolkit::RpcRequest,
) -> Result<(), Error> {
Ok(())
}
}
impl AuthContext for TunnelContext {
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/start-tunnel/rpc.authcookie";
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:root";
fn access_sessions(db: &mut Model<Self::Database>) -> &mut Model<crate::auth::Sessions> {
db.as_sessions_mut()
}
fn ephemeral_sessions(&self) -> &SyncMutex<Sessions> {
&self.ephemeral_sessions
}
fn open_authed_continuations(&self) -> &OpenAuthedContinuations<Option<InternedString>> {
&self.open_authed_continuations
}
fn check_password(db: &Model<Self::Database>, password: &str) -> Result<(), Error> {
check_password(&db.as_password().de()?, password)
}
}
impl CallRemote<TunnelContext> for CliContext {
async fn call_remote(
&self,
@@ -200,22 +195,84 @@ impl CallRemote<TunnelContext> for CliContext {
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
let local =
if let Ok(local) = read_file_to_string(TunnelContext::LOCAL_AUTH_COOKIE_PATH).await {
self.cookie_store
.lock()
.unwrap()
.insert_raw(
&Cookie::build(("local", local))
.domain("localhost")
.expires(Expiration::Session)
.same_site(SameSite::Strict)
.build(),
&"http://localhost".parse()?,
)
.with_kind(crate::ErrorKind::Network)?;
true
} else {
false
};
let tunnel_addr = if let Some(addr) = self.tunnel_addr {
addr
Some(addr)
} else if let Some(addr) = self.tunnel_listen {
addr
Some(addr)
} else {
None
};
let (url, sig_ctx) = if let Some(tunnel_addr) = tunnel_addr {
(
format!("https://{tunnel_addr}/rpc/v0").parse()?,
Some(InternedString::from_display(
&self.tunnel_listen.unwrap_or(tunnel_addr).ip(),
)),
)
} else if local {
(
format!("http://localhost:{TUNNEL_DEFAULT_PORT}/rpc/v0").parse()?,
None,
)
} else {
return Err(Error::new(eyre!("`--tunnel` required"), ErrorKind::InvalidRequest).into());
};
let sig_addr = self.tunnel_listen.unwrap_or(tunnel_addr);
let url = format!("https://{tunnel_addr}").parse()?;
method = method.strip_prefix("tunnel.").unwrap_or(method);
crate::middleware::signature::call_remote(
self,
url,
&InternedString::from_display(&sig_addr.ip()),
HeaderMap::new(),
sig_ctx.as_deref(),
method,
params,
)
.await
}
}
#[derive(Debug, Deserialize, Serialize, Parser)]
pub struct TunnelUrlParams {
pub tunnel: Url,
}
impl CallRemote<TunnelContext, TunnelUrlParams> for RpcContext {
async fn call_remote(
&self,
mut method: &str,
params: Value,
TunnelUrlParams { tunnel }: TunnelUrlParams,
) -> Result<Value, RpcError> {
let url = tunnel.join("rpc/v0")?;
method = method.strip_prefix("tunnel.").unwrap_or(method);
let sig_ctx = url.host_str().map(InternedString::from_display);
crate::middleware::signature::call_remote(
self,
url,
HeaderMap::new(),
sig_ctx.as_deref(),
method,
params,
)

View File

@@ -1,15 +1,14 @@
use std::collections::{BTreeMap, HashSet};
use std::net::{Ipv4Addr, SocketAddrV4};
use std::collections::BTreeMap;
use std::net::SocketAddrV4;
use std::path::PathBuf;
use clap::Parser;
use imbl_value::InternedString;
use ipnet::Ipv4Net;
use imbl::HashMap;
use itertools::Itertools;
use patch_db::Dump;
use patch_db::json_ptr::{JsonPointer, ROOT};
use patch_db::Dump;
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use tracing::instrument;
use ts_rs::TS;
@@ -18,9 +17,10 @@ use crate::auth::Sessions;
use crate::context::CliContext;
use crate::prelude::*;
use crate::sign::AnyVerifyingKey;
use crate::tunnel::auth::SignerInfo;
use crate::tunnel::context::TunnelContext;
use crate::tunnel::wg::WgServer;
use crate::util::serde::{HandlerExtSerde, apply_expr};
use crate::util::serde::{apply_expr, HandlerExtSerde};
#[derive(Default, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "camelCase")]
@@ -28,7 +28,7 @@ use crate::util::serde::{HandlerExtSerde, apply_expr};
pub struct TunnelDatabase {
pub sessions: Sessions,
pub password: String,
pub auth_pubkeys: HashSet<AnyVerifyingKey>,
pub auth_pubkeys: HashMap<AnyVerifyingKey, SignerInfo>,
pub wg: WgServer,
pub port_forwards: BTreeMap<SocketAddrV4, SocketAddrV4>,
}

View File

@@ -1,6 +1,6 @@
use axum::Router;
use futures::future::ready;
use rpc_toolkit::{Context, HandlerExt, ParentHandler, Server, from_fn_async};
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server};
use crate::context::CliContext;
use crate::middleware::auth::Auth;
@@ -12,6 +12,7 @@ use crate::rpc_continuations::Guid;
use crate::tunnel::context::TunnelContext;
pub mod api;
pub mod auth;
pub mod context;
pub mod db;
pub mod forward;

View File

@@ -1,17 +1,17 @@
use std::collections::BTreeMap;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::net::{Ipv4Addr, SocketAddr};
use ed25519_dalek::{SigningKey, VerifyingKey};
use imbl_value::InternedString;
use ipnet::Ipv4Net;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use x25519_dalek::{PublicKey, StaticSecret};
use crate::prelude::*;
use crate::util::Invoke;
use crate::util::io::write_file_atomic;
use crate::util::serde::Base64;
use crate::util::Invoke;
#[derive(Deserialize, Serialize, HasModel)]
#[serde(rename_all = "camelCase")]
@@ -79,31 +79,38 @@ impl Map for WgSubnetMap {
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
pub struct WgSubnetConfig {
pub default_forward_target: Option<Ipv4Addr>,
pub clients: BTreeMap<Ipv4Addr, WgConfig>,
pub name: InternedString,
pub clients: WgSubnetClients,
}
impl WgSubnetConfig {
pub fn new() -> Self {
Self::default()
}
pub fn add_client<'a>(
&'a mut self,
subnet: Ipv4Net,
) -> Result<(Ipv4Addr, &'a WgConfig), Error> {
let addr = subnet
.hosts()
.find(|a| !self.clients.contains_key(a))
.ok_or_else(|| Error::new(eyre!("subnet exhausted"), ErrorKind::Network))?;
let config = self.clients.entry(addr).or_insert(WgConfig::generate());
Ok((addr, config))
pub fn new(name: InternedString) -> Self {
Self {
name,
..Self::default()
}
}
}
pub struct WgKey(SigningKey);
#[derive(Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WgSubnetClients(pub BTreeMap<Ipv4Addr, WgConfig>);
impl Map for WgSubnetClients {
type Key = Ipv4Addr;
type Value = WgConfig;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Self::key_string(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(InternedString::from_display(key))
}
}
#[derive(Clone)]
pub struct WgKey(StaticSecret);
impl WgKey {
pub fn generate() -> Self {
Self(SigningKey::generate(
&mut ssh_key::rand_core::OsRng::default(),
Self(StaticSecret::random_from_rng(
ssh_key::rand_core::OsRng::default(),
))
}
}
@@ -113,33 +120,39 @@ impl AsRef<[u8]> for WgKey {
}
}
impl TryFrom<Vec<u8>> for WgKey {
type Error = ed25519_dalek::SignatureError;
type Error = Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
Ok(Self(value.as_slice().try_into()?))
Ok(Self(
<[u8; 32]>::try_from(value)
.map_err(|_| Error::new(eyre!("invalid key length"), ErrorKind::Deserialization))?
.into(),
))
}
}
impl std::ops::Deref for WgKey {
type Target = SigningKey;
type Target = StaticSecret;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Base64<WgKey> {
pub fn verifying_key(&self) -> Base64<VerifyingKey> {
Base64(self.0.verifying_key())
pub fn verifying_key(&self) -> Base64<PublicKey> {
Base64((&*self.0).into())
}
}
#[derive(Deserialize, Serialize, HasModel)]
#[derive(Clone, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
pub struct WgConfig {
pub name: InternedString,
pub key: Base64<WgKey>,
pub psk: Base64<[u8; 32]>,
}
impl WgConfig {
pub fn generate() -> Self {
pub fn generate(name: InternedString) -> Self {
Self {
name,
key: Base64(WgKey::generate()),
psk: Base64(rand::random()),
}
@@ -150,12 +163,12 @@ impl WgConfig {
client_addr: addr,
}
}
pub fn client_config<'a>(
&'a self,
pub fn client_config(
self,
addr: Ipv4Addr,
server_pubkey: Base64<VerifyingKey>,
server_addr: SocketAddrV4,
) -> ClientConfig<'a> {
server_pubkey: Base64<PublicKey>,
server_addr: SocketAddr,
) -> ClientConfig {
ClientConfig {
client_config: self,
client_addr: addr,
@@ -181,19 +194,33 @@ impl<'a> std::fmt::Display for ServerPeerConfig<'a> {
}
}
pub struct ClientConfig<'a> {
client_config: &'a WgConfig,
client_addr: Ipv4Addr,
server_pubkey: Base64<VerifyingKey>,
server_addr: SocketAddrV4,
fn deserialize_verifying_key<'de, D>(deserializer: D) -> Result<Base64<PublicKey>, D::Error>
where
D: serde::Deserializer<'de>,
{
Base64::<Vec<u8>>::deserialize(deserializer).and_then(|b| {
Ok(Base64(PublicKey::from(<[u8; 32]>::try_from(b.0).map_err(
|e: Vec<u8>| serde::de::Error::invalid_length(e.len(), &"a 32 byte base64 string"),
)?)))
})
}
impl<'a> std::fmt::Display for ClientConfig<'a> {
#[derive(Clone, Serialize, Deserialize)]
pub struct ClientConfig {
client_config: WgConfig,
client_addr: Ipv4Addr,
#[serde(deserialize_with = "deserialize_verifying_key")]
server_pubkey: Base64<PublicKey>,
server_addr: SocketAddr,
}
impl std::fmt::Display for ClientConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
include_str!("./client.conf.template"),
name = self.client_config.name,
privkey = self.client_config.key.to_padded_string(),
psk = self.client_config.psk,
psk = self.client_config.psk.to_padded_string(),
addr = self.client_addr,
server_pubkey = self.server_pubkey.to_padded_string(),
server_addr = self.server_addr,
@@ -212,7 +239,7 @@ impl<'a> std::fmt::Display for ServerConfig<'a> {
server_port = server.port,
server_privkey = server.key.to_padded_string(),
)?;
for (addr, peer) in server.subnets.0.values().flat_map(|s| &s.clients) {
for (addr, peer) in server.subnets.0.values().flat_map(|s| &s.clients.0) {
write!(f, "{}", peer.server_peer_config(*addr))?;
}
Ok(())

View File

@@ -1055,7 +1055,11 @@ impl<T: TryFrom<Vec<u8>>> ValueParserFactory for Base64<T> {
Self::Parser::new()
}
}
impl<'de, T: TryFrom<Vec<u8>>> Deserialize<'de> for Base64<T> {
impl<'de, T> Deserialize<'de> for Base64<T>
where
Base64<T>: FromStr,
<Base64<T> as FromStr>::Err: std::fmt::Display,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,

View File

@@ -3,7 +3,7 @@ use imbl_value::json;
use tokio::process::Command;
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_3_6_alpha_6};
use super::{v0_3_6_alpha_6, VersionT};
use crate::context::RpcContext;
use crate::prelude::*;
use crate::util::Invoke;
@@ -50,10 +50,7 @@ impl VersionT for Version {
async fn post_up(self, ctx: &RpcContext, _input: Value) -> Result<(), Error> {
Command::new("systemd-firstboot")
.arg("--root=/media/startos/config/overlay/")
.arg(format!(
"--hostname={}",
ctx.account.read().await.hostname.0
))
.arg(ctx.account.peek(|a| format!("--hostname={}", a.hostname.0)))
.invoke(ErrorKind::ParseSysInfo)
.await?;
Ok(())

View File

@@ -4,18 +4,18 @@ use exver::{PreReleaseSegment, VersionRange};
use tokio::fs::File;
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_3_6_alpha_7};
use crate::DATA_DIR;
use super::{v0_3_6_alpha_7, VersionT};
use crate::context::RpcContext;
use crate::install::PKG_ARCHIVE_DIR;
use crate::prelude::*;
use crate::s9pk::S9pk;
use crate::s9pk::manifest::{DeviceFilter, Manifest};
use crate::s9pk::merkle_archive::MerkleArchive;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::merkle_archive::MerkleArchive;
use crate::s9pk::v2::SIG_CONTEXT;
use crate::s9pk::S9pk;
use crate::service::LoadDisposition;
use crate::util::io::create_file;
use crate::DATA_DIR;
lazy_static::lazy_static! {
static ref V0_3_6_alpha_8: exver::Version = exver::Version::new(
@@ -115,7 +115,7 @@ impl VersionT for Version {
let manifest: Manifest = from_value(manifest.clone())?;
let id = manifest.id.clone();
let mut s9pk: S9pk<_> = S9pk::new_with_manifest(archive, None, manifest);
let s9pk_compat_key = ctx.account.read().await.developer_key.clone();
let s9pk_compat_key = ctx.account.peek(|a| a.developer_key.clone());
s9pk.as_archive_mut()
.set_signer(s9pk_compat_key, SIG_CONTEXT);
s9pk.serialize(&mut tmp_file, true).await?;