From f41710c892899dcfec94a06cae65c3c527e7119f Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 18 Dec 2025 20:19:08 -0700 Subject: [PATCH] dynamic subnet in port forward --- build/lib/scripts/forward-port | 12 ++--- core/startos/src/net/forward.rs | 66 ++++++++++++++++++-------- core/startos/src/net/net_controller.rs | 21 +++++++- core/startos/src/s9pk/v2/pack.rs | 2 + core/startos/src/tunnel/api.rs | 17 ++++++- core/startos/src/tunnel/context.rs | 16 ++++++- 6 files changed, 105 insertions(+), 29 deletions(-) diff --git a/build/lib/scripts/forward-port b/build/lib/scripts/forward-port index ec669bc02..a6c58259b 100755 --- a/build/lib/scripts/forward-port +++ b/build/lib/scripts/forward-port @@ -1,11 +1,11 @@ #!/bin/bash -if [ -z "$sip" ] || [ -z "$dip" ] || [ -z "$sport" ] || [ -z "$dport" ]; then +if [ -z "$sip" ] || [ -z "$dip" ] || [ -z "$dprefix" ] || [ -z "$sport" ] || [ -z "$dport" ]; then >&2 echo 'missing required env var' exit 1 fi -NAME="F$(echo "$sip:$sport -> $dip:$dport" | sha256sum | head -c 15)" +NAME="F$(echo "$sip:$sport -> $dip/$dprefix:$dport" | sha256sum | head -c 15)" for kind in INPUT FORWARD ACCEPT; do if ! iptables -C $kind -j "${NAME}_${kind}" 2> /dev/null; then @@ -40,10 +40,10 @@ iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p udp --dport "$sport" -j DNAT iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport" iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport" -iptables -t nat -A ${NAME}_PREROUTING -s "$dip/24" -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport" -iptables -t nat -A ${NAME}_PREROUTING -s "$dip/24" -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport" -iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/24" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE -iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/24" -d "$dip" -p udp --dport "$dport" -j MASQUERADE +iptables -t nat -A ${NAME}_PREROUTING -s "$dip/$dprefix" -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport" +iptables -t nat -A ${NAME}_PREROUTING -s "$dip/$dprefix" -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport" +iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE +iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p udp --dport "$dport" -j MASQUERADE iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT diff --git a/core/startos/src/net/forward.rs b/core/startos/src/net/forward.rs index a9451e4e6..25f0b2116 100644 --- a/core/startos/src/net/forward.rs +++ b/core/startos/src/net/forward.rs @@ -78,6 +78,7 @@ pub fn forward_api() -> ParentHandler { struct ForwardMapping { source: SocketAddrV4, target: SocketAddrV4, + target_prefix: u8, rc: Weak<()>, } @@ -91,6 +92,7 @@ impl PortForwardState { &mut self, source: SocketAddrV4, target: SocketAddrV4, + target_prefix: u8, ) -> Result, Error> { if let Some(existing) = self.mappings.get_mut(&source) { if existing.target == target { @@ -104,18 +106,19 @@ impl PortForwardState { } else { // Different target, need to remove old and add new if let Some(mapping) = self.mappings.remove(&source) { - unforward(mapping.source, mapping.target).await?; + unforward(mapping.source, mapping.target, mapping.target_prefix).await?; } } } let rc = Arc::new(()); - forward(source, target).await?; + forward(source, target, target_prefix).await?; self.mappings.insert( source, ForwardMapping { source, target, + target_prefix, rc: Arc::downgrade(&rc), }, ); @@ -133,7 +136,7 @@ impl PortForwardState { for source in to_remove { if let Some(mapping) = self.mappings.remove(&source) { - unforward(mapping.source, mapping.target).await?; + unforward(mapping.source, mapping.target, mapping.target_prefix).await?; } } Ok(()) @@ -154,7 +157,9 @@ impl Drop for PortForwardState { let mappings = std::mem::take(&mut self.mappings); tokio::spawn(async move { for (_, mapping) in mappings { - unforward(mapping.source, mapping.target).await.log_err(); + unforward(mapping.source, mapping.target, mapping.target_prefix) + .await + .log_err(); } }); } @@ -165,6 +170,7 @@ enum PortForwardCommand { AddForward { source: SocketAddrV4, target: SocketAddrV4, + target_prefix: u8, respond: oneshot::Sender, Error>>, }, Gc { @@ -244,9 +250,10 @@ impl PortForwardController { PortForwardCommand::AddForward { source, target, + target_prefix, respond, } => { - let result = state.add_forward(source, target).await; + let result = state.add_forward(source, target, target_prefix).await; respond.send(result).ok(); } PortForwardCommand::Gc { respond } => { @@ -270,12 +277,14 @@ impl PortForwardController { &self, source: SocketAddrV4, target: SocketAddrV4, + target_prefix: u8, ) -> Result, Error> { let (send, recv) = oneshot::channel(); self.req .send(PortForwardCommand::AddForward { source, target, + target_prefix, respond: send, }) .map_err(err_has_exited)?; @@ -305,6 +314,7 @@ impl PortForwardController { struct InterfaceForwardRequest { external: u16, target: SocketAddrV4, + target_prefix: u8, filter: DynInterfaceFilter, rc: Arc<()>, } @@ -312,7 +322,7 @@ struct InterfaceForwardRequest { #[derive(Clone)] struct InterfaceForwardEntry { external: u16, - filter: BTreeMap)>, + filter: BTreeMap)>, // Maps source SocketAddr -> strong reference for the forward created in PortForwardController forwards: BTreeMap>, } @@ -343,12 +353,12 @@ impl InterfaceForwardEntry { let mut keep = BTreeSet::::new(); for (iface, info) in ip_info.iter() { - if let Some(target) = self + if let Some((target, target_prefix)) = self .filter .iter() - .filter(|(_, (_, rc))| rc.strong_count() > 0) + .filter(|(_, (_, _, rc))| rc.strong_count() > 0) .find(|(filter, _)| filter.filter(iface, info)) - .map(|(_, (target, _))| *target) + .map(|(_, (target, target_prefix, _))| (*target, *target_prefix)) { if let Some(ip_info) = &info.ip_info { for addr in ip_info.subnets.iter().filter_map(|net| { @@ -360,7 +370,9 @@ impl InterfaceForwardEntry { }) { keep.insert(addr); if !self.forwards.contains_key(&addr) { - let rc = port_forward.add_forward(addr, target).await?; + let rc = port_forward + .add_forward(addr, target, target_prefix) + .await?; self.forwards.insert(addr, rc); } } @@ -379,6 +391,7 @@ impl InterfaceForwardEntry { InterfaceForwardRequest { external, target, + target_prefix, filter, mut rc, }: InterfaceForwardRequest, @@ -395,15 +408,16 @@ impl InterfaceForwardEntry { let entry = self .filter .entry(filter) - .or_insert_with(|| (target, Arc::downgrade(&rc))); + .or_insert_with(|| (target, target_prefix, Arc::downgrade(&rc))); if entry.0 != target { entry.0 = target; - entry.1 = Arc::downgrade(&rc); + entry.1 = target_prefix; + entry.2 = Arc::downgrade(&rc); } - if let Some(existing) = entry.1.upgrade() { + if let Some(existing) = entry.2.upgrade() { rc = existing; } else { - entry.1 = Arc::downgrade(&rc); + entry.2 = Arc::downgrade(&rc); } self.update(ip_info, port_forward).await?; @@ -416,7 +430,7 @@ impl InterfaceForwardEntry { ip_info: &OrdMap, port_forward: &PortForwardController, ) -> Result<(), Error> { - self.filter.retain(|_, (_, rc)| rc.strong_count() > 0); + self.filter.retain(|_, (_, _, rc)| rc.strong_count() > 0); self.update(ip_info, port_forward).await } @@ -474,6 +488,7 @@ pub struct ForwardTable(pub BTreeMap); #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ForwardTarget { pub target: SocketAddrV4, + pub target_prefix: u8, pub filter: String, } @@ -487,12 +502,13 @@ impl From<&InterfaceForwardState> for ForwardTable { entry .filter .iter() - .filter(|(_, (_, rc))| rc.strong_count() > 0) - .map(|(filter, (target, _))| { + .filter(|(_, (_, _, rc))| rc.strong_count() > 0) + .map(|(filter, (target, target_prefix, _))| { ( entry.external, ForwardTarget { target: *target, + target_prefix: *target_prefix, filter: format!("{:#?}", filter), }, ) @@ -573,6 +589,7 @@ impl InterfacePortForwardController { external: u16, filter: DynInterfaceFilter, target: SocketAddrV4, + target_prefix: u8, ) -> Result, Error> { let rc = Arc::new(()); let (send, recv) = oneshot::channel(); @@ -581,6 +598,7 @@ impl InterfacePortForwardController { InterfaceForwardRequest { external, target, + target_prefix, filter, rc, }, @@ -609,10 +627,15 @@ impl InterfacePortForwardController { } } -async fn forward(source: SocketAddrV4, target: SocketAddrV4) -> Result<(), Error> { +async fn forward( + source: SocketAddrV4, + target: SocketAddrV4, + target_prefix: u8, +) -> Result<(), Error> { Command::new("/usr/lib/startos/scripts/forward-port") .env("sip", source.ip().to_string()) .env("dip", target.ip().to_string()) + .env("dprefix", target_prefix.to_string()) .env("sport", source.port().to_string()) .env("dport", target.port().to_string()) .invoke(ErrorKind::Network) @@ -620,11 +643,16 @@ async fn forward(source: SocketAddrV4, target: SocketAddrV4) -> Result<(), Error Ok(()) } -async fn unforward(source: SocketAddrV4, target: SocketAddrV4) -> Result<(), Error> { +async fn unforward( + source: SocketAddrV4, + target: SocketAddrV4, + target_prefix: u8, +) -> Result<(), Error> { Command::new("/usr/lib/startos/scripts/forward-port") .env("UNDO", "1") .env("sip", source.ip().to_string()) .env("dip", target.ip().to_string()) + .env("dprefix", target_prefix.to_string()) .env("sport", source.port().to_string()) .env("dport", target.port().to_string()) .invoke(ErrorKind::Network) diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index 098726802..dc46fde33 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -1,5 +1,5 @@ use std::collections::{BTreeMap, BTreeSet}; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::{Arc, Weak}; use color_eyre::eyre::eyre; @@ -693,7 +693,24 @@ impl NetServiceData { ( internal, filter.clone(), - ctrl.forward.add(external, filter, internal).await?, + ctrl.forward + .add( + external, + filter, + internal, + net_ifaces + .iter() + .find_map(|(_, i)| { + i.ip_info.as_ref().and_then(|i| { + i.subnets.iter().find(|i| { + i.contains(&IpAddr::from(*internal.ip())) + }) + }) + }) + .map(|s| s.prefix_len()) + .unwrap_or(32), + ) + .await?, ) }, ); diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs index 4e714eff7..cf42d6e90 100644 --- a/core/startos/src/s9pk/v2/pack.rs +++ b/core/startos/src/s9pk/v2/pack.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; +use std::process::Stdio; use std::sync::{Arc, LazyLock, OnceLock}; use clap::Parser; @@ -39,6 +40,7 @@ pub static CONTAINER_TOOL: LazyLock<&'static str> = LazyLock::new(|| { if *PREFER_DOCKER.get_or_init(|| false) { if std::process::Command::new("which") .arg("docker") + .stdout(Stdio::null()) .status() .map_or(false, |o| o.success()) { diff --git a/core/startos/src/tunnel/api.rs b/core/startos/src/tunnel/api.rs index ac6e1cdf3..22b284623 100644 --- a/core/startos/src/tunnel/api.rs +++ b/core/startos/src/tunnel/api.rs @@ -435,7 +435,22 @@ pub async fn add_forward( ctx: TunnelContext, AddPortForwardParams { source, target }: AddPortForwardParams, ) -> Result<(), Error> { - let rc = ctx.forward.add_forward(source, target).await?; + let prefix = ctx + .net_iface + .peek(|i| { + i.iter() + .find_map(|(_, i)| { + i.ip_info.as_ref().and_then(|i| { + i.subnets + .iter() + .find(|s| s.contains(&IpAddr::from(*target.ip()))) + }) + }) + .cloned() + }) + .map(|s| s.prefix_len()) + .unwrap_or(32); + let rc = ctx.forward.add_forward(source, target, prefix).await?; ctx.active_forwards.mutate(|m| { m.insert(source, rc); }); diff --git a/core/startos/src/tunnel/context.rs b/core/startos/src/tunnel/context.rs index daa2164ec..9d1c22655 100644 --- a/core/startos/src/tunnel/context.rs +++ b/core/startos/src/tunnel/context.rs @@ -185,7 +185,21 @@ impl TunnelContext { let mut active_forwards = BTreeMap::new(); for (from, to) in peek.as_port_forwards().de()?.0 { - active_forwards.insert(from, forward.add_forward(from, to).await?); + let prefix = net_iface + .peek(|i| { + i.iter() + .find_map(|(_, i)| { + i.ip_info.as_ref().and_then(|i| { + i.subnets + .iter() + .find(|s| s.contains(&IpAddr::from(*to.ip()))) + }) + }) + .cloned() + }) + .map(|s| s.prefix_len()) + .unwrap_or(32); + active_forwards.insert(from, forward.add_forward(from, to, prefix).await?); } Ok(Self(Arc::new(TunnelContextSeed {