diff --git a/build/dpkg-deps/depends b/build/dpkg-deps/depends index f495df85d6..4c2dbc5570 100644 --- a/build/dpkg-deps/depends +++ b/build/dpkg-deps/depends @@ -11,6 +11,7 @@ cryptsetup curl dnsutils dmidecode +dnsutils dosfstools e2fsprogs ecryptfs-utils @@ -57,4 +58,5 @@ systemd-timesyncd tor util-linux vim +wireguard-tools wireless-tools diff --git a/build/lib/scripts/dhclient-exit-hook b/build/lib/scripts/dhclient-exit-hook deleted file mode 100755 index 8c4a977465..0000000000 --- a/build/lib/scripts/dhclient-exit-hook +++ /dev/null @@ -1 +0,0 @@ -start-cli net dhcp update $interface \ No newline at end of file diff --git a/build/lib/scripts/enable-kiosk b/build/lib/scripts/enable-kiosk index 45bed5fe96..40753af405 100755 --- a/build/lib/scripts/enable-kiosk +++ b/build/lib/scripts/enable-kiosk @@ -4,7 +4,7 @@ set -e # install dependencies /usr/bin/apt update -/usr/bin/apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit firefox-esr matchbox-window-manager libnss3-tools +/usr/bin/apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit firefox-esr matchbox-window-manager libnss3-tools p11-kit-modules #Change a default preference set by stock debian firefox-esr sed -i 's|^pref("extensions.update.enabled", true);$|pref("extensions.update.enabled", false);|' /etc/firefox-esr/firefox-esr.js @@ -83,6 +83,9 @@ user_pref("toolkit.telemetry.updatePing.enabled", false); user_pref("toolkit.telemetry.cachedClientID", ""); EOF +cp /usr/lib/firefox-esr/libnssckbi.so /usr/lib/firefox-esr/libnssckbi.so.bak +ln -sf /usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox-esr/libnssckbi.so + # create kiosk script cat > /home/kiosk/kiosk.sh << 'EOF' #!/bin/sh diff --git a/core/Cargo.lock b/core/Cargo.lock index cdafe81a1f..16a0a0e7b1 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -254,6 +254,18 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "async-broadcast" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -261,8 +273,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue 2.5.0", - "event-listener", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue 2.5.0", + "event-listener-strategy", "futures-core", + "pin-project-lite", ] [[package]] @@ -279,6 +303,108 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue 2.5.0", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue 2.5.0", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel 2.3.1", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.3.1", + "futures-lite", + "rustix", + "tracing", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -301,6 +427,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.83" @@ -520,7 +652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be5951c75bdabb58753d140dd5802f12ff3a483cb2e16fb5276e111b94b19e87" dependencies = [ "concurrent-queue 1.2.4", - "event-listener", + "event-listener 2.5.3", "spin", ] @@ -707,6 +839,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brotli" version = "7.0.0" @@ -1676,6 +1821,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -1688,6 +1839,27 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "enumflags2" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1742,6 +1914,27 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue 2.5.0", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + [[package]] name = "exver" version = "0.2.0" @@ -1956,6 +2149,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -2038,6 +2244,17 @@ dependencies = [ "webpki-roots 0.26.6", ] +[[package]] +name = "getifaddrs" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba121d81ab5ea05b0cd5858516266800bf965531a794f7ac58e3eeb804f364f" +dependencies = [ + "bitflags 2.6.0", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -3029,7 +3246,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06cf485d4867e0714e35c1652e736bcf892d28fceecca01036764575db64ba84" dependencies = [ - "async-channel", + "async-channel 1.9.0", "futures", ] @@ -3233,6 +3450,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -3311,6 +3537,7 @@ dependencies = [ "tracing", "ts-rs", "yasi", + "zbus", ] [[package]] @@ -3381,6 +3608,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset 0.9.1", ] [[package]] @@ -3656,6 +3884,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "overload" version = "0.1.1" @@ -3706,6 +3944,12 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -3913,6 +4157,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -3940,6 +4195,21 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue 2.5.0", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "portable-atomic" version = "1.9.0" @@ -4836,6 +5106,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -5120,7 +5401,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -5406,6 +5687,7 @@ dependencies = [ "fd-lock-rs", "form_urlencoded", "futures", + "getifaddrs", "gpt", "helpers", "hex", @@ -5502,6 +5784,7 @@ dependencies = [ "url", "urlencoding", "uuid", + "zbus", "zeroize", ] @@ -6358,6 +6641,17 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "winapi", +] + [[package]] name = "unarray" version = "0.1.4" @@ -7001,6 +7295,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "xz2" version = "0.1.7" @@ -7067,6 +7371,69 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.3.1", + "futures-core", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.6.20", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.6.20", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -7178,3 +7545,45 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zvariant" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "winnow 0.6.20", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.87", + "winnow 0.6.20", +] diff --git a/core/models/Cargo.toml b/core/models/Cargo.toml index 44295745d6..b402cb2193 100644 --- a/core/models/Cargo.toml +++ b/core/models/Cargo.toml @@ -39,3 +39,4 @@ tokio = { version = "1", features = ["full"] } torut = { git = "https://github.com/Start9Labs/torut.git", branch = "update/dependencies" } tracing = "0.1.39" yasi = "0.1.5" +zbus = "5" diff --git a/core/models/src/errors.rs b/core/models/src/errors.rs index 2077a8bbdd..eba2a377f8 100644 --- a/core/models/src/errors.rs +++ b/core/models/src/errors.rs @@ -90,6 +90,7 @@ pub enum ErrorKind { Lxc = 72, Cancelled = 73, Git = 74, + DBus = 75, } impl ErrorKind { pub fn as_str(&self) -> &'static str { @@ -169,6 +170,7 @@ impl ErrorKind { Lxc => "LXC Error", Cancelled => "Cancelled", Git => "Git Error", + DBus => "DBus Error", } } } @@ -327,6 +329,11 @@ impl From for Error { Error::new(e, ErrorKind::Tor) } } +impl From for Error { + fn from(e: zbus::Error) -> Self { + Error::new(e, ErrorKind::DBus) + } +} impl From for Error { fn from(value: patch_db::value::Error) -> Self { match value.kind { diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index cba5bc6dce..a9d7bb1fca 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -96,6 +96,7 @@ exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", fd-lock-rs = "0.1.4" form_urlencoded = "1.2.1" futures = "0.3.28" +getifaddrs = "0.1.5" gpt = "3.1.0" helpers = { path = "../helpers" } hex = "0.4.3" @@ -216,6 +217,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"] } +zbus = "5.1.1" zeroize = "1.6.0" [profile.test] diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index 85978134d4..6cc81722e2 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -1,10 +1,10 @@ use std::collections::{BTreeMap, BTreeSet}; -use std::net::{Ipv4Addr, Ipv6Addr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use chrono::{DateTime, Utc}; use exver::{Version, VersionRange}; use imbl_value::InternedString; -use ipnet::{Ipv4Net, Ipv6Net}; +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use isocountry::CountryCode; use itertools::Itertools; use models::PackageId; @@ -54,7 +54,7 @@ impl Public { tor_address: format!("https://{}", account.tor_key.public().get_onion_address()) .parse() .unwrap(), - ip_info: BTreeMap::new(), + network_interfaces: BTreeMap::new(), acme: None, status_info: ServerStatus { backup_progress: None, @@ -130,7 +130,9 @@ pub struct ServerInfo { /// for backwards compatibility #[ts(type = "string")] pub tor_address: Url, - pub ip_info: BTreeMap, + #[ts(as = "BTreeMap::")] + #[serde(default)] + pub network_interfaces: BTreeMap, pub acme: Option, #[serde(default)] pub status_info: ServerStatus, @@ -151,31 +153,34 @@ pub struct ServerInfo { pub devices: Vec, } -#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] -pub struct IpInfo { - #[ts(type = "string | null")] - pub ipv4_range: Option, - pub ipv4: Option, - #[ts(type = "string | null")] - pub ipv6_range: Option, - pub ipv6: Option, +pub struct NetworkInterfaceInfo { + pub public: Option, + pub scope_id: Option, + pub ip_info: IpInfo, } -impl IpInfo { - pub async fn for_interface(iface: &str) -> Result { - let (ipv4, ipv4_range) = get_iface_ipv4_addr(iface).await?.unzip(); - let (ipv6, ipv6_range) = get_iface_ipv6_addr(iface).await?.unzip(); - Ok(Self { - ipv4_range, - ipv4, - ipv6_range, - ipv6, +impl NetworkInterfaceInfo { + pub fn public(&self) -> bool { + self.public.unwrap_or_else(|| { + !self.ip_info.0.iter().all(|ipnet| { + if let IpAddr::V4(ip4) = ipnet.addr() { + ip4.is_loopback() || ip4.is_private() || ip4.is_link_local() + } else { + true + } + }) }) } } +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, TS)] +#[ts(export)] +#[ts(type = "string[]")] +pub struct IpInfo(pub BTreeSet); + #[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index a81e7e3362..8bdff7ff2e 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -509,7 +509,6 @@ pub async fn init( enable_zram.complete(); update_server_info.start(); - server_info.ip_info = crate::net::dhcp::init_ips().await?; server_info.ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024; server_info.devices = lshw().await?; server_info.status_info = ServerStatus { diff --git a/core/startos/src/lxc/dev.rs b/core/startos/src/lxc/dev.rs index 248546d88b..a918672dab 100644 --- a/core/startos/src/lxc/dev.rs +++ b/core/startos/src/lxc/dev.rs @@ -8,13 +8,11 @@ use rpc_toolkit::{ use serde::{Deserialize, Serialize}; use ts_rs::TS; +use crate::context::{CliContext, RpcContext}; use crate::lxc::{ContainerId, LxcConfig}; use crate::prelude::*; use crate::rpc_continuations::Guid; -use crate::{ - context::{CliContext, RpcContext}, - service::ServiceStats, -}; +use crate::service::ServiceStats; pub fn lxc() -> ParentHandler { ParentHandler::new() diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index c0fb6eaba1..60f9f4301c 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -1,8 +1,9 @@ +use std::collections::BTreeSet; +use std::ffi::OsString; use std::net::Ipv4Addr; use std::path::Path; use std::sync::{Arc, Weak}; use std::time::Duration; -use std::{collections::BTreeSet, ffi::OsString}; use clap::builder::ValueParserFactory; use futures::{AsyncWriteExt, StreamExt}; diff --git a/core/startos/src/net/dhcp.rs b/core/startos/src/net/dhcp.rs deleted file mode 100644 index e323ba371e..0000000000 --- a/core/startos/src/net/dhcp.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::net::IpAddr; - -use clap::Parser; -use futures::TryStreamExt; -use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; -use serde::{Deserialize, Serialize}; -use tokio::sync::RwLock; -use ts_rs::TS; - -use crate::context::{CliContext, RpcContext}; -use crate::db::model::public::IpInfo; -use crate::net::utils::{iface_is_physical, list_interfaces}; -use crate::prelude::*; -use crate::Error; - -lazy_static::lazy_static! { - static ref CACHED_IPS: RwLock> = RwLock::new(BTreeSet::new()); -} - -async fn _ips() -> Result, Error> { - Ok(init_ips() - .await? - .values() - .flat_map(|i| { - std::iter::empty() - .chain(i.ipv4.map(IpAddr::from)) - .chain(i.ipv6.map(IpAddr::from)) - }) - .collect()) -} - -pub async fn ips() -> Result, Error> { - let ips = CACHED_IPS.read().await.clone(); - if !ips.is_empty() { - return Ok(ips); - } - let ips = _ips().await?; - *CACHED_IPS.write().await = ips.clone(); - Ok(ips) -} - -pub async fn init_ips() -> Result, Error> { - let mut res = BTreeMap::new(); - let mut ifaces = list_interfaces(); - while let Some(iface) = ifaces.try_next().await? { - if iface_is_physical(&iface).await { - let ip_info = IpInfo::for_interface(&iface).await?; - res.insert(iface, ip_info); - } - } - Ok(res) -} - -// #[command(subcommands(update))] -pub fn dhcp() -> ParentHandler { - ParentHandler::new().subcommand( - "update", - from_fn_async::<_, _, (), Error, (RpcContext, UpdateParams)>(update) - .no_display() - .with_about("Update IP assigned by dhcp") - .with_call_remote::(), - ) -} -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct UpdateParams { - interface: String, -} - -pub async fn update( - ctx: RpcContext, - UpdateParams { interface }: UpdateParams, -) -> Result<(), Error> { - if iface_is_physical(&interface).await { - let ip_info = IpInfo::for_interface(&interface).await?; - ctx.db - .mutate(|db| { - db.as_public_mut() - .as_server_info_mut() - .as_ip_info_mut() - .insert(&interface, &ip_info) - }) - .await?; - - let mut cached = CACHED_IPS.write().await; - if cached.is_empty() { - *cached = _ips().await?; - } else { - cached.extend( - std::iter::empty() - .chain(ip_info.ipv4.map(IpAddr::from)) - .chain(ip_info.ipv6.map(IpAddr::from)), - ); - } - } - Ok(()) -} diff --git a/core/startos/src/net/host/binding.rs b/core/startos/src/net/host/binding.rs index 174f0330f2..41261b7c61 100644 --- a/core/startos/src/net/host/binding.rs +++ b/core/startos/src/net/host/binding.rs @@ -41,12 +41,14 @@ impl FromStr for BindId { pub struct BindInfo { pub enabled: bool, pub options: BindOptions, - pub lan: LanInfo, + pub net: NetInfo, } + #[derive(Clone, Copy, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "camelCase")] #[ts(export)] -pub struct LanInfo { +pub struct NetInfo { + pub public: bool, pub assigned_port: Option, pub assigned_ssl_port: Option, } @@ -63,7 +65,8 @@ impl BindInfo { Ok(Self { enabled: true, options, - lan: LanInfo { + net: NetInfo { + public: false, assigned_port, assigned_ssl_port, }, @@ -74,7 +77,7 @@ impl BindInfo { available_ports: &mut AvailablePorts, options: BindOptions, ) -> Result { - let Self { mut lan, .. } = self; + let Self { net: mut lan, .. } = self; if options .secure .map_or(false, |s| !(s.ssl && options.add_ssl.is_some())) @@ -104,7 +107,7 @@ impl BindInfo { Ok(Self { enabled: true, options, - lan, + net: lan, }) } pub fn disable(&mut self) { diff --git a/core/startos/src/net/mod.rs b/core/startos/src/net/mod.rs index 53b94454db..53ba39887a 100644 --- a/core/startos/src/net/mod.rs +++ b/core/startos/src/net/mod.rs @@ -1,13 +1,13 @@ use rpc_toolkit::{Context, HandlerExt, ParentHandler}; pub mod acme; -pub mod dhcp; pub mod dns; pub mod forward; pub mod host; pub mod keys; pub mod mdns; pub mod net_controller; +pub mod network_interface; pub mod service_interface; pub mod ssl; pub mod static_server; @@ -25,12 +25,17 @@ pub fn net() -> ParentHandler { "tor", tor::tor::().with_about("Tor commands such as list-services, logs, and reset"), ) - .subcommand( - "dhcp", - dhcp::dhcp::().with_about("Command to update IP assigned from dhcp"), - ) + // .subcommand( + // "dhcp", + // network_interface::dhcp::().with_about("Command to update IP assigned from dhcp"), + // ) .subcommand( "acme", acme::acme::().with_about("Setup automatic clearnet certificate acquisition"), ) + .subcommand( + "network-interface", + network_interface::network_interface_api::() + .with_about("View and edit network interface configurations"), + ) } diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index a8beaf55fd..2d425cdfd3 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, Weak}; use color_eyre::eyre::eyre; use imbl::OrdMap; use imbl_value::InternedString; +use ipnet::IpNet; use models::{HostId, OptionExt, PackageId}; use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use tracing::instrument; @@ -15,8 +16,9 @@ use crate::hostname::Hostname; use crate::net::dns::DnsController; use crate::net::forward::LanPortForwardController; use crate::net::host::address::HostAddress; -use crate::net::host::binding::{AddSslOptions, BindId, BindOptions, LanInfo}; +use crate::net::host::binding::{AddSslOptions, BindId, BindOptions, NetInfo}; use crate::net::host::{host_for, Host, HostKind, Hosts}; +use crate::net::network_interface::NetworkInterfaceController; use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname}; use crate::net::tor::TorController; use crate::net::vhost::{AlpnInfo, VHostController}; @@ -28,6 +30,7 @@ pub struct PreInitNetController { pub db: TypedPatchDb, tor: TorController, vhost: VHostController, + net_iface: Arc, os_bindings: Vec>, server_hostnames: Vec>, } @@ -40,10 +43,12 @@ impl PreInitNetController { hostname: &Hostname, os_tor_key: TorSecretKeyV3, ) -> Result { + let net_iface = Arc::new(NetworkInterfaceController::new(db.clone())); let mut res = Self { db: db.clone(), tor: TorController::new(tor_control, tor_socks), - vhost: VHostController::new(db), + vhost: VHostController::new(db, net_iface.clone()), + net_iface, os_bindings: Vec::new(), server_hostnames: Vec::new(), }; @@ -75,26 +80,25 @@ impl PreInitNetController { ]; for hostname in self.server_hostnames.iter().cloned() { - self.os_bindings.push( - self.vhost - .add(hostname, 443, ([127, 0, 0, 1], 80).into(), alpn.clone()) - .await?, - ); + self.os_bindings.push(self.vhost.add( + hostname, + 443, + false, + ([127, 0, 0, 1], 80).into(), + alpn.clone(), + )?); } // Tor - self.os_bindings.push( - self.vhost - .add( - Some(InternedString::from_display( - &tor_key.public().get_onion_address(), - )), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); + self.os_bindings.push(self.vhost.add( + Some(InternedString::from_display( + &tor_key.public().get_onion_address(), + )), + 443, + false, + ([127, 0, 0, 1], 80).into(), + alpn.clone(), + )?); self.os_bindings.extend( self.tor .add( @@ -115,6 +119,7 @@ pub struct NetController { db: TypedPatchDb, pub(super) tor: TorController, pub(super) vhost: VHostController, + pub(super) net_iface: Arc, pub(super) dns: DnsController, pub(super) forward: LanPortForwardController, pub(super) os_bindings: Vec>, @@ -127,6 +132,7 @@ impl NetController { db, tor, vhost, + net_iface, os_bindings, server_hostnames, }: PreInitNetController, @@ -136,6 +142,7 @@ impl NetController { db, tor, vhost, + net_iface, dns: DnsController::init(dns_bind).await?, forward: LanPortForwardController::new(), os_bindings, @@ -172,7 +179,7 @@ struct HostBinds { lan: BTreeMap< u16, ( - LanInfo, + NetInfo, Option, BTreeSet, Vec>, @@ -270,7 +277,7 @@ impl NetService { // LAN let server_info = peek.as_public().as_server_info(); - let ip_info = server_info.as_ip_info().de()?; + let net_ifaces = server_info.as_network_interfaces().de()?; let hostname = server_info.as_hostname().de()?; for (port, bind) in &host.bindings { if !bind.enabled { @@ -280,10 +287,10 @@ impl NetService { let lan_bind = old_lan_bind .as_ref() .filter(|(external, ssl, _, _)| { - ssl == &bind.options.add_ssl && bind.lan == *external + ssl == &bind.options.add_ssl && bind.net == *external }) .cloned(); // only keep existing binding if relevant details match - if bind.lan.assigned_port.is_some() || bind.lan.assigned_ssl_port.is_some() { + if bind.net.assigned_port.is_some() || bind.net.assigned_ssl_port.is_some() { let new_lan_bind = if let Some(b) = lan_bind { b } else { @@ -291,7 +298,7 @@ impl NetService { let mut hostnames = BTreeSet::new(); if let Some(ssl) = &bind.options.add_ssl { let external = bind - .lan + .net .assigned_ssl_port .or_not_found("assigned ssl port")?; let target = (self.ip, *port).into(); @@ -305,53 +312,53 @@ impl NetService { } }; for hostname in ctrl.server_hostnames.iter().cloned() { - rcs.push( - ctrl.vhost - .add(hostname, external, target, connect_ssl.clone()) - .await?, - ); + rcs.push(ctrl.vhost.add( + hostname, + external, + bind.net.public, + target, + connect_ssl.clone(), + )?); } for address in host.addresses() { match address { HostAddress::Onion { address } => { let hostname = InternedString::from_display(address); if hostnames.insert(hostname.clone()) { - rcs.push( - ctrl.vhost - .add( - Some(hostname), - external, - target, - connect_ssl.clone(), - ) - .await?, - ); + rcs.push(ctrl.vhost.add( + Some(hostname), + external, + false, + target, + connect_ssl.clone(), + )?); } } HostAddress::Domain { address } => { if hostnames.insert(address.clone()) { let address = Some(address.clone()); - rcs.push( - ctrl.vhost - .add( - address.clone(), - external, - target, - connect_ssl.clone(), - ) - .await?, - ); + rcs.push(ctrl.vhost.add( + address.clone(), + external, + bind.net.public, + target, + connect_ssl.clone(), + )?); if ssl.preferred_external_port == 443 { - rcs.push( - ctrl.vhost - .add( - address.clone(), - 5443, - target, - connect_ssl.clone(), - ) - .await?, - ); + rcs.push(ctrl.vhost.add( + address.clone(), + 5443, + false, + target, + connect_ssl.clone(), + )?); + rcs.push(ctrl.vhost.add( + address.clone(), + 443, + true, + target, + connect_ssl.clone(), + )?); } } } @@ -363,67 +370,88 @@ impl NetService { // doesn't make sense to have 2 listening ports, both with ssl } else { let external = - bind.lan.assigned_port.or_not_found("assigned lan port")?; + bind.net.assigned_port.or_not_found("assigned lan port")?; rcs.push(ctrl.forward.add(external, (self.ip, *port).into()).await?); } } - (bind.lan, bind.options.add_ssl.clone(), hostnames, rcs) + (bind.net, bind.options.add_ssl.clone(), hostnames, rcs) }; let mut bind_hostname_info: Vec = hostname_info.remove(port).unwrap_or_default(); - for (interface, ip_info) in &ip_info { - bind_hostname_info.push(HostnameInfo::Ip { - network_interface_id: interface.clone(), - public: false, - hostname: IpHostname::Local { - value: InternedString::from_display(&{ - let hostname = &hostname; - lazy_format!("{hostname}.local") - }), - port: new_lan_bind.0.assigned_port, - ssl_port: new_lan_bind.0.assigned_ssl_port, - }, - }); + for (interface, iface_info) in &net_ifaces { + if !iface_info.public() { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: false, + hostname: IpHostname::Local { + value: InternedString::from_display(&{ + let hostname = &hostname; + lazy_format!("{hostname}.local") + }), + port: new_lan_bind.0.assigned_port, + ssl_port: new_lan_bind.0.assigned_ssl_port, + }, + }); + } for address in host.addresses() { if let HostAddress::Domain { address } = address { - if let Some(ssl) = &new_lan_bind.1 { - if ssl.preferred_external_port == 443 { + if new_lan_bind + .1 + .as_ref() + .map_or(false, |ssl| ssl.preferred_external_port == 443) + { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: true, // TODO: check if port forward is active + hostname: IpHostname::Domain { + domain: address.clone(), + subdomain: None, + port: None, + ssl_port: Some(443), + }, + }); + } else if iface_info.public() && new_lan_bind.0.public { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: iface_info.public(), + hostname: IpHostname::Domain { + domain: address.clone(), + subdomain: None, + port: new_lan_bind.0.assigned_port, + ssl_port: new_lan_bind.0.assigned_ssl_port, + }, + }); + } + } + } + if !iface_info.public() || new_lan_bind.0.public { + for ipnet in &iface_info.ip_info.0 { + match ipnet { + IpNet::V4(net) => { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: iface_info.public(), + hostname: IpHostname::Ipv4 { + value: net.addr(), + port: new_lan_bind.0.assigned_port, + ssl_port: new_lan_bind.0.assigned_ssl_port, + }, + }); + } + IpNet::V6(net) => { bind_hostname_info.push(HostnameInfo::Ip { network_interface_id: interface.clone(), - public: false, - hostname: IpHostname::Domain { - domain: address.clone(), - subdomain: None, - port: None, - ssl_port: Some(443), + public: iface_info.public(), + hostname: IpHostname::Ipv6 { + value: net.addr(), + port: new_lan_bind.0.assigned_port, + ssl_port: new_lan_bind.0.assigned_ssl_port, }, }); } } } } - if let Some(ipv4) = ip_info.ipv4 { - bind_hostname_info.push(HostnameInfo::Ip { - network_interface_id: interface.clone(), - public: false, - hostname: IpHostname::Ipv4 { - value: ipv4, - port: new_lan_bind.0.assigned_port, - ssl_port: new_lan_bind.0.assigned_ssl_port, - }, - }); - } - if let Some(ipv6) = ip_info.ipv6 { - bind_hostname_info.push(HostnameInfo::Ip { - network_interface_id: interface.clone(), - public: false, - hostname: IpHostname::Ipv6 { - value: ipv6, - port: new_lan_bind.0.assigned_port, - ssl_port: new_lan_bind.0.assigned_ssl_port, - }, - }); - } } hostname_info.insert(*port, bind_hostname_info); binds.lan.insert(*port, new_lan_bind); @@ -431,10 +459,10 @@ impl NetService { if let Some((lan, _, hostnames, _)) = old_lan_bind { if let Some(external) = lan.assigned_ssl_port { for hostname in ctrl.server_hostnames.iter().cloned() { - ctrl.vhost.gc(hostname, external).await?; + ctrl.vhost.gc(hostname, external)?; } for hostname in hostnames { - ctrl.vhost.gc(Some(hostname), external).await?; + ctrl.vhost.gc(Some(hostname), external)?; } } if let Some(external) = lan.assigned_port { @@ -455,10 +483,10 @@ impl NetService { for (lan, hostnames) in removed { if let Some(external) = lan.assigned_ssl_port { for hostname in ctrl.server_hostnames.iter().cloned() { - ctrl.vhost.gc(hostname, external).await?; + ctrl.vhost.gc(hostname, external)?; } for hostname in hostnames { - ctrl.vhost.gc(Some(hostname), external).await?; + ctrl.vhost.gc(Some(hostname), external)?; } } if let Some(external) = lan.assigned_port { @@ -481,7 +509,7 @@ impl NetService { SocketAddr::from((self.ip, *internal)), ); if let (Some(ssl), Some(ssl_internal)) = - (&info.options.add_ssl, info.lan.assigned_ssl_port) + (&info.options.add_ssl, info.net.assigned_ssl_port) { tor_binds.insert( ssl.preferred_external_port, @@ -580,7 +608,7 @@ impl NetService { self.ip } - pub fn get_lan_port(&self, host_id: HostId, internal_port: u16) -> Result { + pub fn get_lan_port(&self, host_id: HostId, internal_port: u16) -> Result { let host_id_binds = self.binds.get_key_value(&host_id); match host_id_binds { Some((_, binds)) => { diff --git a/core/startos/src/net/network_interface.rs b/core/startos/src/net/network_interface.rs new file mode 100644 index 0000000000..f4624bcfa4 --- /dev/null +++ b/core/startos/src/net/network_interface.rs @@ -0,0 +1,718 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::future::Future; +use std::net::{IpAddr, SocketAddr, SocketAddrV6}; +use std::pin::Pin; +use std::sync::{Arc, Weak}; +use std::task::Poll; + +use clap::Parser; +use futures::future::pending; +use futures::TryFutureExt; +use getifaddrs::if_nametoindex; +use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; +use ipnet::IpNet; +use itertools::Itertools; +use patch_db::json_ptr::JsonPointer; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::watch; +use tokio_stream::StreamExt; +use ts_rs::TS; +use zbus::proxy::PropertyStream; +use zbus::zvariant::{ + DeserializeDict, OwnedObjectPath, OwnedValue, Type as ZType, Value as ZValue, +}; +use zbus::{proxy, Connection}; + +use crate::context::{CliContext, RpcContext}; +use crate::db::model::public::{IpInfo, NetworkInterfaceInfo}; +use crate::db::model::Database; +use crate::prelude::*; +use crate::util::serde::{display_serializable, HandlerExtSerde}; +use crate::util::sync::SyncMutex; + +pub fn network_interface_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "list", + from_fn_async(list_interfaces) + .with_display_serializable() + .with_custom_display_fn(|HandlerArgs { params, .. }, res| { + use prettytable::*; + + if let Some(format) = params.format { + return Ok(display_serializable(format, res)); + } + + let mut table = Table::new(); + table.add_row(row![bc => "INTERFACE", "PUBLIC", "ADDRESSES"]); + for (iface, info) in res { + table.add_row(row![ + iface, + info.public(), + info.ip_info + .0 + .into_iter() + .map(|ipnet| match ipnet.addr() { + IpAddr::V4(ip) => format!("{ip}/{}", ipnet.prefix_len()), + IpAddr::V6(ip) => format!( + "[{ip}{}]/{}", + info.scope_id.map(|s| format!("%{s}")).unwrap_or_default(), + ipnet.prefix_len() + ), + }) + .join(", ") + ]); + } + + table.print_tty(false).unwrap(); + + Ok(()) + }) + .with_about("Show network interfaces StartOS can listen on") + .with_call_remote::(), + ) + .subcommand( + "set-public", + from_fn_async(set_public) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Indicate whether this interface is publicly addressable") + .with_call_remote::(), + ).subcommand( + "unset-public", + from_fn_async(unset_public) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Allow this interface to infer whether it is publicly addressable based on its IPv4 address") + .with_call_remote::(), + ) +} + +async fn list_interfaces( + ctx: RpcContext, +) -> Result, Error> { + Ok(ctx.net_controller.net_iface.ip_info.borrow().clone()) +} + +#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] +#[ts(export)] +struct SetPublicParams { + #[ts(type = "string")] + interface: InternedString, + public: Option, +} + +async fn set_public( + ctx: RpcContext, + SetPublicParams { interface, public }: SetPublicParams, +) -> Result<(), Error> { + ctx.net_controller + .net_iface + .set_public(&interface, Some(public.unwrap_or(true))) + .await +} + +#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] +#[ts(export)] +struct UnsetPublicParams { + #[ts(type = "string")] + interface: InternedString, +} + +async fn unset_public( + ctx: RpcContext, + UnsetPublicParams { interface }: UnsetPublicParams, +) -> Result<(), Error> { + ctx.net_controller + .net_iface + .set_public(&interface, None) + .await +} + +#[proxy( + interface = "org.freedesktop.NetworkManager", + default_service = "org.freedesktop.NetworkManager", + default_path = "/org/freedesktop/NetworkManager" +)] +trait NetworkManager { + #[zbus(property)] + fn devices(&self) -> Result, Error>; +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.Connection.Active", + default_service = "org.freedesktop.NetworkManager" +)] +trait ActiveConnection { + #[zbus(property)] + fn state_flags(&self) -> Result; + #[zbus(property, name = "Type")] + fn connection_type(&self) -> Result; +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.IP4Config", + default_service = "org.freedesktop.NetworkManager" +)] +trait Ip4Config { + #[zbus(property)] + fn address_data(&self) -> Result, Error>; +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.IP6Config", + default_service = "org.freedesktop.NetworkManager" +)] +trait Ip6Config { + #[zbus(property)] + fn address_data(&self) -> Result, Error>; +} + +#[derive(Clone, Debug, DeserializeDict, ZValue, ZType)] +#[zvariant(signature = "dict")] +struct AddressData { + address: String, + prefix: u32, +} +impl TryFrom> for IpInfo { + type Error = Error; + fn try_from(value: Vec) -> Result { + value + .into_iter() + .map(|a| { + IpNet::new(a.address.parse()?, a.prefix as u8).with_kind(ErrorKind::ParseNetAddress) + }) + .filter_ok(|ipnet| !ipnet.addr().is_unspecified() && !ipnet.addr().is_multicast()) + .collect::>() + .map(Self) + } +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.Device", + default_service = "org.freedesktop.NetworkManager" +)] +trait Device { + #[zbus(property)] + fn ip_interface(&self) -> Result; + + #[zbus(property)] + fn managed(&self) -> Result; + + #[zbus(property)] + fn active_connection(&self) -> Result; + + #[zbus(property)] + fn ip4_config(&self) -> Result; + + #[zbus(property)] + fn ip6_config(&self) -> Result; +} + +struct WatchPropertyStream<'a, T> { + stream: PropertyStream<'a, T>, + last: T, +} +impl<'a, T> WatchPropertyStream<'a, T> +where + T: Unpin + TryFrom, + T::Error: Into, +{ + fn new(stream: PropertyStream<'a, T>, first: T) -> Self { + Self { + stream, + last: first, + } + } + async fn until_changed>>( + &mut self, + fut: Fut, + ) -> Result<(), Error> { + let next = self.stream.next(); + tokio::select! { + changed = next => { + self.last = changed.ok_or_else(|| Error::new(eyre!("stream is empty"), ErrorKind::DBus))?.get().await?; + Ok(()) + }, + res = fut.and_then(|_| pending()) => { + res + } + } + } +} + +#[instrument(skip_all)] +async fn watcher(write_to: watch::Sender>) { + loop { + let res: Result<(), Error> = async { + let connection = Connection::system().await?; + let netman_proxy = NetworkManagerProxy::new(&connection).await?; + + let mut devices_sub = WatchPropertyStream::new( + netman_proxy.receive_devices_changed().await, + netman_proxy.devices().await?, + ); + + loop { + let devices = devices_sub.last.clone(); + devices_sub + .until_changed(async { + let mut ifaces = BTreeSet::new(); + let mut jobs = Vec::new(); + for device in devices { + let device_proxy = + DeviceProxy::new(&connection, device.clone()).await?; + let iface = InternedString::intern(device_proxy.ip_interface().await?); + if iface.is_empty() { + continue; + } + let managed = device_proxy.managed().await?; + if !managed { + continue; + } + let dac = device_proxy.active_connection().await?; + if &*dac == "/" { + continue; + } + let ac_proxy = ActiveConnectionProxy::new(&connection, dac).await?; + let external = ac_proxy.state_flags().await? & 0x80 != 0; + if external && iface != "lo" { + continue; + } + jobs.push(watch_ip( + &connection, + device_proxy.clone(), + iface.clone(), + &write_to, + )); + ifaces.insert(iface); + } + + write_to.send_if_modified(|m| { + let mut changed = false; + m.retain(|i, _| { + if ifaces.contains(i) { + true + } else { + changed |= true; + false + } + }); + changed + }); + futures::future::try_join_all(jobs).await?; + + Ok::<_, Error>(()) + }) + .await?; + } + } + .await; + if let Err(e) = res { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + } + } +} + +#[instrument(skip_all)] +async fn watch_ip( + connection: &Connection, + device_proxy: DeviceProxy<'_>, + iface: InternedString, + write_to: &watch::Sender>, +) -> Result<(), Error> { + let mut ip4_config_sub = WatchPropertyStream::new( + device_proxy.receive_ip4_config_changed().await, + device_proxy.ip4_config().await?, + ); + let mut ip6_config_sub = WatchPropertyStream::new( + device_proxy.receive_ip6_config_changed().await, + device_proxy.ip6_config().await?, + ); + + loop { + let ip4_config = ip4_config_sub.last.clone(); + let ip6_config = ip6_config_sub.last.clone(); + ip4_config_sub + .until_changed(ip6_config_sub.until_changed(async { + let ip4_proxy = Ip4ConfigProxy::new(&connection, ip4_config).await?; + let mut address4_sub = WatchPropertyStream::new( + ip4_proxy.receive_address_data_changed().await, + ip4_proxy.address_data().await?, + ); + let ip6_proxy = Ip6ConfigProxy::new(&connection, ip6_config).await?; + let mut address6_sub = WatchPropertyStream::new( + ip6_proxy.receive_address_data_changed().await, + ip6_proxy.address_data().await?, + ); + + loop { + let addresses = address4_sub + .last + .iter() + .cloned() + .chain(address6_sub.last.iter().cloned()) + .collect_vec(); + address4_sub + .until_changed(address6_sub.until_changed(async { + let ip_info: IpInfo = addresses.try_into()?; + let scope_id = + Some(if_nametoindex(&*iface).with_kind(ErrorKind::Network)?); + + write_to.send_if_modified(|m| { + let public = m.get(&iface).map_or(None, |i| i.public); + m.insert( + iface.clone(), + NetworkInterfaceInfo { + public, + scope_id, + ip_info: ip_info.clone(), + }, + ) + .filter(|old| &old.ip_info == &ip_info && old.scope_id == scope_id) + .is_none() + }); + + Ok::<_, Error>(()) + })) + .await?; + } + })) + .await?; + } +} + +pub struct NetworkInterfaceController { + db: TypedPatchDb, + ip_info: watch::Sender>, + _watcher: NonDetachingJoinHandle<()>, + listeners: SyncMutex>>, +} +impl NetworkInterfaceController { + async fn sync( + db: &TypedPatchDb, + ip_info: &BTreeMap, + ) -> Result<(), Error> { + db.mutate(|db| { + let ifaces_model = db + .as_public_mut() + .as_server_info_mut() + .as_network_interfaces_mut(); + let mut keep = BTreeSet::new(); + for (iface, ip_info) in ip_info { + keep.insert(iface); + ifaces_model + .upsert(&iface, || Ok(NetworkInterfaceInfo::default()))? + .ser(&ip_info)?; + } + for iface in ifaces_model.keys()? { + if !keep.contains(&&iface) { + if let Some(info) = ifaces_model.as_idx_mut(&iface) { + info.as_ip_info_mut().ser(&IpInfo::default())?; + } + } + } + Ok(()) + }) + .await?; + Ok(()) + } + pub fn new(db: TypedPatchDb) -> Self { + let (write_to, mut read_from) = watch::channel(BTreeMap::new()); + Self { + db: db.clone(), + ip_info: write_to.clone(), + _watcher: tokio::spawn(async move { + match db + .peek() + .await + .as_public() + .as_server_info() + .as_network_interfaces() + .de() + { + Ok(info) => { + write_to.send_replace(info); + } + Err(e) => { + tracing::error!("Error loading network interface info: {e}"); + tracing::debug!("{e:?}"); + } + }; + tokio::join!(watcher(write_to), async { + loop { + if let Err(e) = async { + let ip_info = read_from.borrow().clone(); + Self::sync(&db, &ip_info).await?; + + read_from.changed().await.map_err(|_| { + Error::new( + eyre!("NetworkManager watch thread exited"), + ErrorKind::Network, + ) + })?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("Error syncing ip info to db: {e}"); + tracing::debug!("{e:?}"); + } + } + }); + }) + .into(), + listeners: SyncMutex::new(BTreeMap::new()), + } + } + + pub fn bind(&self, port: u16) -> Result { + let arc = Arc::new(()); + self.listeners.mutate(|l| { + if l.get(&port).filter(|w| w.strong_count() > 0).is_some() { + return Err(Error::new( + std::io::Error::from_raw_os_error(libc::EADDRINUSE), + ErrorKind::Network, + )); + } + l.insert(port, Arc::downgrade(&arc)); + Ok(()) + })?; + Ok(NetworkInterfaceListener { + _arc: arc, + ip_info: self.ip_info.subscribe(), + listeners: ListenerMap::new(port), + needs_update: true, + }) + } + + pub async fn set_public( + &self, + interface: &InternedString, + public: Option, + ) -> Result<(), Error> { + let mut sub = self + .db + .subscribe( + "/public/serverInfo/networkInterfaces" + .parse::>() + .with_kind(ErrorKind::Database)?, + ) + .await; + let mut err = None; + let changed = self.ip_info.send_if_modified(|ip_info| { + let prev = std::mem::replace( + &mut match ip_info.get_mut(interface).or_not_found(interface) { + Ok(a) => a, + Err(e) => { + err = Some(e); + return false; + } + } + .public, + public, + ); + prev == public + }); + if let Some(e) = err { + return Err(e); + } + if changed { + sub.recv().await; + } + Ok(()) + } +} + +struct ListenerMap { + port: u16, + listeners: BTreeMap<(IpAddr, u32), (TcpListener, bool)>, +} +impl ListenerMap { + fn new(port: u16) -> Self { + Self { + port, + listeners: BTreeMap::new(), + } + } + async fn update( + &mut self, + ip_info: &BTreeMap, + public: bool, + ) -> Result<(), Error> { + let mut keep = BTreeSet::<(IpAddr, u32)>::new(); + for info in ip_info.values() { + if public || !info.public() { + for ipnet in &info.ip_info.0 { + let scope_id = info.scope_id.unwrap_or(0); + let key = (ipnet.addr(), scope_id); + keep.insert(key); + if let Some((_, is_public)) = self.listeners.get_mut(&key) { + *is_public = info.public(); + continue; + } + self.listeners.insert( + key, + ( + TcpListener::bind(match ipnet.addr() { + IpAddr::V6(ip6) => { + SocketAddrV6::new(ip6, self.port, 0, scope_id).into() + } + ip => SocketAddr::new(ip, self.port), + }) + .await?, + info.public(), + ), + ); + } + } + } + self.listeners.retain(|key, _| keep.contains(key)); + Ok(()) + } + fn accept(&mut self) -> ListenerMapFut { + ListenerMapFut(&mut self.listeners) + } +} +#[pin_project::pin_project] +struct ListenerMapFut<'a>(&'a mut BTreeMap<(IpAddr, u32), (TcpListener, bool)>); +impl<'a> Future for ListenerMapFut<'a> { + type Output = Result<(IpAddr, bool, TcpStream, SocketAddr), Error>; + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let this = self.project(); + for ((ip, _), listener) in this.0.iter() { + if let Poll::Ready((stream, addr)) = listener.0.poll_accept(cx)? { + return Poll::Ready(Ok((*ip, listener.1, stream, addr))); + } + } + Poll::Pending + } +} + +pub struct NetworkInterfaceListener { + needs_update: bool, + ip_info: watch::Receiver>, + listeners: ListenerMap, + _arc: Arc<()>, +} +impl NetworkInterfaceListener { + pub async fn accept(&mut self, public: bool) -> Result { + loop { + if self.needs_update { + let ip_info = self.ip_info.borrow().clone(); + self.listeners.update(&ip_info, public).await?; + self.needs_update = false; + } + tokio::select! { + accepted = self.listeners.accept() => { + let (ip, is_public, stream, peer) = accepted?; + return Ok(Accepted { + stream, + peer, + is_public, + bind: (ip, self.listeners.port).into(), + }) + }, + res = self.ip_info.changed() => { + res.map_err(|_| Error::new( + eyre!("NetworkManager watch thread exited"), + ErrorKind::Network, + ))?; + self.needs_update = true; + } + } + } + } +} + +pub struct Accepted { + pub stream: TcpStream, + pub peer: SocketAddr, + pub is_public: bool, + pub bind: SocketAddr, +} + +// async fn _ips() -> Result, Error> { +// Ok(init_ips() +// .await? +// .values() +// .flat_map(|i| { +// std::iter::empty() +// .chain(i.ipv4.map(IpAddr::from)) +// .chain(i.ipv6.map(IpAddr::from)) +// }) +// .collect()) +// } + +// pub async fn ips() -> Result, Error> { +// let ips = CACHED_IPS.read().await.clone(); +// if !ips.is_empty() { +// return Ok(ips); +// } +// let ips = _ips().await?; +// *CACHED_IPS.write().await = ips.clone(); +// Ok(ips) +// } + +// pub async fn init_ips() -> Result, Error> { +// let mut res = BTreeMap::new(); +// let mut ifaces = list_interfaces(); +// while let Some(iface) = ifaces.try_next().await? { +// if iface_is_physical(&iface).await { +// let ip_info = IpInfo::for_interface(&iface).await?; +// res.insert(iface, ip_info); +// } +// } +// Ok(res) +// } + +// // #[command(subcommands(update))] +// pub fn dhcp() -> ParentHandler { +// ParentHandler::new().subcommand( +// "update", +// from_fn_async::<_, _, (), Error, (RpcContext, UpdateParams)>(update) +// .no_display() +// .with_about("Update IP assigned by dhcp") +// .with_call_remote::(), +// ) +// } +// #[derive(Deserialize, Serialize, Parser, TS)] +// #[serde(rename_all = "camelCase")] +// #[command(rename_all = "kebab-case")] +// pub struct UpdateParams { +// interface: String, +// } + +// pub async fn update( +// ctx: RpcContext, +// UpdateParams { interface }: UpdateParams, +// ) -> Result<(), Error> { +// if iface_is_physical(&interface).await { +// let ip_info = IpInfo::for_interface(&interface).await?; +// ctx.db +// .mutate(|db| { +// db.as_public_mut() +// .as_server_info_mut() +// .as_ip_info_mut() +// .insert(&interface, &ip_info) +// }) +// .await?; + +// let mut cached = CACHED_IPS.write().await; +// if cached.is_empty() { +// *cached = _ips().await?; +// } else { +// cached.extend( +// std::iter::empty() +// .chain(ip_info.ipv4.map(IpAddr::from)) +// .chain(ip_info.ipv6.map(IpAddr::from)), +// ); +// } +// } +// Ok(()) +// } diff --git a/core/startos/src/net/service_interface.rs b/core/startos/src/net/service_interface.rs index ade10d9594..b0a8a7676d 100644 --- a/core/startos/src/net/service_interface.rs +++ b/core/startos/src/net/service_interface.rs @@ -12,7 +12,8 @@ use ts_rs::TS; #[serde(tag = "kind")] pub enum HostnameInfo { Ip { - network_interface_id: String, + #[ts(type = "string")] + network_interface_id: InternedString, public: bool, hostname: IpHostname, }, diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index 7d48b14690..9f662f7904 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -32,11 +32,14 @@ use ts_rs::TS; use crate::db::model::Database; use crate::net::acme::AcmeCertCache; +use crate::net::network_interface::{ + Accepted, NetworkInterfaceController, NetworkInterfaceListener, +}; use crate::net::static_server::server_error; use crate::prelude::*; use crate::util::io::BackTrackingIO; -use crate::util::sync::SyncMutex; use crate::util::serde::MaybeUtf8String; +use crate::util::sync::SyncMutex; #[derive(Debug)] struct SingleCertResolver(Arc); @@ -49,60 +52,69 @@ impl ResolvesServerCert for SingleCertResolver { // not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353 pub struct VHostController { - crypto_provider: Arc, db: TypedPatchDb, - servers: Mutex>, + interfaces: Arc, + crypto_provider: Arc, + servers: SyncMutex>, } impl VHostController { - pub fn new(db: TypedPatchDb) -> Self { + pub fn new(db: TypedPatchDb, interfaces: Arc) -> Self { Self { - crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()), db, - servers: Mutex::new(BTreeMap::new()), + interfaces, + crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()), + servers: SyncMutex::new(BTreeMap::new()), } } #[instrument(skip_all)] - pub async fn add( + pub fn add( &self, hostname: Option, external: u16, + public: bool, target: SocketAddr, connect_ssl: Result<(), AlpnInfo>, // Ok: yes, connect using ssl, pass through alpn; Err: connect tcp, use provided strategy for alpn ) -> Result, Error> { - let mut writable = self.servers.lock().await; - let server = if let Some(server) = writable.remove(&external) { - server - } else { - tracing::info!("Listening on {external}"); - VHostServer::new(external, self.db.clone(), self.crypto_provider.clone()).await? - }; - let rc = server - .add( + self.servers.mutate(|writable| { + let server = if let Some(server) = writable.remove(&external) { + server + } else { + VHostServer::new( + external, + self.db.clone(), + self.interfaces.clone(), + self.crypto_provider.clone(), + )? + }; + let rc = server.add( hostname, TargetInfo { + public, addr: target, connect_ssl, }, - ) - .await; - writable.insert(external, server); - Ok(rc?) + ); + writable.insert(external, server); + Ok(rc?) + }) } #[instrument(skip_all)] - pub async fn gc(&self, hostname: Option, external: u16) -> Result<(), Error> { - let mut writable = self.servers.lock().await; - if let Some(server) = writable.remove(&external) { - server.gc(hostname).await?; - if !server.is_empty().await? { - writable.insert(external, server); + pub fn gc(&self, hostname: Option, external: u16) -> Result<(), Error> { + self.servers.mutate(|writable| { + if let Some(server) = writable.remove(&external) { + server.gc(hostname)?; + if !server.is_empty()? { + writable.insert(external, server); + } } - } - Ok(()) + Ok(()) + }) } } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] struct TargetInfo { + public: bool, addr: SocketAddr, connect_ssl: Result<(), AlpnInfo>, } @@ -120,421 +132,461 @@ impl Default for AlpnInfo { } } +type AcmeTlsAlpnCache = + Arc>>>>>; +type Mapping = SyncMutex, BTreeMap>>>; + struct VHostServer { - mapping: Weak, BTreeMap>>>>, + mapping: Weak, _thread: NonDetachingJoinHandle<()>, } + impl VHostServer { - #[instrument(skip_all)] - async fn new(port: u16, db: TypedPatchDb, crypto_provider: Arc) -> Result { - let acme_tls_alpn_cache = Arc::new(SyncMutex::new(BTreeMap::< - InternedString, - watch::Receiver>>, - >::new())); - // check if port allowed - let listener = TcpListener::bind(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), port)) - .await - .with_kind(crate::ErrorKind::Network)?; - let mapping = Arc::new(RwLock::new(BTreeMap::new())); - Ok(Self { - mapping: Arc::downgrade(&mapping), - _thread: tokio::spawn(async move { - loop { - match listener.accept().await { - Ok((stream, _)) => { - if let Err(e) = socket2::SockRef::from(&stream).set_tcp_keepalive( - &socket2::TcpKeepalive::new() - .with_time(Duration::from_secs(900)) - .with_interval(Duration::from_secs(60)) - .with_retries(5), - ) { - tracing::error!("Failed to set tcp keepalive: {e}"); - tracing::debug!("{e:?}"); - } + async fn accept( + listener: &mut NetworkInterfaceListener, + mapping: Arc, + db: TypedPatchDb, + acme_tls_alpn_cache: AcmeTlsAlpnCache, + crypto_provider: Arc, + ) -> Result<(), Error> { + let any_public = mapping.peek(|m| { + m.iter() + .any(|(_, targets)| targets.keys().any(|target| target.public)) + }); + let accepted = listener.accept(any_public).await?; + if let Err(e) = socket2::SockRef::from(&accepted.stream).set_tcp_keepalive( + &socket2::TcpKeepalive::new() + .with_time(Duration::from_secs(900)) + .with_interval(Duration::from_secs(60)) + .with_retries(5), + ) { + tracing::error!("Failed to set tcp keepalive: {e}"); + tracing::debug!("{e:?}"); + } - let mut stream = BackTrackingIO::new(stream); - let mapping = mapping.clone(); - let db = db.clone(); - let acme_tls_alpn_cache = acme_tls_alpn_cache.clone(); - let crypto_provider = crypto_provider.clone(); - tokio::spawn(async move { - if let Err(e) = async { - let mid: tokio_rustls::StartHandshake<&mut BackTrackingIO> = match LazyConfigAcceptor::new( - Acceptor::default(), - &mut stream, - ) - .await - { - Ok(a) => a, - Err(_) => { - stream.rewind(); - return hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new()) - .serve_connection( - hyper_util::rt::TokioIo::new(stream), - hyper_util::service::TowerToHyperService::new(axum::Router::new().fallback( - axum::routing::method_routing::any(move |req: Request| async move { - match async move { - let host = req - .headers() - .get(http::header::HOST) - .and_then(|host| host.to_str().ok()); - let uri = Uri::from_parts({ - let mut parts = req.uri().to_owned().into_parts(); - parts.scheme = Some("https".parse()?); - parts.authority = host.map(FromStr::from_str).transpose()?; - parts - })?; - Response::builder() - .status(http::StatusCode::TEMPORARY_REDIRECT) - .header(http::header::LOCATION, uri.to_string()) - .body(Body::default()) - }.await { - Ok(a) => a, - Err(e) => { - tracing::warn!("Error redirecting http request on ssl port: {e}"); - tracing::error!("{e:?}"); - server_error(Error::new(e, ErrorKind::Network)) - } - } - }), - )), - ) - .await - .map_err(|e| Error::new(color_eyre::eyre::Report::msg(e), ErrorKind::Network)); - } - }; - let target_name = - mid.client_hello().server_name().map(|s| s.into()); - let target = { - let mapping = mapping.read().await; - mapping - .get(&target_name) - .into_iter() - .flatten() - .find(|(_, rc)| rc.strong_count() > 0) - .or_else(|| { - if target_name - .as_ref() - .map(|s| s.parse::().is_ok()) - .unwrap_or(true) - { - mapping - .get(&None) - .into_iter() - .flatten() - .find(|(_, rc)| rc.strong_count() > 0) - } else { - None - } - }) - .map(|(target, _)| target.clone()) - }; - if let Some(target) = target { - let peek = db.peek().await; - let root = peek.as_private().as_key_store().as_local_certs().as_root_cert().de()?; - let mut cfg = match async { - if let Some(acme_settings) = peek.as_public().as_server_info().as_acme().de()? { - if let Some(domain) = target_name.as_ref().filter(|target_name| acme_settings.domains.contains(*target_name)) { - if mid - .client_hello() - .alpn() - .into_iter() - .flatten() - .any(|alpn| alpn == ACME_TLS_ALPN_NAME) - { - let cert = WatchStream::new( - acme_tls_alpn_cache.peek(|c| c.get(&**domain).cloned()) - .ok_or_else(|| { - Error::new( - eyre!("No challenge recv available for {domain}"), - ErrorKind::OpenSsl - ) - })?, - ); - tracing::info!("Waiting for verification cert for {domain}"); - let cert = cert - .filter(|c| c.is_some()) - .next() - .await - .flatten() - .ok_or_else(|| { - Error::new(eyre!("No challenge available for {domain}"), ErrorKind::OpenSsl) - })?; - tracing::info!("Verification cert received for {domain}"); - let mut cfg = ServerConfig::builder_with_provider(crypto_provider.clone()) - .with_safe_default_protocol_versions() - .with_kind(crate::ErrorKind::OpenSsl)? - .with_no_client_auth() - .with_cert_resolver(Arc::new(SingleCertResolver(cert))); + tokio::spawn(async move { + let bind = accepted.bind; + if let Err(e) = + Self::handle_stream(accepted, mapping, db, acme_tls_alpn_cache, crypto_provider) + .await + { + tracing::error!("Error in VHostController on {bind}: {e}"); + tracing::debug!("{e:?}") + } + }); + Ok(()) + } - cfg.alpn_protocols = vec![ACME_TLS_ALPN_NAME.to_vec()]; - return Ok(Err(cfg)); - } else { - let domains = [domain.to_string()]; - let (send, recv) = watch::channel(None); - acme_tls_alpn_cache.mutate(|c| c.insert(domain.clone(), recv)); - let cert = - async_acme::rustls_helper::order( - |_, cert| { - send.send_replace(Some(Arc::new(cert))); - Ok(()) - }, - acme_settings.provider.as_str(), - &domains, - Some(&AcmeCertCache(&db)), - &acme_settings.contact, - ) - .await - .with_kind(ErrorKind::OpenSsl)?; - return Ok(Ok( - ServerConfig::builder_with_provider(crypto_provider.clone()) - .with_safe_default_protocol_versions() - .with_kind(crate::ErrorKind::OpenSsl)? - .with_no_client_auth() - .with_cert_resolver(Arc::new(SingleCertResolver(Arc::new(cert)))) - )); - } - } - } - let hostnames = target_name - .into_iter() - .chain( - peek - .as_public() - .as_server_info() - .as_ip_info() - .as_entries()? - .into_iter() - .flat_map(|(_, ips)| [ - ips.as_ipv4().de().map(|ip| ip.map(IpAddr::V4)), - ips.as_ipv6().de().map(|ip| ip.map(IpAddr::V6)) - ]) - .filter_map(|a| a.transpose()) - .map(|a| a.map(|ip| InternedString::from_display(&ip))) - .collect::, _>>()?, - ) - .collect(); - let key = db - .mutate(|v| { - v.as_private_mut() - .as_key_store_mut() - .as_local_certs_mut() - .cert_for(&hostnames) - }) - .await?; - let cfg = ServerConfig::builder_with_provider(crypto_provider.clone()) - .with_safe_default_protocol_versions() - .with_kind(crate::ErrorKind::OpenSsl)? - .with_no_client_auth(); - if mid.client_hello().signature_schemes().contains( - &tokio_rustls::rustls::SignatureScheme::ED25519, - ) { - cfg.with_single_cert( - key.fullchain_ed25519() - .into_iter() - .map(|c| { - Ok(tokio_rustls::rustls::pki_types::CertificateDer::from( - c.to_der()?, - )) - }) - .collect::>()?, - PrivateKeyDer::from(PrivatePkcs8KeyDer::from( - key.leaf - .keys - .ed25519 - .private_key_to_pkcs8()?, - )), - ) - } else { - cfg.with_single_cert( - key.fullchain_nistp256() - .into_iter() - .map(|c| { - Ok(tokio_rustls::rustls::pki_types::CertificateDer::from( - c.to_der()?, - )) - }) - .collect::>()?, - PrivateKeyDer::from(PrivatePkcs8KeyDer::from( - key.leaf - .keys - .nistp256 - .private_key_to_pkcs8()?, - )), - ) - } - .with_kind(crate::ErrorKind::OpenSsl) - .map(Ok) - }.await? { + async fn handle_stream( + Accepted { + stream, + is_public, + bind, + .. + }: Accepted, + mapping: Arc, + db: TypedPatchDb, + acme_tls_alpn_cache: AcmeTlsAlpnCache, + crypto_provider: Arc, + ) -> Result<(), Error> { + let mut stream = BackTrackingIO::new(stream); + let mid: tokio_rustls::StartHandshake<&mut BackTrackingIO> = + match LazyConfigAcceptor::new(Acceptor::default(), &mut stream).await { + Ok(a) => a, + Err(e) => { + let (_, buf) = stream.rewind(); + if std::str::from_utf8(buf) + .ok() + .and_then(|buf| { + buf.lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .next() + }) + .map_or(false, |buf| { + regex::Regex::new("[A-Z]+ (.+) HTTP/1") + .unwrap() + .is_match(buf) + }) + { + return hyper_util::server::conn::auto::Builder::new( + hyper_util::rt::TokioExecutor::new(), + ) + .serve_connection( + hyper_util::rt::TokioIo::new(stream), + hyper_util::service::TowerToHyperService::new( + axum::Router::new().fallback(axum::routing::method_routing::any( + move |req: Request| async move { + match async move { + let host = req + .headers() + .get(http::header::HOST) + .and_then(|host| host.to_str().ok()); + let uri = Uri::from_parts({ + let mut parts = req.uri().to_owned().into_parts(); + parts.scheme = Some("https".parse()?); + parts.authority = + host.map(FromStr::from_str).transpose()?; + parts + })?; + Response::builder() + .status(http::StatusCode::TEMPORARY_REDIRECT) + .header(http::header::LOCATION, uri.to_string()) + .body(Body::default()) + } + .await + { Ok(a) => a, - Err(cfg) => { - tracing::info!("performing ACME auth challenge"); - let mut accept = mid.into_stream(Arc::new(cfg)); - let io = accept.get_mut().unwrap(); - let buffered = io.stop_buffering(); - io.write_all(&buffered).await?; - accept.await?; - tracing::info!("ACME auth challenge completed"); - return Ok(()); - } - }; - let mut tcp_stream = - TcpStream::connect(target.addr).await?; - match target.connect_ssl { - Ok(()) => { - let mut client_cfg = - tokio_rustls::rustls::ClientConfig::builder_with_provider(crypto_provider) - .with_safe_default_protocol_versions() - .with_kind(crate::ErrorKind::OpenSsl)? - .with_root_certificates({ - let mut store = RootCertStore::empty(); - store.add( - CertificateDer::from( - root.to_der()?, - ), - ).with_kind(crate::ErrorKind::OpenSsl)?; - store - }) - .with_no_client_auth(); - client_cfg.alpn_protocols = mid - .client_hello() - .alpn() - .into_iter() - .flatten() - .map(|x| x.to_vec()) - .collect(); - let mut target_stream = - TlsConnector::from(Arc::new(client_cfg)) - .connect_with( - ServerName::IpAddress( - target.addr.ip().into(), - ), - tcp_stream, - |conn| { - cfg.alpn_protocols.extend( - conn.alpn_protocol() - .into_iter() - .map(|p| p.to_vec()), - ) - }, - ) - .await - .with_kind(crate::ErrorKind::OpenSsl)?; - let mut accept = mid.into_stream(Arc::new(cfg)); - let io = accept.get_mut().unwrap(); - let buffered = io.stop_buffering(); - io.write_all(&buffered).await?; - let mut tls_stream = - match accept.await { - Ok(a) => a, - Err(e) => { - tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); - tracing::trace!("{e:?}"); - return Ok(()) - } - }; - tokio::io::copy_bidirectional( - &mut tls_stream, - &mut target_stream, - ) - .await - } - Err(AlpnInfo::Reflect) => { - for proto in - mid.client_hello().alpn().into_iter().flatten() - { - cfg.alpn_protocols.push(proto.into()); - } - let mut accept = mid.into_stream(Arc::new(cfg)); - let io = accept.get_mut().unwrap(); - let buffered = io.stop_buffering(); - io.write_all(&buffered).await?; - let mut tls_stream = - match accept.await { - Ok(a) => a, - Err(e) => { - tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); - tracing::trace!("{e:?}"); - return Ok(()) - } - }; - tokio::io::copy_bidirectional( - &mut tls_stream, - &mut tcp_stream, - ) - .await - } - Err(AlpnInfo::Specified(alpn)) => { - cfg.alpn_protocols = alpn.into_iter().map(|a| a.0).collect(); - let mut accept = mid.into_stream(Arc::new(cfg)); - let io = accept.get_mut().unwrap(); - let buffered = io.stop_buffering(); - io.write_all(&buffered).await?; - let mut tls_stream = - match accept.await { - Ok(a) => a, - Err(e) => { - tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); - tracing::trace!("{e:?}"); - return Ok(()) - } - }; - tokio::io::copy_bidirectional( - &mut tls_stream, - &mut tcp_stream, - ) - .await + Err(e) => { + tracing::warn!( + "Error redirecting http request on ssl port: {e}" + ); + tracing::error!("{e:?}"); + server_error(Error::new(e, ErrorKind::Network)) } } - .map_or_else( - |e| { - use std::io::ErrorKind as E; - match e.kind() { - E::UnexpectedEof | E::BrokenPipe | E::ConnectionAborted | E::ConnectionReset | E::ConnectionRefused | E::TimedOut | E::Interrupted | E::NotConnected => Ok(()), - _ => Err(e), - }}, - |_| Ok(()), - )?; - } else { - // 503 - } - Ok::<_, Error>(()) - } + }, + )), + ), + ) + .await + .map_err(|e| { + Error::new(color_eyre::eyre::Report::msg(e), ErrorKind::Network) + }); + } else { + return Err(e).with_kind(ErrorKind::Network); + } + } + }; + let target_name = mid.client_hello().server_name().map(|s| s.into()); + let target = mapping.peek(|m| { + m.get(&target_name) + .into_iter() + .flatten() + .find(|(_, rc)| rc.strong_count() > 0) + .or_else(|| { + if target_name + .as_ref() + .map(|s| s.parse::().is_ok()) + .unwrap_or(true) + { + m.get(&None) + .into_iter() + .flatten() + .find(|(_, rc)| rc.strong_count() > 0) + } else { + None + } + }) + .map(|(target, _)| target.clone()) + }); + if let Some(target) = target { + if is_public && !target.public { + log::warn!("Rejecting connection from public interface to private bind"); + return Ok(()); + } + let peek = db.peek().await; + let root = peek + .as_private() + .as_key_store() + .as_local_certs() + .as_root_cert() + .de()?; + let mut cfg = match async { + if let Some(acme_settings) = peek.as_public().as_server_info().as_acme().de()? { + if let Some(domain) = target_name + .as_ref() + .filter(|target_name| acme_settings.domains.contains(*target_name)) + { + if mid + .client_hello() + .alpn() + .into_iter() + .flatten() + .any(|alpn| alpn == ACME_TLS_ALPN_NAME) + { + let cert = WatchStream::new( + acme_tls_alpn_cache + .peek(|c| c.get(&**domain).cloned()) + .ok_or_else(|| { + Error::new( + eyre!("No challenge recv available for {domain}"), + ErrorKind::OpenSsl, + ) + })?, + ); + tracing::info!("Waiting for verification cert for {domain}"); + let cert = cert + .filter(|c| c.is_some()) + .next() .await - { - tracing::error!("Error in VHostController on port {port}: {e}"); - tracing::debug!("{e:?}") - } - }); + .flatten() + .ok_or_else(|| { + Error::new( + eyre!("No challenge available for {domain}"), + ErrorKind::OpenSsl, + ) + })?; + tracing::info!("Verification cert received for {domain}"); + let mut cfg = + ServerConfig::builder_with_provider(crypto_provider.clone()) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_no_client_auth() + .with_cert_resolver(Arc::new(SingleCertResolver(cert))); + + cfg.alpn_protocols = vec![ACME_TLS_ALPN_NAME.to_vec()]; + return Ok(Err(cfg)); + } else { + let domains = [domain.to_string()]; + let (send, recv) = watch::channel(None); + acme_tls_alpn_cache.mutate(|c| c.insert(domain.clone(), recv)); + let cert = async_acme::rustls_helper::order( + |_, cert| { + send.send_replace(Some(Arc::new(cert))); + Ok(()) + }, + acme_settings.provider.as_str(), + &domains, + Some(&AcmeCertCache(&db)), + &acme_settings.contact, + ) + .await + .with_kind(ErrorKind::OpenSsl)?; + return Ok(Ok(ServerConfig::builder_with_provider( + crypto_provider.clone(), + ) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_no_client_auth() + .with_cert_resolver(Arc::new(SingleCertResolver(Arc::new(cert)))))); } + } + } + let hostnames = target_name + .into_iter() + .chain([InternedString::from_display(&bind.ip())]) + .collect(); + let key = db + .mutate(|v| { + v.as_private_mut() + .as_key_store_mut() + .as_local_certs_mut() + .cert_for(&hostnames) + }) + .await?; + let cfg = ServerConfig::builder_with_provider(crypto_provider.clone()) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_no_client_auth(); + if mid + .client_hello() + .signature_schemes() + .contains(&tokio_rustls::rustls::SignatureScheme::ED25519) + { + cfg.with_single_cert( + key.fullchain_ed25519() + .into_iter() + .map(|c| { + Ok(tokio_rustls::rustls::pki_types::CertificateDer::from( + c.to_der()?, + )) + }) + .collect::>()?, + PrivateKeyDer::from(PrivatePkcs8KeyDer::from( + key.leaf.keys.ed25519.private_key_to_pkcs8()?, + )), + ) + } else { + cfg.with_single_cert( + key.fullchain_nistp256() + .into_iter() + .map(|c| { + Ok(tokio_rustls::rustls::pki_types::CertificateDer::from( + c.to_der()?, + )) + }) + .collect::>()?, + PrivateKeyDer::from(PrivatePkcs8KeyDer::from( + key.leaf.keys.nistp256.private_key_to_pkcs8()?, + )), + ) + } + .with_kind(crate::ErrorKind::OpenSsl) + .map(Ok) + } + .await? + { + Ok(a) => a, + Err(cfg) => { + tracing::info!("performing ACME auth challenge"); + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; + accept.await?; + tracing::info!("ACME auth challenge completed"); + return Ok(()); + } + }; + let mut tcp_stream = TcpStream::connect(target.addr).await?; + match target.connect_ssl { + Ok(()) => { + let mut client_cfg = + tokio_rustls::rustls::ClientConfig::builder_with_provider(crypto_provider) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_root_certificates({ + let mut store = RootCertStore::empty(); + store + .add(CertificateDer::from(root.to_der()?)) + .with_kind(crate::ErrorKind::OpenSsl)?; + store + }) + .with_no_client_auth(); + client_cfg.alpn_protocols = mid + .client_hello() + .alpn() + .into_iter() + .flatten() + .map(|x| x.to_vec()) + .collect(); + let mut target_stream = TlsConnector::from(Arc::new(client_cfg)) + .connect_with( + ServerName::IpAddress(target.addr.ip().into()), + tcp_stream, + |conn| { + cfg.alpn_protocols + .extend(conn.alpn_protocol().into_iter().map(|p| p.to_vec())) + }, + ) + .await + .with_kind(crate::ErrorKind::OpenSsl)?; + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; + let mut tls_stream = match accept.await { + Ok(a) => a, + Err(e) => { + tracing::trace!( + "VHostController: failed to accept TLS connection on {bind}: {e}" + ); + tracing::trace!("{e:?}"); + return Ok(()); + } + }; + tokio::io::copy_bidirectional(&mut tls_stream, &mut target_stream).await + } + Err(AlpnInfo::Reflect) => { + for proto in mid.client_hello().alpn().into_iter().flatten() { + cfg.alpn_protocols.push(proto.into()); + } + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; + let mut tls_stream = match accept.await { + Ok(a) => a, Err(e) => { tracing::trace!( - "VHostController: failed to accept connection on port {port}: {e}" + "VHostController: failed to accept TLS connection on {bind}: {e}" ); tracing::trace!("{e:?}"); + return Ok(()); } + }; + tokio::io::copy_bidirectional(&mut tls_stream, &mut tcp_stream).await + } + Err(AlpnInfo::Specified(alpn)) => { + cfg.alpn_protocols = alpn.into_iter().map(|a| a.0).collect(); + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; + let mut tls_stream = match accept.await { + Ok(a) => a, + Err(e) => { + tracing::trace!( + "VHostController: failed to accept TLS connection on {bind}: {e}" + ); + tracing::trace!("{e:?}"); + return Ok(()); + } + }; + tokio::io::copy_bidirectional(&mut tls_stream, &mut tcp_stream).await + } + } + .map_or_else( + |e| { + use std::io::ErrorKind as E; + match e.kind() { + E::UnexpectedEof + | E::BrokenPipe + | E::ConnectionAborted + | E::ConnectionReset + | E::ConnectionRefused + | E::TimedOut + | E::Interrupted + | E::NotConnected => Ok(()), + _ => Err(e), + } + }, + |_| Ok(()), + )?; + } else { + // 503 + } + Ok::<_, Error>(()) + } + + #[instrument(skip_all)] + fn new( + port: u16, + db: TypedPatchDb, + iface_ctrl: Arc, + crypto_provider: Arc, + ) -> Result { + let acme_tls_alpn_cache = Arc::new(SyncMutex::new(BTreeMap::new())); + let mut listener = iface_ctrl.bind(port).with_kind(crate::ErrorKind::Network)?; + let mapping = Arc::new(SyncMutex::new(BTreeMap::new())); + Ok(Self { + mapping: Arc::downgrade(&mapping), + _thread: tokio::spawn(async move { + loop { + if let Err(e) = Self::accept( + &mut listener, + mapping.clone(), + db.clone(), + acme_tls_alpn_cache.clone(), + crypto_provider.clone(), + ) + .await + { + tracing::error!( + "VHostController: failed to accept connection on {port}: {e}" + ); + tracing::debug!("{e:?}"); } } }) .into(), }) } - async fn add( - &self, - hostname: Option, - target: TargetInfo, - ) -> Result, Error> { + fn add(&self, hostname: Option, target: TargetInfo) -> Result, Error> { if let Some(mapping) = Weak::upgrade(&self.mapping) { - let mut writable = mapping.write().await; - let mut targets = writable.remove(&hostname).unwrap_or_default(); - let rc = if let Some(rc) = Weak::upgrade(&targets.remove(&target).unwrap_or_default()) { - rc - } else { - Arc::new(()) - }; - targets.insert(target, Arc::downgrade(&rc)); - writable.insert(hostname, targets); - Ok(rc) + mapping.mutate(|writable| { + let mut targets = writable.remove(&hostname).unwrap_or_default(); + let rc = + if let Some(rc) = Weak::upgrade(&targets.remove(&target).unwrap_or_default()) { + rc + } else { + Arc::new(()) + }; + targets.insert(target, Arc::downgrade(&rc)); + writable.insert(hostname, targets); + Ok(rc) + }) } else { Err(Error::new( eyre!("VHost Service Thread has exited"), @@ -542,18 +594,19 @@ impl VHostServer { )) } } - async fn gc(&self, hostname: Option) -> Result<(), Error> { + fn gc(&self, hostname: Option) -> Result<(), Error> { if let Some(mapping) = Weak::upgrade(&self.mapping) { - let mut writable = mapping.write().await; - let mut targets = writable.remove(&hostname).unwrap_or_default(); - targets = targets - .into_iter() - .filter(|(_, rc)| rc.strong_count() > 0) - .collect(); - if !targets.is_empty() { - writable.insert(hostname, targets); - } - Ok(()) + mapping.mutate(|writable| { + let mut targets = writable.remove(&hostname).unwrap_or_default(); + targets = targets + .into_iter() + .filter(|(_, rc)| rc.strong_count() > 0) + .collect(); + if !targets.is_empty() { + writable.insert(hostname, targets); + } + Ok(()) + }) } else { Err(Error::new( eyre!("VHost Service Thread has exited"), @@ -561,9 +614,9 @@ impl VHostServer { )) } } - async fn is_empty(&self) -> Result { + fn is_empty(&self) -> Result { if let Some(mapping) = Weak::upgrade(&self.mapping) { - Ok(mapping.read().await.is_empty()) + Ok(mapping.peek(|m| m.is_empty())) } else { Err(Error::new( eyre!("VHost Service Thread has exited"), diff --git a/core/startos/src/registry/package/index.rs b/core/startos/src/registry/package/index.rs index 428200165b..9973bae7e2 100644 --- a/core/startos/src/registry/package/index.rs +++ b/core/startos/src/registry/package/index.rs @@ -72,7 +72,6 @@ pub struct PackageVersionInfo { pub icon: DataUrl<'static>, pub description: Description, pub release_notes: String, - #[ts(type = "string")] pub git_hash: GitHash, #[ts(type = "string")] pub license: InternedString, diff --git a/core/startos/src/s9pk/git_hash.rs b/core/startos/src/s9pk/git_hash.rs index 02f83bf4a1..762ef8704d 100644 --- a/core/startos/src/s9pk/git_hash.rs +++ b/core/startos/src/s9pk/git_hash.rs @@ -1,11 +1,13 @@ use std::path::Path; use tokio::process::Command; +use ts_rs::TS; use crate::prelude::*; use crate::util::Invoke; -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS)] +#[ts(type = "string")] pub struct GitHash(String); impl GitHash { @@ -31,6 +33,31 @@ impl GitHash { } Ok(GitHash(hash)) } + pub fn load_sync() -> Option { + let mut hash = String::from_utf8( + std::process::Command::new("git") + .arg("rev-parse") + .arg("HEAD") + .output() + .ok()? + .stdout, + ) + .ok()?; + if !std::process::Command::new("git") + .arg("diff-index") + .arg("--quiet") + .arg("HEAD") + .arg("--") + .output() + .ok()? + .status + .success() + { + hash += "-modified"; + } + + Some(GitHash(hash)) + } } impl AsRef for GitHash { diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index 85f3cd796f..187b2dede0 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -62,8 +62,8 @@ pub struct Manifest { pub dependencies: Dependencies, #[serde(default)] pub hardware_requirements: HardwareRequirements, - #[serde(default)] - #[ts(type = "string | null")] + #[ts(optional)] + #[serde(default = "GitHash::load_sync")] pub git_hash: Option, #[serde(default = "current_version")] #[ts(type = "string")] diff --git a/core/startos/src/service/effects/net/bind.rs b/core/startos/src/service/effects/net/bind.rs index 5619375eba..d30b45f72f 100644 --- a/core/startos/src/service/effects/net/bind.rs +++ b/core/startos/src/service/effects/net/bind.rs @@ -1,6 +1,6 @@ use models::{HostId, PackageId}; -use crate::net::host::binding::{BindId, BindOptions, LanInfo}; +use crate::net::host::binding::{BindId, BindOptions, NetInfo}; use crate::net::host::HostKind; use crate::service::effects::prelude::*; @@ -58,7 +58,7 @@ pub struct GetServicePortForwardParams { pub async fn get_service_port_forward( context: EffectContext, data: GetServicePortForwardParams, -) -> Result { +) -> Result { let internal_port = data.internal_port as u16; let context = context.deref()?; diff --git a/core/startos/src/service/effects/subcontainer/mod.rs b/core/startos/src/service/effects/subcontainer/mod.rs index 65fcbd387d..943c70dbf6 100644 --- a/core/startos/src/service/effects/subcontainer/mod.rs +++ b/core/startos/src/service/effects/subcontainer/mod.rs @@ -4,12 +4,11 @@ use imbl_value::InternedString; use models::ImageId; use tokio::process::Command; +use crate::disk::mount::filesystem::overlayfs::OverlayGuard; use crate::rpc_continuations::Guid; use crate::service::effects::prelude::*; +use crate::service::persistent_container::Subcontainer; use crate::util::Invoke; -use crate::{ - disk::mount::filesystem::overlayfs::OverlayGuard, service::persistent_container::Subcontainer, -}; #[cfg(feature = "container-runtime")] mod sync; diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index d73c51beb4..d2e2939090 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -149,10 +149,10 @@ impl ServiceRef { .values() .flat_map(|h| h.bindings.values()) .flat_map(|b| { - b.lan + b.net .assigned_port .into_iter() - .chain(b.lan.assigned_ssl_port) + .chain(b.net.assigned_ssl_port) }), ); Ok(()) diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index 0e7aada54c..8e270c9128 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -460,18 +460,30 @@ impl BackTrackingIO { } } } - pub fn rewind(&mut self) -> Vec { + pub fn rewind<'a>(&'a mut self) -> (Vec, &'a [u8]) { match std::mem::take(&mut self.buffer) { BTBuffer::Buffering { read, write } => { self.buffer = BTBuffer::Rewound { read: Cursor::new(read), }; - write + ( + write, + match &self.buffer { + BTBuffer::Rewound { read } => read.get_ref(), + _ => unreachable!(), + }, + ) } - BTBuffer::NotBuffering => Vec::new(), + BTBuffer::NotBuffering => (Vec::new(), &[]), BTBuffer::Rewound { read } => { self.buffer = BTBuffer::Rewound { read }; - Vec::new() + ( + Vec::new(), + match &self.buffer { + BTBuffer::Rewound { read } => read.get_ref(), + _ => unreachable!(), + }, + ) } } } diff --git a/sdk/base/lib/Effects.ts b/sdk/base/lib/Effects.ts index e4424fafb5..a0eb08d67b 100644 --- a/sdk/base/lib/Effects.ts +++ b/sdk/base/lib/Effects.ts @@ -8,7 +8,7 @@ import { SetHealth, BindParams, HostId, - LanInfo, + NetInfo, Host, ExportServiceInterfaceParams, ServiceInterface, @@ -117,7 +117,7 @@ export type Effects = { packageId?: PackageId hostId: HostId internalPort: number - }): Promise + }): Promise /** Removes all network bindings, called in the setupInputSpec */ clearBindings(options: { except: { id: HostId; internalPort: number }[] diff --git a/sdk/base/lib/osBindings/BindInfo.ts b/sdk/base/lib/osBindings/BindInfo.ts index 85fc38e949..b03dbe6b2e 100644 --- a/sdk/base/lib/osBindings/BindInfo.ts +++ b/sdk/base/lib/osBindings/BindInfo.ts @@ -1,5 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { BindOptions } from "./BindOptions" -import type { LanInfo } from "./LanInfo" +import type { NetInfo } from "./NetInfo" -export type BindInfo = { enabled: boolean; options: BindOptions; lan: LanInfo } +export type BindInfo = { enabled: boolean; options: BindOptions; net: NetInfo } diff --git a/sdk/base/lib/osBindings/GitHash.ts b/sdk/base/lib/osBindings/GitHash.ts new file mode 100644 index 0000000000..43f6adde3a --- /dev/null +++ b/sdk/base/lib/osBindings/GitHash.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GitHash = string diff --git a/sdk/base/lib/osBindings/IpInfo.ts b/sdk/base/lib/osBindings/IpInfo.ts index ae8c88d1b5..184e72ddfb 100644 --- a/sdk/base/lib/osBindings/IpInfo.ts +++ b/sdk/base/lib/osBindings/IpInfo.ts @@ -1,8 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type IpInfo = { - ipv4Range: string | null - ipv4: string | null - ipv6Range: string | null - ipv6: string | null -} +export type IpInfo = string[] diff --git a/sdk/base/lib/osBindings/Manifest.ts b/sdk/base/lib/osBindings/Manifest.ts index 8007b565b3..2c9a2457ef 100644 --- a/sdk/base/lib/osBindings/Manifest.ts +++ b/sdk/base/lib/osBindings/Manifest.ts @@ -2,6 +2,7 @@ import type { Alerts } from "./Alerts" import type { Dependencies } from "./Dependencies" import type { Description } from "./Description" +import type { GitHash } from "./GitHash" import type { HardwareRequirements } from "./HardwareRequirements" import type { ImageConfig } from "./ImageConfig" import type { ImageId } from "./ImageId" @@ -30,6 +31,6 @@ export type Manifest = { alerts: Alerts dependencies: Dependencies hardwareRequirements: HardwareRequirements - gitHash: string | null + gitHash?: GitHash osVersion: string } diff --git a/sdk/base/lib/osBindings/LanInfo.ts b/sdk/base/lib/osBindings/NetInfo.ts similarity index 80% rename from sdk/base/lib/osBindings/LanInfo.ts rename to sdk/base/lib/osBindings/NetInfo.ts index 59b8a5519d..e790cadaaa 100644 --- a/sdk/base/lib/osBindings/LanInfo.ts +++ b/sdk/base/lib/osBindings/NetInfo.ts @@ -1,6 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type LanInfo = { +export type NetInfo = { + public: boolean assignedPort: number | null assignedSslPort: number | null } diff --git a/sdk/base/lib/osBindings/NetworkInterfaceInfo.ts b/sdk/base/lib/osBindings/NetworkInterfaceInfo.ts new file mode 100644 index 0000000000..c9f82005dd --- /dev/null +++ b/sdk/base/lib/osBindings/NetworkInterfaceInfo.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { IpInfo } from "./IpInfo" + +export type NetworkInterfaceInfo = { + public: boolean | null + scopeId: number | null + ipInfo: IpInfo +} diff --git a/sdk/base/lib/osBindings/PackageVersionInfo.ts b/sdk/base/lib/osBindings/PackageVersionInfo.ts index 80481acb3a..c71fd5921c 100644 --- a/sdk/base/lib/osBindings/PackageVersionInfo.ts +++ b/sdk/base/lib/osBindings/PackageVersionInfo.ts @@ -3,6 +3,7 @@ import type { Alerts } from "./Alerts" import type { DataUrl } from "./DataUrl" import type { DependencyMetadata } from "./DependencyMetadata" import type { Description } from "./Description" +import type { GitHash } from "./GitHash" import type { HardwareRequirements } from "./HardwareRequirements" import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" import type { PackageId } from "./PackageId" @@ -13,7 +14,7 @@ export type PackageVersionInfo = { icon: DataUrl description: Description releaseNotes: string - gitHash: string + gitHash: GitHash license: string wrapperRepo: string upstreamRepo: string diff --git a/sdk/base/lib/osBindings/ServerInfo.ts b/sdk/base/lib/osBindings/ServerInfo.ts index 89d7fc1b05..b5d5be2933 100644 --- a/sdk/base/lib/osBindings/ServerInfo.ts +++ b/sdk/base/lib/osBindings/ServerInfo.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AcmeSettings } from "./AcmeSettings" import type { Governor } from "./Governor" -import type { IpInfo } from "./IpInfo" import type { LshwDevice } from "./LshwDevice" +import type { NetworkInterfaceInfo } from "./NetworkInterfaceInfo" import type { ServerStatus } from "./ServerStatus" import type { SmtpValue } from "./SmtpValue" import type { WifiInfo } from "./WifiInfo" @@ -22,7 +22,7 @@ export type ServerInfo = { * for backwards compatibility */ torAddress: string - ipInfo: { [key: string]: IpInfo } + networkInterfaces: { [key: string]: NetworkInterfaceInfo } acme: AcmeSettings | null statusInfo: ServerStatus wifi: WifiInfo diff --git a/sdk/base/lib/osBindings/SetPublicParams.ts b/sdk/base/lib/osBindings/SetPublicParams.ts new file mode 100644 index 0000000000..03bc3082b2 --- /dev/null +++ b/sdk/base/lib/osBindings/SetPublicParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetPublicParams = { interface: string; public: boolean | null } diff --git a/sdk/base/lib/osBindings/UnsetPublicParams.ts b/sdk/base/lib/osBindings/UnsetPublicParams.ts new file mode 100644 index 0000000000..db8f730e1b --- /dev/null +++ b/sdk/base/lib/osBindings/UnsetPublicParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type UnsetPublicParams = { interface: string } diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index f76f595c9c..623ebc23a1 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -90,6 +90,7 @@ export { GetSslKeyParams } from "./GetSslKeyParams" export { GetStatusParams } from "./GetStatusParams" export { GetStoreParams } from "./GetStoreParams" export { GetSystemSmtpParams } from "./GetSystemSmtpParams" +export { GitHash } from "./GitHash" export { Governor } from "./Governor" export { Guid } from "./Guid" export { HardwareRequirements } from "./HardwareRequirements" @@ -112,7 +113,6 @@ export { InstallingState } from "./InstallingState" export { InstallParams } from "./InstallParams" export { IpHostname } from "./IpHostname" export { IpInfo } from "./IpInfo" -export { LanInfo } from "./LanInfo" export { ListPackageSignersParams } from "./ListPackageSignersParams" export { ListServiceInterfacesParams } from "./ListServiceInterfacesParams" export { ListVersionSignersParams } from "./ListVersionSignersParams" @@ -128,6 +128,8 @@ export { MountParams } from "./MountParams" export { MountTarget } from "./MountTarget" export { NamedHealthCheckResult } from "./NamedHealthCheckResult" export { NamedProgress } from "./NamedProgress" +export { NetInfo } from "./NetInfo" +export { NetworkInterfaceInfo } from "./NetworkInterfaceInfo" export { OnionHostname } from "./OnionHostname" export { OsIndex } from "./OsIndex" export { OsVersionInfoMap } from "./OsVersionInfoMap" @@ -172,6 +174,7 @@ export { SetIconParams } from "./SetIconParams" export { SetMainStatusStatus } from "./SetMainStatusStatus" export { SetMainStatus } from "./SetMainStatus" export { SetNameParams } from "./SetNameParams" +export { SetPublicParams } from "./SetPublicParams" export { SetStoreParams } from "./SetStoreParams" export { SetupExecuteParams } from "./SetupExecuteParams" export { SetupProgress } from "./SetupProgress" @@ -181,6 +184,7 @@ export { SignAssetParams } from "./SignAssetParams" export { SignerInfo } from "./SignerInfo" export { SmtpValue } from "./SmtpValue" export { StartStop } from "./StartStop" +export { UnsetPublicParams } from "./UnsetPublicParams" export { UpdatingState } from "./UpdatingState" export { VerifyCifsParams } from "./VerifyCifsParams" export { VersionSignerParams } from "./VersionSignerParams" diff --git a/sdk/base/package-lock.json b/sdk/base/package-lock.json index d7b4913036..4d5625489c 100644 --- a/sdk/base/package-lock.json +++ b/sdk/base/package-lock.json @@ -14,7 +14,7 @@ "isomorphic-fetch": "^3.0.0", "lodash.merge": "^4.6.2", "mime-types": "^2.1.35", - "ts-matches": "^6.1.0", + "ts-matches": "^6.2.1", "yaml": "^2.2.2" }, "devDependencies": { @@ -3897,9 +3897,9 @@ "dev": true }, "node_modules/ts-matches": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.1.0.tgz", - "integrity": "sha512-01qvbIpOiKdbzzXDH84JeHunvCwBGFdZw94jS6kOGLSN5ms+1nBZtfe8WSuYMIPb1xPA+qyAiVgznFi2VCQ6UQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.2.1.tgz", + "integrity": "sha512-qdnMgTHsGCEGGK6QiaNMY2vD9eQtRp2Q+pAxcOAzxHJKDKTBYsc1ISTg1zp8H2+EmtCB0eko/1TwYUA5/mUGug==", "license": "MIT" }, "node_modules/ts-morph": { diff --git a/sdk/base/package.json b/sdk/base/package.json index 4cc2fc7ca9..6eae719a7f 100644 --- a/sdk/base/package.json +++ b/sdk/base/package.json @@ -27,7 +27,7 @@ "isomorphic-fetch": "^3.0.0", "lodash.merge": "^4.6.2", "mime-types": "^2.1.35", - "ts-matches": "^6.1.0", + "ts-matches": "^6.2.1", "yaml": "^2.2.2" }, "prettier": { diff --git a/sdk/package/lib/manifest/setupManifest.ts b/sdk/package/lib/manifest/setupManifest.ts index 1a78c062c5..3cd4f8bfbd 100644 --- a/sdk/package/lib/manifest/setupManifest.ts +++ b/sdk/package/lib/manifest/setupManifest.ts @@ -26,16 +26,6 @@ export function setupManifest< return manifest } -function gitHash(): string { - const hash = execSync("git rev-parse HEAD").toString().trim() - try { - execSync("git diff-index --quiet HEAD --") - return hash - } catch (e) { - return hash + "-modified" - } -} - export function buildManifest< Id extends string, Version extends string, @@ -68,7 +58,6 @@ export function buildManifest< ) return { ...manifest, - gitHash: gitHash(), osVersion: SDKVersion, version: versions.current.options.version, releaseNotes: versions.current.options.releaseNotes, diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index 1f57152584..3befe3fbfe 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -1,12 +1,12 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha.21", + "version": "0.3.6-alpha.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha.21", + "version": "0.3.6-alpha.23", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -15,7 +15,7 @@ "isomorphic-fetch": "^3.0.0", "lodash.merge": "^4.6.2", "mime-types": "^2.1.35", - "ts-matches": "^6.1.0", + "ts-matches": "^6.2.1", "yaml": "^2.2.2" }, "devDependencies": { @@ -3918,9 +3918,9 @@ "dev": true }, "node_modules/ts-matches": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.1.0.tgz", - "integrity": "sha512-01qvbIpOiKdbzzXDH84JeHunvCwBGFdZw94jS6kOGLSN5ms+1nBZtfe8WSuYMIPb1xPA+qyAiVgznFi2VCQ6UQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.2.1.tgz", + "integrity": "sha512-qdnMgTHsGCEGGK6QiaNMY2vD9eQtRp2Q+pAxcOAzxHJKDKTBYsc1ISTg1zp8H2+EmtCB0eko/1TwYUA5/mUGug==", "license": "MIT" }, "node_modules/ts-morph": { diff --git a/sdk/package/package.json b/sdk/package/package.json index 2bf4b71f53..b720350a45 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha.21", + "version": "0.3.6-alpha.23", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts", @@ -33,7 +33,7 @@ "isomorphic-fetch": "^3.0.0", "lodash.merge": "^4.6.2", "mime-types": "^2.1.35", - "ts-matches": "^6.1.0", + "ts-matches": "^6.2.1", "yaml": "^2.2.2", "@iarna/toml": "^2.2.5", "@noble/curves": "^1.4.0", diff --git a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html index 03d7ef3d71..f685b3b329 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html +++ b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html @@ -58,25 +58,20 @@

