From 98f31d4891b12779c6254290666007de0e2032cf Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Sat, 27 Sep 2025 03:04:37 -0600 Subject: [PATCH] wip: start-tunnel --- Makefile | 68 +++-- basename.sh | 4 +- build/os-compat/run-compat.sh | 2 +- core/Cargo.lock | 1 + core/startos/Cargo.toml | 1 + core/startos/src/account.rs | 3 +- core/startos/src/auth.rs | 51 ++-- core/startos/src/backup/backup_bulk.rs | 8 +- core/startos/src/bins/start_cli.rs | 4 +- core/startos/src/bins/start_init.rs | 12 +- core/startos/src/bins/startd.rs | 10 +- core/startos/src/context/cli.rs | 46 ++- core/startos/src/context/rpc.rs | 12 +- core/startos/src/db/mod.rs | 2 +- core/startos/src/db/prelude.rs | 36 +-- core/startos/src/install/mod.rs | 2 +- core/startos/src/lib.rs | 184 ++++-------- core/startos/src/middleware/auth.rs | 74 +---- core/startos/src/middleware/connect_info.rs | 55 ++++ core/startos/src/middleware/mod.rs | 1 + core/startos/src/middleware/signature.rs | 115 ++++++-- core/startos/src/net/gateway.rs | 20 +- core/startos/src/net/static_server.rs | 184 ++++++------ core/startos/src/net/tor.rs | 3 +- core/startos/src/net/web_server.rs | 62 ++-- core/startos/src/net/wifi.rs | 33 ++- core/startos/src/notifications.rs | 3 +- core/startos/src/registry/context.rs | 83 ++---- core/startos/src/registry/device_info.rs | 4 +- core/startos/src/registry/os/asset/add.rs | 4 +- core/startos/src/registry/os/asset/sign.rs | 2 +- core/startos/src/registry/os/version/mod.rs | 4 +- core/startos/src/registry/package/add.rs | 4 +- core/startos/src/registry/package/get.rs | 4 +- core/startos/src/service/mod.rs | 2 +- core/startos/src/service/service_actor.rs | 251 +++++++++------- core/startos/src/system.rs | 12 +- core/startos/src/tunnel/api.rs | 275 ++++++++++++++++-- core/startos/src/tunnel/auth.rs | 183 ++++++++++++ core/startos/src/tunnel/client.conf.template | 4 +- core/startos/src/tunnel/context.rs | 195 ++++++++----- core/startos/src/tunnel/db.rs | 16 +- core/startos/src/tunnel/mod.rs | 3 +- core/startos/src/tunnel/wg.rs | 109 ++++--- core/startos/src/util/serde.rs | 6 +- core/startos/src/version/v0_3_6_alpha_7.rs | 7 +- core/startos/src/version/v0_3_6_alpha_8.rs | 10 +- ...gistry.service => start-registryd.service} | 2 +- core/startos/start-tunneld.service | 13 + debian/start-registry/postinst | 9 + debian/start-tunnel/postinst | 9 + debian/{ => startos}/postinst | 0 dpkg-build.sh | 35 ++- sdk/package/lib/util/SubContainer.ts | 9 +- 54 files changed, 1432 insertions(+), 819 deletions(-) create mode 100644 core/startos/src/middleware/connect_info.rs create mode 100644 core/startos/src/tunnel/auth.rs rename core/startos/{registry.service => start-registryd.service} (85%) create mode 100644 core/startos/start-tunneld.service create mode 100755 debian/start-registry/postinst create mode 100755 debian/start-tunnel/postinst rename debian/{ => startos}/postinst (100%) diff --git a/Makefile b/Makefile index f7003913b..f3f492766 100644 --- a/Makefile +++ b/Makefile @@ -5,15 +5,16 @@ PLATFORM_FILE := $(shell ./check-platform.sh) ENVIRONMENT_FILE := $(shell ./check-environment.sh) GIT_HASH_FILE := $(shell ./check-git-hash.sh) VERSION_FILE := $(shell ./check-version.sh) -BASENAME := $(shell ./basename.sh) +BASENAME := $(shell PROJECT=startos ./basename.sh) PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi) ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi) +REGISTRY_BASENAME := $(shell PROJECT=start-registry PLATFORM=$(ARCH) ./basename.sh) +TUNNEL_BASENAME := $(shell PROJECT=start-tunnel PLATFORM=$(ARCH) ./basename.sh) IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi) WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html web/dist/raw/install-wizard/index.html COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html web/dist/static/install-wizard/index.html FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json) BUILD_SRC := $(call ls-files, build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS) -DEBIAN_SRC := $(call ls-files, debian/) IMAGE_RECIPE_SRC := $(call ls-files, image-recipe/) STARTD_SRC := core/startos/startd.service $(BUILD_SRC) CORE_SRC := $(call ls-files, core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE) @@ -25,7 +26,7 @@ PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client GZIP_BIN := $(shell which pigz || which gzip) TAR_BIN := $(shell which gtar || which tar) COMPILED_TARGETS := core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox container-runtime/rootfs.$(ARCH).squashfs -ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs $(PLATFORM_FILE) \ +STARTOS_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs $(PLATFORM_FILE) \ $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then \ echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; \ fi) \ @@ -35,6 +36,8 @@ ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]; then \ echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; \ fi') +REGISTRY_TARGETS := core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/registrybox core/startos/start-registryd.service +TUNNEL_TARGETS := core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service REBUILD_TYPES = 1 ifeq ($(REMOTE),) @@ -58,12 +61,12 @@ endif .DELETE_ON_ERROR: -.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry +.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry install-registry tunnel install-tunnel -all: $(ALL_TARGETS) +all: $(STARTOS_TARGETS) touch: - touch $(ALL_TARGETS) + touch $(STARTOS_TARGETS) metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) @@ -111,20 +114,49 @@ test-container-runtime: container-runtime/node_modules/.package-lock.json $(call cli: ./core/install-cli.sh -registry: - ./core/build-registrybox.sh +registry: core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/registrybox -tunnel: - ./core/build-tunnelbox.sh +install-registry: $(REGISTRY_TARGETS) + $(call mkdir,$(DESTDIR)/usr/bin) + $(call cp,core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/registrybox,$(DESTDIR)/usr/bin/start-registrybox) + $(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registryd) + $(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registry) + + $(call mkdir,$(DESTDIR)/lib/systemd/system) + $(call cp,core/startos/start-registryd.service,$(DESTDIR)/lib/systemd/system/start-registryd.service) + +core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/registrybox: $(CORE_SRC) $(ENVIRONMENT_FILE) + ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-registrybox.sh + +tunnel: core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox + +install-tunnel: core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service + $(call mkdir,$(DESTDIR)/usr/bin) + $(call cp,core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox,$(DESTDIR)/usr/bin/start-tunnelbox) + $(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunneld) + $(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunnel) + + $(call mkdir,$(DESTDIR)/lib/systemd/system) + $(call cp,core/startos/start-tunneld.service,$(DESTDIR)/lib/systemd/system/start-tunneld.service) + +core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox: $(CORE_SRC) $(ENVIRONMENT_FILE) + ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-tunnelbox.sh deb: results/$(BASENAME).deb -debian/control: build/lib/depends build/lib/conflicts - ./debuild/control.sh - -results/$(BASENAME).deb: dpkg-build.sh $(DEBIAN_SRC) $(ALL_TARGETS) +results/$(BASENAME).deb: dpkg-build.sh $(call ls-files,debian/startos) $(STARTOS_TARGETS) PLATFORM=$(PLATFORM) REQUIRES=debian ./build/os-compat/run-compat.sh ./dpkg-build.sh +registry-deb: results/$(REGISTRY_BASENAME).deb + +results/$(REGISTRY_BASENAME).deb: dpkg-build.sh $(call ls-files,debian/start-registry) $(REGISTRY_TARGETS) + PROJECT=start-registry PLATFORM=$(ARCH) REQUIRES=debian ./build/os-compat/run-compat.sh ./dpkg-build.sh + +tunnel-deb: results/$(TUNNEL_BASENAME).deb + +results/$(TUNNEL_BASENAME).deb: dpkg-build.sh $(call ls-files,debian/start-tunnel) $(TUNNEL_TARGETS) + PROJECT=start-tunnel PLATFORM=$(ARCH) REQUIRES=debian DEPENDS=wireguard-tools,iptables,network-manager ./build/os-compat/run-compat.sh ./dpkg-build.sh + $(IMAGE_TYPE): results/$(BASENAME).$(IMAGE_TYPE) squashfs: results/$(BASENAME).squashfs @@ -133,7 +165,7 @@ results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_S REQUIRES=debian ./build/os-compat/run-compat.sh ./image-recipe/run-local-build.sh "results/$(BASENAME).deb" # For creating os images. DO NOT USE -install: $(ALL_TARGETS) +install: $(STARTOS_TARGETS) $(call mkdir,$(DESTDIR)/usr/bin) $(call mkdir,$(DESTDIR)/usr/sbin) $(call cp,core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/startbox,$(DESTDIR)/usr/bin/startbox) @@ -165,7 +197,7 @@ install: $(ALL_TARGETS) $(call cp,firmware/$(PLATFORM),$(DESTDIR)/usr/lib/startos/firmware) -update-overlay: $(ALL_TARGETS) +update-overlay: $(STARTOS_TARGETS) @echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m" @echo "\033[33mALL CHANGES WILL BE REVERTED IF YOU RESTART THE DEVICE\033[0m" @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi @@ -191,7 +223,7 @@ wormhole-squashfs: results/$(BASENAME).squashfs @echo @wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && /usr/lib/startos/scripts/prune-boot && cd /media/startos/images && wormhole receive --accept-file %s && CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/use-img ./$(BASENAME).squashfs'"'"'\n", $$3 }' -update: $(ALL_TARGETS) +update: $(STARTOS_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM) @@ -219,7 +251,7 @@ update-squashfs: results/$(BASENAME).squashfs $(call cp,results/$(BASENAME).squashfs,/media/startos/images/next.rootfs) $(call ssh,'sudo CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/use-img /media/startos/images/next.rootfs') -emulate-reflash: $(ALL_TARGETS) +emulate-reflash: $(STARTOS_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM) diff --git a/basename.sh b/basename.sh index 679faa5bc..fd5bd759d 100755 --- a/basename.sh +++ b/basename.sh @@ -1,5 +1,7 @@ #!/bin/bash +PROJECT=${PROJECT:-"startos"} + cd "$(dirname "${BASH_SOURCE[0]}")" PLATFORM="$(if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)" @@ -16,4 +18,4 @@ if [ -n "$STARTOS_ENV" ]; then VERSION_FULL="$VERSION_FULL~${STARTOS_ENV}" fi -echo -n "startos-${VERSION_FULL}_${PLATFORM}" \ No newline at end of file +echo -n "${PROJECT}-${VERSION_FULL}_${PLATFORM}" \ No newline at end of file diff --git a/build/os-compat/run-compat.sh b/build/os-compat/run-compat.sh index ff04217ba..2151fd1b2 100755 --- a/build/os-compat/run-compat.sh +++ b/build/os-compat/run-compat.sh @@ -18,7 +18,7 @@ if [ "$FORCE_COMPAT" = 1 ] || ( [ "$REQUIRES" = "linux" ] && [ "$(uname -s)" != docker run -d --rm --name os-compat --privileged --security-opt apparmor=unconfined -v "${project_pwd}:/root/start-os" -v /lib/modules:/lib/modules:ro start9/build-env while ! docker exec os-compat systemctl is-active --quiet multi-user.target 2> /dev/null; do sleep .5; done - docker exec -eARCH -eENVIRONMENT -ePLATFORM -eGIT_BRANCH_AS_HASH $USE_TTY -w "/root/start-os${rel_pwd}" os-compat $@ + docker exec -eARCH -eENVIRONMENT -ePLATFORM -eGIT_BRANCH_AS_HASH -ePROJECT -eDEPENDS -eCONFLICTS $USE_TTY -w "/root/start-os${rel_pwd}" os-compat $@ code=$? docker stop os-compat exit $code diff --git a/core/Cargo.lock b/core/Cargo.lock index 69515d048..dbf8e79f7 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -7439,6 +7439,7 @@ dependencies = [ "url", "urlencoding", "uuid", + "x25519-dalek", "zbus", "zeroize", ] diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index b7ea7dcf2..8ecc317f9 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -259,6 +259,7 @@ unix-named-pipe = "0.2.0" url = { version = "2.4.1", features = ["serde"] } urlencoding = "2.1.3" uuid = { version = "1.4.1", features = ["v4"] } +x25519-dalek = "2.0.1" zbus = "5.1.1" zeroize = "1.6.0" mail-send = { git = "https://github.com/dr-bonez/mail-send.git", branch = "main", optional = true } diff --git a/core/startos/src/account.rs b/core/startos/src/account.rs index 58d4649e0..b86ebbbfb 100644 --- a/core/startos/src/account.rs +++ b/core/startos/src/account.rs @@ -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() diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index a49c8f2ab..54c62e0df 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -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); @@ -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, #[serde(default)] ephemeral: bool, @@ -279,7 +279,7 @@ pub async fn login_impl( #[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, 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, } @@ -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)] diff --git a/core/startos/src/backup/backup_bulk.rs b/core/startos/src/backup/backup_bulk.rs index 28170eb9b..9098b4deb 100644 --- a/core/startos/src/backup/backup_bulk.rs +++ b/core/startos/src/backup/backup_bulk.rs @@ -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); diff --git a/core/startos/src/bins/start_cli.rs b/core/startos/src/bins/start_cli.rs index b3b10cf71..5e25a0cca 100644 --- a/core/startos/src/bins/start_cli.rs +++ b/core/startos/src/bins/start_cli.rs @@ -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) { if let Err(e) = CliApp::new( |cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?), - crate::expanded_api(), + crate::main_api(), ) .run(args) { diff --git a/core/startos/src/bins/start_init.rs b/core/startos/src/bins/start_init.rs index 70c32ed1a..477207e91 100644 --- a/core/startos/src/bins/start_init.rs +++ b/core/startos/src/bins/start_init.rs @@ -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(); diff --git a/core/startos/src/bins/startd.rs b/core/startos/src/bins/startd.rs index 8c1e77afb..ac0531b62 100644 --- a/core/startos/src/bins/startd.rs +++ b/core/startos/src/bins/startd.rs @@ -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) { e, )?; - server.serve_diagnostic(ctx.clone()); + server.serve_ui_for(ctx.clone()); let mut shutdown = ctx.shutdown.subscribe(); diff --git a/core/startos/src/context/cli.rs b/core/startos/src/context/cli.rs index e93963f56..c3a2d9151 100644 --- a/core/startos/src/context/cli.rs +++ b/core/startos/src/context/cli.rs @@ -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 = ::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 for CliContext { + fn as_ref(&self) -> &Client { + &self.client + } +} + impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - 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 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 for CliContext { } impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - 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 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 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 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, ) diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 0e05a1cf8..585523028 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -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, pub db: TypedPatchDb, pub sync_db: watch::Sender, - pub account: RwLock, + pub account: SyncRwLock, pub net_controller: Arc, 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 { >::call_remote(&self, method, params, extra).await } } +impl AsRef for RpcContext { + fn as_ref(&self) -> &Client { + &self.client + } +} impl AsRef for RpcContext { fn as_ref(&self) -> &Jwk { &CURRENT_SECRET diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index f1a16ab2f..3c48b12d8 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -127,7 +127,7 @@ pub struct SubscribeParams { #[ts(type = "string | null")] pointer: Option, #[ts(skip)] - #[serde(rename = "__auth_session")] + #[serde(rename = "__Auth_session")] session: Option, } diff --git a/core/startos/src/db/prelude.rs b/core/startos/src/db/prelude.rs index bb779a3c0..6237ca050 100644 --- a/core/startos/src/db/prelude.rs +++ b/core/startos/src/db/prelude.rs @@ -216,13 +216,18 @@ impl Model 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, + ) -> Result>, 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>, Error> { + self.insert_model(key, Model::new(value)?) + } pub fn upsert(&mut self, key: &T::Key, value: F) -> Result<&mut Model, Error> where F: FnOnce() -> Result, @@ -257,22 +269,6 @@ where .into()), } } - pub fn insert_model(&mut self, key: &T::Key, value: Model) -> 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 Model diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 3c4d54ebc..0deb2da91 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -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, } diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index bcf3d3418..25b4733e0 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -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() -> ParentHandler { .with_about("Display the API that is currently serving") .with_call_remote::(), ) + .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::(), + ) + .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::(), + ) .subcommand( "server", server::() @@ -191,6 +204,19 @@ pub fn main_api() -> ParentHandler { ) .no_cli(), ) + .subcommand( + "registry", + registry::registry_api::().with_about("Commands related to the registry"), + ) + .subcommand( + "tunnel", + CallRemoteHandler::::new(tunnel::api::tunnel_api()) + .no_cli(), + ) + .subcommand( + "tunnel", + tunnel::api::tunnel_api::().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() -> ParentHandler { .subcommand( "util", util::rpc::util::().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::() + .with_about("Commands to display logs, restart the server, etc"), + ) + .subcommand("setup", setup::setup::()) + .subcommand( + "install", + os_install::install::() + .with_about("Commands to list disk info, install StartOS, and reboot"), ); if &*PLATFORM != "raspberrypi" { api = api.subcommand("kiosk", kiosk::()); @@ -484,127 +532,3 @@ pub fn package() -> ParentHandler { net::host::host_api::().with_about("Manage network hosts for a package"), ) } - -pub fn diagnostic_api() -> ParentHandler { - 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::) - .with_about("Echo a message") - .with_call_remote::(), - ) - .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::(), - ) - .subcommand( - "diagnostic", - diagnostic::diagnostic::() - .with_about("Diagnostic commands i.e. logs, restart, rebuild"), - ) -} - -pub fn init_api() -> ParentHandler { - 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::) - .with_about("Echo a message") - .with_call_remote::(), - ) - .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::(), - ) - .subcommand( - "init", - init::init_api::() - .with_about("Commands to get logs or initialization progress"), - ) -} - -pub fn setup_api() -> ParentHandler { - 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::) - .with_about("Echo a message") - .with_call_remote::(), - ) - .subcommand("setup", setup::setup::()) -} - -pub fn install_api() -> ParentHandler { - 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::) - .with_about("Echo a message") - .with_call_remote::(), - ) - .subcommand( - "install", - os_install::install::() - .with_about("Commands to list disk info, install StartOS, and reboot"), - ) -} - -pub fn expanded_api() -> ParentHandler { - 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::() - .with_about("Commands to display logs, restart the server, etc"), - ) - .subcommand("setup", setup::setup::()) - .subcommand( - "install", - os_install::install::() - .with_about("Commands to list disk info, install StartOS, and reboot"), - ) - .subcommand( - "registry", - registry::registry_api::().with_about("Commands related to the registry"), - ) -} diff --git a/core/startos/src/middleware/auth.rs b/core/startos/src/middleware/auth.rs index 4fed0fd12..47a6935a8 100644 --- a/core/startos/src/middleware/auth.rs +++ b/core/startos/src/middleware/auth.rs @@ -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.db - } - async fn sig_context( - &self, - ) -> impl IntoIterator + 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::>() - } - fn check_pubkey( - db: &Model, - pubkey: Option<&AnyVerifyingKey>, - _: Self::AdditionalMetadata, - ) -> Result { - 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 Middleware 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 Middleware 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())); } _ => (), diff --git a/core/startos/src/middleware/connect_info.rs b/core/startos/src/middleware/connect_info.rs new file mode 100644 index 000000000..2f759a16d --- /dev/null +++ b/core/startos/src/middleware/connect_info.rs @@ -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, + local_addr: Option, +} +impl ConnectInfo { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Deserialize)] +pub struct Metadata { + get_connect_info: bool, +} + +impl Middleware 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(()) + } +} diff --git a/core/startos/src/middleware/mod.rs b/core/startos/src/middleware/mod.rs index c1b8cb573..f71837a93 100644 --- a/core/startos/src/middleware/mod.rs +++ b/core/startos/src/middleware/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod connect_info; pub mod cors; pub mod db; pub mod signature; diff --git a/core/startos/src/middleware/signature.rs b/core/startos/src/middleware/signature.rs index b07d79a20..21df6e053 100644 --- a/core/startos/src/middleware/signature.rs +++ b/core/startos/src/middleware/signature.rs @@ -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> + Send + Sync; type AdditionalMetadata: DeserializeOwned + Send; @@ -28,7 +33,7 @@ pub trait SignatureAuthContext: Context { fn sig_context( &self, ) -> impl Future + Send, Error>> + Send> - + Send; + + Send; fn check_pubkey( db: &Model, pubkey: Option<&AnyVerifyingKey>, @@ -41,7 +46,82 @@ pub trait SignatureAuthContext: Context { ) -> impl Future> + 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.db + } + async fn sig_context( + &self, + ) -> impl IntoIterator + 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::>() + }) + } + fn check_pubkey( + db: &Model, + pubkey: Option<&AnyVerifyingKey>, + _: Self::AdditionalMetadata, + ) -> Result { + 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; +} + +impl SigningContext for CliContext { + fn signing_key(&self) -> Result { + Ok(AnySigningKey::Ed25519(self.developer_key()?.clone())) + } +} + +impl SigningContext for RpcContext { + fn signing_key(&self) -> Result { + Ok(AnySigningKey::Ed25519( + self.account.peek(|a| a.developer_key.clone()), + )) + } +} #[derive(Deserialize)] pub struct Metadata { @@ -203,7 +283,7 @@ impl Middleware 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 Middleware for SignatureAuth { } } -pub async fn call_remote( - ctx: &CliContext, +pub async fn call_remote>( + ctx: &Ctx, url: Url, - sig_context: &str, + headers: HeaderMap, + sig_context: Option<&str>, method: &str, params: Value, ) -> Result { - 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?; diff --git a/core/startos/src/net/gateway.rs b/core/startos/src/net/gateway.rs index f67a3700f..8c9fb3137 100644 --- a/core/startos/src/net/gateway.rs +++ b/core/startos/src/net/gateway.rs @@ -244,6 +244,8 @@ mod active_connection { default_service = "org.freedesktop.NetworkManager" )] trait ConnectionSettings { + fn delete(&self) -> Result<(), Error>; + fn get_settings(&self) -> Result>, 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())) diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index b055dec72..aad3aca20 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -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 + Clone + Sized { + fn path(path: &str) -> PathBuf; + fn middleware(server: Server) -> HttpServer; + 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) -> HttpServer { + 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, 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) -> HttpServer { + server.middleware(Cors::new()) + } +} + +impl UiContext for DiagnosticContext { + fn path(path: &str) -> PathBuf { + Path::new("ui").join(path) + } + fn middleware(server: Server) -> HttpServer { + server.middleware(Cors::new()) + } +} + +impl UiContext for SetupContext { + fn path(path: &str) -> PathBuf { + Path::new("setup-wizard").join(path) + } + fn middleware(server: Server) -> HttpServer { + server.middleware(Cors::new()) + } +} + +impl UiContext for InstallContext { + fn path(path: &str) -> PathBuf { + Path::new("install-wizard").join(path) + } + fn middleware(server: Server) -> HttpServer { + server.middleware(Cors::new()) } } @@ -111,11 +175,11 @@ pub fn rpc_router>( ) } -fn serve_ui(req: Request, ui_mode: UiMode) -> Result { +fn serve_ui(req: Request) -> Result { 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 { 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 { } } -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::()) - .middleware(Cors::new()) - .middleware(Auth::new()) - .middleware(SyncDb::new()) - }) - .route("/proxy/{url}", { - let ctx = ctx.clone(); - any(move |x::Path(url): x::Path, 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(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::(request).unwrap_or_else(server_error) + })) } pub fn refresher() -> Router { diff --git a/core/startos/src/net/tor.rs b/core/startos/src/net/tor.rs index 8a82cd5e4..2cb18e45b 100644 --- a/core/startos/src/net/tor.rs +++ b/core/startos/src/net/tor.rs @@ -191,7 +191,8 @@ impl Model { 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 { self.as_idx(address) diff --git a/core/startos/src/net/web_server.rs b/core/startos/src/net/web_server.rs index 8a2a66d7d..0a6f584bb 100644 --- a/core/startos/src/net/web_server.rs +++ b/core/startos/src/net/web_server.rs @@ -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 { fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { 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 WebServer { } } - struct SwappableRouter(Watch>, bool); + struct SwappableRouter { + router: Watch>, + redirect: bool, + local_addr: SocketAddr, + peer_addr: SocketAddr, + } impl hyper::service::Service> for SwappableRouter { type Response = , @@ -199,13 +207,16 @@ impl WebServer { hyper::Request, >>::Future; - fn call(&self, req: hyper::Request) -> Self::Future { + fn call(&self, mut req: hyper::Request) -> 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 WebServer { 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 WebServer { 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(&mut self, ctx: C) { + self.serve_router(ui_router(ctx)) } } diff --git a/core/startos/src/net/wifi.rs b/core/startos/src/net/wifi.rs index 44465e501..8f4f58091 100644 --- a/core/startos/src/net/wifi.rs +++ b/core/startos/src/net/wifi.rs @@ -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>>; @@ -1017,6 +1017,31 @@ pub async fn synchronize_network_manager>( .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") diff --git a/core/startos/src/notifications.rs b/core/startos/src/notifications.rs index 6a55fb722..1ab569dc7 100644 --- a/core/startos/src/notifications.rs +++ b/core/startos/src/notifications.rs @@ -463,7 +463,8 @@ pub fn notify( data, seen: false, }, - ) + )?; + Ok(()) } #[test] diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs index b97a2679f..35b19207c 100644 --- a/core/startos/src/registry/context.rs +++ b/core/startos/src/registry/context.rs @@ -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 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 for RpcContext { params: Value, RegistryUrlParams { registry }: RegistryUrlParams, ) -> Result { - 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::(&*res.bytes().await?) - .with_kind(ErrorKind::Deserialization)? - .result - } - _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), - } + ) + .await } } diff --git a/core/startos/src/registry/device_info.rs b/core/startos/src/registry/device_info.rs index cb3f19089..acddf644e 100644 --- a/core/startos/src/registry/device_info.rs +++ b/core/startos/src/registry/device_info.rs @@ -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 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)?)?; } } diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs index e5ca3a934..915fb882f 100644 --- a/core/startos/src/registry/os/asset/add.rs +++ b/core/startos/src/registry/os/asset/add.rs @@ -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, } diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs index 61501b46e..68c0f571c 100644 --- a/core/startos/src/registry/os/asset/sign.rs +++ b/core/startos/src/registry/os/asset/sign.rs @@ -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, } diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index 4fb2b10e9..29105d577 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -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, } @@ -146,7 +146,7 @@ pub struct GetOsVersionParams { platform: Option, #[ts(skip)] #[arg(skip)] - #[serde(rename = "__device_info")] + #[serde(rename = "__DeviceInfo_device_info")] pub device_info: Option, } diff --git a/core/startos/src/registry/package/add.rs b/core/startos/src/registry/package/add.rs index 7bd836ff9..baf1703cc 100644 --- a/core/startos/src/registry/package/add.rs +++ b/core/startos/src/registry/package/add.rs @@ -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, } diff --git a/core/startos/src/registry/package/get.rs b/core/startos/src/registry/package/get.rs index cdaef66a8..b242217c4 100644 --- a/core/startos/src/registry/package/get.rs +++ b/core/startos/src/registry/package/get.rs @@ -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, #[ts(skip)] #[arg(skip)] - #[serde(rename = "__device_info")] + #[serde(rename = "__DeviceInfo_device_info")] pub device_info: Option, #[serde(default)] #[arg(default_value = "none")] diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index d98f43379..3c6da0da4 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -768,7 +768,7 @@ pub struct AttachParams { pub stderr_tty: bool, pub pty_size: Option, #[ts(skip)] - #[serde(rename = "__auth_session")] + #[serde(rename = "__Auth_session")] session: Option, #[ts(type = "string | null")] subcontainer: Option, diff --git a/core/startos/src/service/service_actor.rs b/core/startos/src/service/service_actor.rs index 697245c5d..cdd45c836 100644 --- a/core/startos/src/service/service_actor.rs +++ b/core/startos/src/service/service_actor.rs @@ -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); -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> = None; loop { - match service_actor_loop(¤t, &seed).await { - ServiceActorLoopNext::Wait => tokio::select! { - _ = current.changed() => (), - }, - ServiceActorLoopNext::DontWait => (), - } + let wait = match service_actor_loop(¤t, &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, - seed: &Arc, -) -> ServiceActorLoopNext { + seed: &'a Arc, + start_stop_task: &mut Option< + Either>, 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(()) } diff --git a/core/startos/src/system.rs b/core/startos/src/system.rs index e4befb77f..01b85066e 100644 --- a/core/startos/src/system.rs +++ b/core/startos/src/system.rs @@ -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() -> ParentHandler { @@ -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, } @@ -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 { diff --git a/core/startos/src/tunnel/api.rs b/core/startos/src/tunnel/api.rs index da73232f2..2448e6b71 100644 --- a/core/startos/src/tunnel/api.rs +++ b/core/startos/src/tunnel/api.rs @@ -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() -> ParentHandler { ParentHandler::new() @@ -17,6 +19,10 @@ pub fn tunnel_api() -> ParentHandler { super::db::db_api::() .with_about("Commands to interact with the db i.e. dump and apply"), ) + .subcommand( + "auth", + super::auth::auth_api::().with_about("Add or remove authorized clients"), + ) .subcommand( "subnet", subnet_api::().with_about("Add, remove, or modify subnets"), @@ -44,6 +50,7 @@ pub fn tunnel_api() -> ParentHandler { } #[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] pub struct SubnetParams { subnet: Ipv4Net, } @@ -68,43 +75,94 @@ pub fn subnet_api() -> ParentHandler { .with_about("Remove a subnet") .with_call_remote::(), ) - // .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::(), - // ) - // .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::(), - // ) - // .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::(), - // ) + .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::(), + ) + .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::(), + ) + .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::(), + ) + .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::(), + ) +} + +#[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, +} + +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 { + 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, + #[serde(rename = "__ConnectInfo_local_addr")] + #[arg(skip)] + local_addr: Option, +} + +pub async fn show_config( + ctx: TunnelContext, + ShowConfigParams { + device, + wan_addr, + local_addr, + }: ShowConfigParams, + SubnetParams { subnet }: SubnetParams, +) -> Result { + 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(), + )) +} diff --git a/core/startos/src/tunnel/auth.rs b/core/startos/src/tunnel/auth.rs new file mode 100644 index 000000000..a8f97b961 --- /dev/null +++ b/core/startos/src/tunnel/auth.rs @@ -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.db + } + async fn sig_context( + &self, + ) -> impl IntoIterator + 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, + pubkey: Option<&crate::sign::AnyVerifyingKey>, + _: Self::AdditionalMetadata, + ) -> Result { + 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) -> &mut Model { + db.as_sessions_mut() + } + fn ephemeral_sessions(&self) -> &SyncMutex { + &self.ephemeral_sessions + } + fn open_authed_continuations(&self) -> &OpenAuthedContinuations> { + &self.open_authed_continuations + } + fn check_password(db: &Model, 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"] +#[ts(export)] +pub struct SignerInfo { + pub name: InternedString, +} + +pub fn auth_api() -> ParentHandler { + ParentHandler::new().subcommand( + "key", + ParentHandler::::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::(), + ) + .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::(), + ) + .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::(), + ), + ) +} + +#[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, Error> { + ctx.db.peek().await.into_auth_pubkeys().de() +} diff --git a/core/startos/src/tunnel/client.conf.template b/core/startos/src/tunnel/client.conf.template index 2c7735eb9..58673b890 100644 --- a/core/startos/src/tunnel/client.conf.template +++ b/core/startos/src/tunnel/client.conf.template @@ -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 \ No newline at end of file diff --git a/core/startos/src/tunnel/context.rs b/core/startos/src/tunnel/context.rs index ff4b15a56..37970b880 100644 --- a/core/startos/src/tunnel/context.rs +++ b/core/startos/src/tunnel/context.rs @@ -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, pub net_iface: NetworkInterfaceWatcher, pub forward: PortForwardController, + pub masquerade_thread: NonDetachingJoinHandle<()>, pub shutdown: Sender<()>, } @@ -75,6 +82,7 @@ pub struct TunnelContext(Arc); impl TunnelContext { #[instrument(skip_all)] pub async fn init(config: &TunnelConfig) -> Result { + 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::>()) { + 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.db - } - async fn sig_context( - &self, - ) -> impl IntoIterator + 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, - pubkey: Option<&crate::sign::AnyVerifyingKey>, - _: Self::AdditionalMetadata, - ) -> Result { - 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) -> &mut Model { - db.as_sessions_mut() - } - fn ephemeral_sessions(&self) -> &SyncMutex { - &self.ephemeral_sessions - } - fn open_authed_continuations(&self) -> &OpenAuthedContinuations> { - &self.open_authed_continuations - } - fn check_password(db: &Model, password: &str) -> Result<(), Error> { - check_password(&db.as_password().de()?, password) - } -} - impl CallRemote for CliContext { async fn call_remote( &self, @@ -200,22 +195,84 @@ impl CallRemote for CliContext { params: Value, _: Empty, ) -> Result { + 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 for RpcContext { + async fn call_remote( + &self, + mut method: &str, + params: Value, + TunnelUrlParams { tunnel }: TunnelUrlParams, + ) -> Result { + 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, ) diff --git a/core/startos/src/tunnel/db.rs b/core/startos/src/tunnel/db.rs index 23ed05c63..1f036071e 100644 --- a/core/startos/src/tunnel/db.rs +++ b/core/startos/src/tunnel/db.rs @@ -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, + pub auth_pubkeys: HashMap, pub wg: WgServer, pub port_forwards: BTreeMap, } diff --git a/core/startos/src/tunnel/mod.rs b/core/startos/src/tunnel/mod.rs index 152526c01..f85b495b9 100644 --- a/core/startos/src/tunnel/mod.rs +++ b/core/startos/src/tunnel/mod.rs @@ -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; diff --git a/core/startos/src/tunnel/wg.rs b/core/startos/src/tunnel/wg.rs index 4f5277119..848061d05 100644 --- a/core/startos/src/tunnel/wg.rs +++ b/core/startos/src/tunnel/wg.rs @@ -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"] pub struct WgSubnetConfig { - pub default_forward_target: Option, - pub clients: BTreeMap, + 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); +impl Map for WgSubnetClients { + type Key = Ipv4Addr; + type Value = WgConfig; + fn key_str(key: &Self::Key) -> Result, Error> { + Self::key_string(key) + } + fn key_string(key: &Self::Key) -> Result { + 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> for WgKey { - type Error = ed25519_dalek::SignatureError; + type Error = Error; fn try_from(value: Vec) -> Result { - 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 { - pub fn verifying_key(&self) -> Base64 { - Base64(self.0.verifying_key()) + pub fn verifying_key(&self) -> Base64 { + Base64((&*self.0).into()) } } -#[derive(Deserialize, Serialize, HasModel)] +#[derive(Clone, Deserialize, Serialize, HasModel)] #[serde(rename_all = "camelCase")] #[model = "Model"] pub struct WgConfig { + pub name: InternedString, pub key: Base64, 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, - server_addr: SocketAddrV4, - ) -> ClientConfig<'a> { + server_pubkey: Base64, + 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, - server_addr: SocketAddrV4, +fn deserialize_verifying_key<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + Base64::>::deserialize(deserializer).and_then(|b| { + Ok(Base64(PublicKey::from(<[u8; 32]>::try_from(b.0).map_err( + |e: Vec| 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, + 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(()) diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index e2d621daf..435990fcf 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -1055,7 +1055,11 @@ impl>> ValueParserFactory for Base64 { Self::Parser::new() } } -impl<'de, T: TryFrom>> Deserialize<'de> for Base64 { +impl<'de, T> Deserialize<'de> for Base64 +where + Base64: FromStr, + as FromStr>::Err: std::fmt::Display, +{ fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/core/startos/src/version/v0_3_6_alpha_7.rs b/core/startos/src/version/v0_3_6_alpha_7.rs index 241404fd2..dbecceba1 100644 --- a/core/startos/src/version/v0_3_6_alpha_7.rs +++ b/core/startos/src/version/v0_3_6_alpha_7.rs @@ -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(()) diff --git a/core/startos/src/version/v0_3_6_alpha_8.rs b/core/startos/src/version/v0_3_6_alpha_8.rs index 92f96b4c4..2fc3f060c 100644 --- a/core/startos/src/version/v0_3_6_alpha_8.rs +++ b/core/startos/src/version/v0_3_6_alpha_8.rs @@ -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?; diff --git a/core/startos/registry.service b/core/startos/start-registryd.service similarity index 85% rename from core/startos/registry.service rename to core/startos/start-registryd.service index 63941a25e..e8e6390ba 100644 --- a/core/startos/registry.service +++ b/core/startos/start-registryd.service @@ -4,7 +4,7 @@ Description=StartOS Registry [Service] Type=simple Environment=RUST_LOG=startos=debug,patch_db=warn -ExecStart=/usr/local/bin/registry +ExecStart=/usr/bin/start-registryd Restart=always RestartSec=3 ManagedOOMPreference=avoid diff --git a/core/startos/start-tunneld.service b/core/startos/start-tunneld.service new file mode 100644 index 000000000..b0d0a2043 --- /dev/null +++ b/core/startos/start-tunneld.service @@ -0,0 +1,13 @@ +[Unit] +Description=StartTunnel + +[Service] +Type=simple +Environment=RUST_LOG=startos=debug,patch_db=warn +ExecStart=/usr/bin/start-tunneld +Restart=always +RestartSec=3 +ManagedOOMPreference=avoid + +[Install] +WantedBy=multi-user.target diff --git a/debian/start-registry/postinst b/debian/start-registry/postinst new file mode 100755 index 000000000..9994e3fe5 --- /dev/null +++ b/debian/start-registry/postinst @@ -0,0 +1,9 @@ +#!/bin/sh +set -e + +SYSTEMCTL=systemctl +if [ -n "$DPKG_MAINTSCRIPT_PACKAGE" ]; then + SYSTEMCTL=deb-systemd-helper +fi + +$SYSTEMCTL enable start-registryd.service diff --git a/debian/start-tunnel/postinst b/debian/start-tunnel/postinst new file mode 100755 index 000000000..542ff8cff --- /dev/null +++ b/debian/start-tunnel/postinst @@ -0,0 +1,9 @@ +#!/bin/sh +set -e + +SYSTEMCTL=systemctl +if [ -n "$DPKG_MAINTSCRIPT_PACKAGE" ]; then + SYSTEMCTL=deb-systemd-helper +fi + +$SYSTEMCTL enable start-tunneld.service diff --git a/debian/postinst b/debian/startos/postinst similarity index 100% rename from debian/postinst rename to debian/startos/postinst diff --git a/dpkg-build.sh b/dpkg-build.sh index e8ffdb0ac..e9a539d10 100755 --- a/dpkg-build.sh +++ b/dpkg-build.sh @@ -4,8 +4,9 @@ set -e cd "$(dirname "${BASH_SOURCE[0]}")" -BASENAME=$(./basename.sh) -VERSION=$(cat ./VERSION.txt) +PROJECT=${PROJECT:-"startos"} +BASENAME=${BASENAME:-"$(./basename.sh)"} +VERSION=${VERSION:-$(cat ./VERSION.txt)} if [ "$PLATFORM" = "x86_64" ] || [ "$PLATFORM" = "x86_64-nonfree" ]; then DEB_ARCH=amd64 elif [ "$PLATFORM" = "aarch64" ] || [ "$PLATFORM" = "aarch64-nonfree" ] || [ "$PLATFORM" = "raspberrypi" ]; then @@ -17,14 +18,34 @@ fi rm -rf dpkg-workdir/$BASENAME mkdir -p dpkg-workdir/$BASENAME -make install DESTDIR=dpkg-workdir/$BASENAME +if [ "${PROJECT}" = "startos" ]; then + INSTALL_TARGET="install" +else + INSTALL_TARGET="install-${PROJECT#start-}" +fi +make "${INSTALL_TARGET}" DESTDIR=dpkg-workdir/$BASENAME -DEPENDS=$(cat dpkg-workdir/$BASENAME/usr/lib/startos/depends | tr $'\n' ',' | sed 's/,,\+/,/g' | sed 's/,$//') -CONFLICTS=$(cat dpkg-workdir/$BASENAME/usr/lib/startos/conflicts | tr $'\n' ',' | sed 's/,,\+/,/g' | sed 's/,$//') +if [ -f dpkg-workdir/$BASENAME/usr/lib/$PROJECT/depends ]; then + if [ -n "$DEPENDS" ]; then + DEPENDS="$DEPENDS," + fi + DEPENDS="${DEPENDS}$(cat dpkg-workdir/$BASENAME/usr/lib/$PROJECT/depends | tr $'\n' ',' | sed 's/,,\+/,/g' | sed 's/,$//')" +fi +if [ -f dpkg-workdir/$BASENAME/usr/lib/$PROJECT/conflicts ]; then + if [ -n "$CONFLICTS" ]; then + CONFLICTS="$CONFLICTS," + fi + CONFLICTS="${CONFLICTS}$(cat dpkg-workdir/$BASENAME/usr/lib/$PROJECT/conflicts | tr $'\n' ',' | sed 's/,,\+/,/g' | sed 's/,$//')" +fi +CONFLICTS=${CONFLICTS:-"$(cat dpkg-workdir/$BASENAME/usr/lib/startos/conflicts | tr $'\n' ',' | sed 's/,,\+/,/g' | sed 's/,$//')"} -cp -r debian dpkg-workdir/$BASENAME/DEBIAN +if [ -d debian/${PROJECT} ]; then + cp -r debian/${PROJECT} dpkg-workdir/$BASENAME/DEBIAN +else + mkdir -p dpkg-workdir/$BASENAME/DEBIAN +fi cat > dpkg-workdir/$BASENAME/DEBIAN/control << EOF -Package: startos +Package: ${PROJECT} Version: ${VERSION} Section: unknown Priority: required diff --git a/sdk/package/lib/util/SubContainer.ts b/sdk/package/lib/util/SubContainer.ts index 31a691baa..afa7cc14a 100644 --- a/sdk/package/lib/util/SubContainer.ts +++ b/sdk/package/lib/util/SubContainer.ts @@ -616,6 +616,7 @@ export class SubContainerRc< return this.subcontainer.guid } private destroyed = false + private destroying: Promise | null = null public constructor( private readonly subcontainer: SubContainerOwned, ) { @@ -695,14 +696,16 @@ export class SubContainerRc< get destroy() { return async () => { - if (!this.destroyed) { + if (!this.destroyed && !this.destroying) { const rcs = --this.subcontainer.rcs if (rcs <= 0) { - await this.subcontainer.destroy() + this.destroying = this.subcontainer.destroy() if (rcs < 0) console.error(new Error("UNREACHABLE: rcs < 0").stack) } - this.destroyed = true } + await this.destroying + this.destroyed = true + this.destroying = null return null } }