diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 92ff2179e..426c47637 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -297,6 +297,15 @@ dependencies = [ "scoped-tls", ] +[[package]] +name = "bimap" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0455254eb5c6964c4545d8bac815e1a1be4f3afe0ae695ea539c12d728d44b" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "1.3.3" @@ -1365,6 +1374,7 @@ dependencies = [ "base64 0.13.1", "base64ct", "basic-cookies", + "bimap", "bollard", "bytes", "chrono", @@ -1391,6 +1401,7 @@ dependencies = [ "http", "hyper", "hyper-ws-listener", + "id-pool", "imbl 2.0.0", "indexmap", "ipnet", @@ -2199,6 +2210,15 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "id-pool" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d0df4d8a768821ee4aa2e0353f67125c4586f0e13adbf95b8ebbf8d8fdb344" +dependencies = [ + "serde", +] + [[package]] name = "ident_case" version = "1.0.1" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5f7e4cc90..6fd996465 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -62,6 +62,7 @@ base32 = "0.4.0" base64 = "0.13.0" base64ct = "1.5.1" basic-cookies = "0.1.4" +bimap = { version = "0.6.2", features = ["serde"] } bollard = "0.13.0" bytes = "1" chrono = { version = "0.4.19", features = ["serde"] } @@ -89,6 +90,10 @@ hmac = "0.12.1" http = "0.2.8" hyper = { version = "0.14.20", features = ["full"] } hyper-ws-listener = "0.2.0" +id-pool = { version = "0.2.2", features = [ + "u16", + "serde", +], default-features = false } imbl = "2.0.0" indexmap = { version = "1.9.1", features = ["serde"] } ipnet = { version = "2.7.1", features = ["serde"] } diff --git a/backend/src/db/model.rs b/backend/src/db/model.rs index 013fae2f2..c89c8c854 100644 --- a/backend/src/db/model.rs +++ b/backend/src/db/model.rs @@ -18,6 +18,7 @@ use ssh_key::public::Ed25519PublicKey; use crate::account::AccountInfo; use crate::config::spec::{PackagePointerSpec, SystemPointerSpec}; use crate::install::progress::InstallProgress; +use crate::net::forward::LanPortForwards; use crate::net::interface::InterfaceId; use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; use crate::s9pk::manifest::{Manifest, ManifestModel, PackageId}; @@ -34,6 +35,7 @@ pub struct Database { pub server_info: ServerInfo, #[model] pub package_data: AllPackageData, + pub lan_port_forwards: LanPortForwards, pub ui: Value, } impl Database { @@ -83,6 +85,7 @@ impl Database { zram: false, }, package_data: AllPackageData::default(), + lan_port_forwards: LanPortForwards::new(), ui: serde_json::from_str(include_str!("../../../frontend/patchdb-ui-seed.json")) .unwrap(), } diff --git a/backend/src/manager/js_api.rs b/backend/src/manager/js_api.rs index d2bd7ba8b..9244f81a6 100644 --- a/backend/src/manager/js_api.rs +++ b/backend/src/manager/js_api.rs @@ -95,16 +95,13 @@ impl OsApi for Manager { let mut secrets = self.seed.ctx.secret_store.acquire().await?; let mut tx = secrets.begin().await?; - svc.add_lan(&mut tx, id.clone(), external_port, internal_port, false) + let addr = svc + .add_lan(&mut tx, id.clone(), external_port, internal_port, false) .await .map_err(|e| eyre!("Could not add to local: {e:?}"))?; - let key = Key::for_interface(&mut tx, Some((self.seed.manifest.id.clone(), id))) - .await - .map_err(|e| eyre!("Could not get network name: {e:?}"))? - .local_address(); tx.commit().await?; - Ok(helpers::Address(key)) + Ok(helpers::Address(addr)) } async fn bind_onion( &self, @@ -125,16 +122,13 @@ impl OsApi for Manager { let mut secrets = self.seed.ctx.secret_store.acquire().await?; let mut tx = secrets.begin().await?; - svc.add_tor(&mut tx, id.clone(), external_port, internal_port) + let addr = svc + .add_tor(&mut tx, id.clone(), external_port, internal_port) .await .map_err(|e| eyre!("Could not add to tor: {e:?}"))?; - let key = Key::for_interface(&mut tx, Some((self.seed.manifest.id.clone(), id))) - .await - .map_err(|e| eyre!("Could not get network name: {e:?}"))? - .tor_address() - .to_string(); + tx.commit().await?; - Ok(helpers::Address(key)) + Ok(helpers::Address(addr)) } async fn unbind_local(&self, id: InterfaceId, external: u16) -> Result<(), Report> { let ip = try_get_running_ip(&self.seed) diff --git a/backend/src/net/forward.rs b/backend/src/net/forward.rs new file mode 100644 index 000000000..dde36b8fd --- /dev/null +++ b/backend/src/net/forward.rs @@ -0,0 +1,192 @@ +use std::collections::BTreeMap; +use std::net::SocketAddr; +use std::sync::{Arc, Weak}; + +use id_pool::IdPool; +use models::PackageId; +use serde::{Deserialize, Serialize}; +use tokio::process::Command; +use tokio::sync::Mutex; + +use crate::util::Invoke; +use crate::Error; + +pub const START9_BRIDGE_IFACE: &str = "br-start9"; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LanPortForwards { + pool: IdPool, + allocated: BTreeMap>, +} +impl LanPortForwards { + pub fn new() -> Self { + Self { + pool: IdPool::new_ranged(32768..u16::MAX), + allocated: BTreeMap::new(), + } + } + pub fn alloc(&mut self, package: PackageId, port: u16) -> Option { + if let Some(res) = self.allocated.get(&package).and_then(|a| a.get(&port)) { + Some(*res) + } else if let Some(res) = self.pool.request_id() { + let mut ports = self.allocated.remove(&package).unwrap_or_default(); + ports.insert(port, res); + self.allocated.insert(package, ports); + Some(res) + } else { + None + } + } + pub fn dealloc(&mut self, package: &PackageId) { + for port in self + .allocated + .remove(package) + .into_iter() + .flat_map(|p| p.into_values()) + { + self.pool.return_id(port).unwrap_or_default(); + } + } +} + +pub struct LpfController { + forwards: Mutex>>>, +} +impl LpfController { + pub fn new() -> Self { + Self { + forwards: Mutex::new(BTreeMap::new()), + } + } + pub async fn add(&self, port: u16, addr: SocketAddr) -> Result, Error> { + let mut writable = self.forwards.lock().await; + let (prev, mut forward) = if let Some(forward) = writable.remove(&port) { + ( + forward.keys().next().cloned(), + forward + .into_iter() + .filter(|(_, rc)| rc.strong_count() > 0) + .collect(), + ) + } else { + (None, BTreeMap::new()) + }; + let rc = Arc::new(()); + forward.insert(addr, Arc::downgrade(&rc)); + let next = forward.keys().next().cloned(); + if !forward.is_empty() { + writable.insert(port, forward); + } + + update_forward(port, prev, next).await?; + Ok(rc) + } + pub async fn gc(&self, port: u16) -> Result<(), Error> { + let mut writable = self.forwards.lock().await; + let (prev, forward) = if let Some(forward) = writable.remove(&port) { + ( + forward.keys().next().cloned(), + forward + .into_iter() + .filter(|(_, rc)| rc.strong_count() > 0) + .collect(), + ) + } else { + (None, BTreeMap::new()) + }; + let next = forward.keys().next().cloned(); + if !forward.is_empty() { + writable.insert(port, forward); + } + + update_forward(port, prev, next).await + } +} + +async fn update_forward( + port: u16, + prev: Option, + next: Option, +) -> Result<(), Error> { + if prev != next { + if let Some(prev) = prev { + unforward(START9_BRIDGE_IFACE, port, prev).await?; + } + if let Some(next) = next { + forward(START9_BRIDGE_IFACE, port, next).await?; + } + } + Ok(()) +} + +// iptables -I FORWARD -o br-start9 -p tcp -d 172.18.0.2 --dport 8333 -j ACCEPT +// iptables -t nat -I PREROUTING -p tcp --dport 32768 -j DNAT --to 172.18.0.2:8333 +async fn forward(iface: &str, port: u16, addr: SocketAddr) -> Result<(), Error> { + Command::new("iptables") + .arg("-I") + .arg("FORWARD") + .arg("-o") + .arg(iface) + .arg("-p") + .arg("tcp") + .arg("-d") + .arg(addr.ip().to_string()) + .arg("--dport") + .arg(addr.port().to_string()) + .arg("-j") + .arg("ACCEPT") + .invoke(crate::ErrorKind::Network) + .await?; + Command::new("iptables") + .arg("-t") + .arg("nat") + .arg("-I") + .arg("PREROUTING") + .arg("-p") + .arg("tcp") + .arg("--dport") + .arg(port.to_string()) + .arg("-j") + .arg("DNAT") + .arg("--to") + .arg(addr.to_string()) + .invoke(crate::ErrorKind::Network) + .await?; + Ok(()) +} + +// iptables -D FORWARD -o br-start9 -p tcp -d 172.18.0.2 --dport 8333 -j ACCEPT +// iptables -t nat -D PREROUTING -p tcp --dport 32768 -j DNAT --to 172.18.0.2:8333 +async fn unforward(iface: &str, port: u16, addr: SocketAddr) -> Result<(), Error> { + Command::new("iptables") + .arg("-D") + .arg("FORWARD") + .arg("-o") + .arg(iface) + .arg("-p") + .arg("tcp") + .arg("-d") + .arg(addr.ip().to_string()) + .arg("--dport") + .arg(addr.port().to_string()) + .arg("-j") + .arg("ACCEPT") + .invoke(crate::ErrorKind::Network) + .await?; + Command::new("iptables") + .arg("-t") + .arg("nat") + .arg("-D") + .arg("PREROUTING") + .arg("-p") + .arg("tcp") + .arg("--dport") + .arg(port.to_string()) + .arg("-j") + .arg("DNAT") + .arg("--to") + .arg(addr.to_string()) + .invoke(crate::ErrorKind::Network) + .await?; + Ok(()) +} diff --git a/backend/src/net/mod.rs b/backend/src/net/mod.rs index 1c6010662..d0508943d 100644 --- a/backend/src/net/mod.rs +++ b/backend/src/net/mod.rs @@ -8,6 +8,7 @@ use crate::Error; pub mod dhcp; pub mod dns; +pub mod forward; pub mod interface; pub mod keys; #[cfg(feature = "avahi")] diff --git a/backend/src/net/net_controller.rs b/backend/src/net/net_controller.rs index db695f639..22afbdd6d 100644 --- a/backend/src/net/net_controller.rs +++ b/backend/src/net/net_controller.rs @@ -4,12 +4,14 @@ use std::sync::{Arc, Weak}; use color_eyre::eyre::eyre; use models::InterfaceId; +use patch_db::{DbHandle, LockType, PatchDb}; use sqlx::PgExecutor; use tracing::instrument; use crate::error::ErrorCollection; use crate::hostname::Hostname; use crate::net::dns::DnsController; +use crate::net::forward::LpfController; use crate::net::keys::Key; #[cfg(feature = "avahi")] use crate::net::mdns::MdnsController; @@ -26,6 +28,7 @@ pub struct NetController { pub(super) mdns: MdnsController, pub(super) vhost: VHostController, pub(super) dns: DnsController, + pub(super) lpf: LpfController, pub(super) ssl: Arc, pub(super) os_bindings: Vec>, } @@ -47,6 +50,7 @@ impl NetController { mdns: MdnsController::init().await?, vhost: VHostController::new(ssl.clone()), dns: DnsController::init(dns_bind).await?, + lpf: LpfController::new(), ssl, os_bindings: Vec::new(), }; @@ -155,6 +159,7 @@ impl NetController { controller: Arc::downgrade(self), tor: BTreeMap::new(), lan: BTreeMap::new(), + lpf: BTreeMap::new(), }) } @@ -204,6 +209,15 @@ impl NetController { self.mdns.gc(key.base_address()).await?; self.vhost.gc(Some(key.local_address()), external).await } + + async fn add_lpf(&self, external: u16, target: SocketAddr) -> Result, Error> { + self.lpf.add(external, target).await + } + + async fn remove_lpf(&self, external: u16, rcs: Vec>) -> Result<(), Error> { + drop(rcs); + self.lpf.gc(external).await + } } pub struct NetService { @@ -213,6 +227,7 @@ pub struct NetService { controller: Weak, tor: BTreeMap<(InterfaceId, u16), (Key, Vec>)>, lan: BTreeMap<(InterfaceId, u16), (Key, Vec>)>, + lpf: BTreeMap>)>, } impl NetService { #[instrument(skip(self))] @@ -231,7 +246,7 @@ impl NetService { id: InterfaceId, external: u16, internal: u16, - ) -> Result<(), Error> + ) -> Result where for<'a> &'a mut Ex: PgExecutor<'a>, { @@ -248,7 +263,7 @@ impl NetService { .await?, ); self.tor.insert(tor_idx, tor); - Ok(()) + Ok(key.tor_address().to_string()) } pub async fn remove_tor(&mut self, id: InterfaceId, external: u16) -> Result<(), Error> { let ctrl = self.net_controller()?; @@ -264,11 +279,12 @@ impl NetService { external: u16, internal: u16, connect_ssl: bool, - ) -> Result<(), Error> + ) -> Result where for<'a> &'a mut Ex: PgExecutor<'a>, { let key = Key::for_interface(secrets, Some((self.id.clone(), id.clone()))).await?; + let addr = key.local_address(); let ctrl = self.net_controller()?; let lan_idx = (id, external); let mut lan = self @@ -286,7 +302,7 @@ impl NetService { .await?, ); self.lan.insert(lan_idx, lan); - Ok(()) + Ok(addr) } pub async fn remove_lan(&mut self, id: InterfaceId, external: u16) -> Result<(), Error> { let ctrl = self.net_controller()?; @@ -295,6 +311,35 @@ impl NetService { } Ok(()) } + pub async fn add_lpf(&mut self, db: &PatchDb, internal: u16) -> Result { + let ctrl = self.net_controller()?; + let mut db = db.handle(); + let lpf_model = crate::db::DatabaseModel::new().lan_port_forwards(); + lpf_model.lock(&mut db, LockType::Write).await?; // TODO: replace all this with an RMW + let mut lpf = lpf_model.get_mut(&mut db).await?; + let external = lpf.alloc(self.id.clone(), internal).ok_or_else(|| { + Error::new( + eyre!("No ephemeral ports available"), + crate::ErrorKind::Network, + ) + })?; + lpf.save(&mut db).await?; + drop(db); + let rc = ctrl.add_lpf(external, (self.ip, internal).into()).await?; + let (_, mut lpfs) = self.lpf.remove(&internal).unwrap_or_default(); + lpfs.push(rc); + self.lpf.insert(internal, (external, lpfs)); + + Ok(external) + } + pub async fn remove_lpf(&mut self, db: &PatchDb, internal: u16) -> Result<(), Error> { + let ctrl = self.net_controller()?; + if let Some((external, rcs)) = self.lpf.remove(&internal) { + ctrl.remove_lpf(external, rcs).await?; + } + + Ok(()) + } pub async fn export_cert( &self, secrets: &mut Ex, @@ -330,6 +375,9 @@ impl NetService { for ((_, external), (key, rcs)) in std::mem::take(&mut self.tor) { errors.handle(ctrl.remove_tor(&key, external, rcs).await); } + for (_, (external, rcs)) in std::mem::take(&mut self.lpf) { + errors.handle(ctrl.remove_lpf(external, rcs).await); + } std::mem::take(&mut self.dns); errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await); self.ip = Ipv4Addr::new(0, 0, 0, 0); @@ -356,6 +404,7 @@ impl Drop for NetService { controller: Default::default(), tor: Default::default(), lan: Default::default(), + lpf: Default::default(), }, ); tokio::spawn(async move { svc.remove_all().await.unwrap() });