LAN

- - - -

{{ iface.key }} (IPv4)

-

{{ ipv4 || 'n/a' }}

-
- - - -
- - -

{{ iface.key }} (IPv6)

-

{{ ipv6 || 'n/a' }}

-
- - - -
+ + + + + +

{{ iface.key }} ({{ ipAddr.includes("::") ? "IPv6" : "IPv4" }})

+

{{ ipAddr.includes("::") ? "[" + ipAddr + (iface.value.scopeId ? "%" + iface.value.scopeId : "") + "]" : ipAddr }}

+
+ + + +
+
+
Device Credentials diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 81e316b562..3779f350af 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -41,18 +41,19 @@ export const mockPatchData: DataModel = { lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(), lanAddress: 'https://adjective-noun.local', torAddress: 'https://myveryownspecialtoraddress.onion', - ipInfo: { + networkInterfaces: { eth0: { - ipv4: '10.0.0.1', - ipv4Range: '10.0.0.1/24', - ipv6: null, - ipv6Range: null, + public: false, + scopeId: 1, + ipInfo: ['10.0.0.1/24'], }, wlan0: { - ipv4: '10.0.90.12', - ipv4Range: '10.0.90.12/24', - ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD', - ipv6Range: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD/64', + public: false, + scopeId: 2, + ipInfo: [ + '10.0.90.12/24', + 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD/64', + ], }, }, acme: null,