dynamic subnet in port forward

This commit is contained in:
Aiden McClelland
2025-12-18 20:19:08 -07:00
parent df3f79f282
commit f41710c892
6 changed files with 105 additions and 29 deletions

View File

@@ -1,11 +1,11 @@
#!/bin/bash #!/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' >&2 echo 'missing required env var'
exit 1 exit 1
fi 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 for kind in INPUT FORWARD ACCEPT; do
if ! iptables -C $kind -j "${NAME}_${kind}" 2> /dev/null; then 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 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}_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/$dprefix" -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}_PREROUTING -s "$dip/$dprefix" -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/$dprefix" -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}_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 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 iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT

View File

@@ -78,6 +78,7 @@ pub fn forward_api<C: Context>() -> ParentHandler<C> {
struct ForwardMapping { struct ForwardMapping {
source: SocketAddrV4, source: SocketAddrV4,
target: SocketAddrV4, target: SocketAddrV4,
target_prefix: u8,
rc: Weak<()>, rc: Weak<()>,
} }
@@ -91,6 +92,7 @@ impl PortForwardState {
&mut self, &mut self,
source: SocketAddrV4, source: SocketAddrV4,
target: SocketAddrV4, target: SocketAddrV4,
target_prefix: u8,
) -> Result<Arc<()>, Error> { ) -> Result<Arc<()>, Error> {
if let Some(existing) = self.mappings.get_mut(&source) { if let Some(existing) = self.mappings.get_mut(&source) {
if existing.target == target { if existing.target == target {
@@ -104,18 +106,19 @@ impl PortForwardState {
} else { } else {
// Different target, need to remove old and add new // Different target, need to remove old and add new
if let Some(mapping) = self.mappings.remove(&source) { 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(()); let rc = Arc::new(());
forward(source, target).await?; forward(source, target, target_prefix).await?;
self.mappings.insert( self.mappings.insert(
source, source,
ForwardMapping { ForwardMapping {
source, source,
target, target,
target_prefix,
rc: Arc::downgrade(&rc), rc: Arc::downgrade(&rc),
}, },
); );
@@ -133,7 +136,7 @@ impl PortForwardState {
for source in to_remove { for source in to_remove {
if let Some(mapping) = self.mappings.remove(&source) { if let Some(mapping) = self.mappings.remove(&source) {
unforward(mapping.source, mapping.target).await?; unforward(mapping.source, mapping.target, mapping.target_prefix).await?;
} }
} }
Ok(()) Ok(())
@@ -154,7 +157,9 @@ impl Drop for PortForwardState {
let mappings = std::mem::take(&mut self.mappings); let mappings = std::mem::take(&mut self.mappings);
tokio::spawn(async move { tokio::spawn(async move {
for (_, mapping) in mappings { 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 { AddForward {
source: SocketAddrV4, source: SocketAddrV4,
target: SocketAddrV4, target: SocketAddrV4,
target_prefix: u8,
respond: oneshot::Sender<Result<Arc<()>, Error>>, respond: oneshot::Sender<Result<Arc<()>, Error>>,
}, },
Gc { Gc {
@@ -244,9 +250,10 @@ impl PortForwardController {
PortForwardCommand::AddForward { PortForwardCommand::AddForward {
source, source,
target, target,
target_prefix,
respond, respond,
} => { } => {
let result = state.add_forward(source, target).await; let result = state.add_forward(source, target, target_prefix).await;
respond.send(result).ok(); respond.send(result).ok();
} }
PortForwardCommand::Gc { respond } => { PortForwardCommand::Gc { respond } => {
@@ -270,12 +277,14 @@ impl PortForwardController {
&self, &self,
source: SocketAddrV4, source: SocketAddrV4,
target: SocketAddrV4, target: SocketAddrV4,
target_prefix: u8,
) -> Result<Arc<()>, Error> { ) -> Result<Arc<()>, Error> {
let (send, recv) = oneshot::channel(); let (send, recv) = oneshot::channel();
self.req self.req
.send(PortForwardCommand::AddForward { .send(PortForwardCommand::AddForward {
source, source,
target, target,
target_prefix,
respond: send, respond: send,
}) })
.map_err(err_has_exited)?; .map_err(err_has_exited)?;
@@ -305,6 +314,7 @@ impl PortForwardController {
struct InterfaceForwardRequest { struct InterfaceForwardRequest {
external: u16, external: u16,
target: SocketAddrV4, target: SocketAddrV4,
target_prefix: u8,
filter: DynInterfaceFilter, filter: DynInterfaceFilter,
rc: Arc<()>, rc: Arc<()>,
} }
@@ -312,7 +322,7 @@ struct InterfaceForwardRequest {
#[derive(Clone)] #[derive(Clone)]
struct InterfaceForwardEntry { struct InterfaceForwardEntry {
external: u16, external: u16,
filter: BTreeMap<DynInterfaceFilter, (SocketAddrV4, Weak<()>)>, filter: BTreeMap<DynInterfaceFilter, (SocketAddrV4, u8, Weak<()>)>,
// Maps source SocketAddr -> strong reference for the forward created in PortForwardController // Maps source SocketAddr -> strong reference for the forward created in PortForwardController
forwards: BTreeMap<SocketAddrV4, Arc<()>>, forwards: BTreeMap<SocketAddrV4, Arc<()>>,
} }
@@ -343,12 +353,12 @@ impl InterfaceForwardEntry {
let mut keep = BTreeSet::<SocketAddrV4>::new(); let mut keep = BTreeSet::<SocketAddrV4>::new();
for (iface, info) in ip_info.iter() { for (iface, info) in ip_info.iter() {
if let Some(target) = self if let Some((target, target_prefix)) = self
.filter .filter
.iter() .iter()
.filter(|(_, (_, rc))| rc.strong_count() > 0) .filter(|(_, (_, _, rc))| rc.strong_count() > 0)
.find(|(filter, _)| filter.filter(iface, info)) .find(|(filter, _)| filter.filter(iface, info))
.map(|(_, (target, _))| *target) .map(|(_, (target, target_prefix, _))| (*target, *target_prefix))
{ {
if let Some(ip_info) = &info.ip_info { if let Some(ip_info) = &info.ip_info {
for addr in ip_info.subnets.iter().filter_map(|net| { for addr in ip_info.subnets.iter().filter_map(|net| {
@@ -360,7 +370,9 @@ impl InterfaceForwardEntry {
}) { }) {
keep.insert(addr); keep.insert(addr);
if !self.forwards.contains_key(&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); self.forwards.insert(addr, rc);
} }
} }
@@ -379,6 +391,7 @@ impl InterfaceForwardEntry {
InterfaceForwardRequest { InterfaceForwardRequest {
external, external,
target, target,
target_prefix,
filter, filter,
mut rc, mut rc,
}: InterfaceForwardRequest, }: InterfaceForwardRequest,
@@ -395,15 +408,16 @@ impl InterfaceForwardEntry {
let entry = self let entry = self
.filter .filter
.entry(filter) .entry(filter)
.or_insert_with(|| (target, Arc::downgrade(&rc))); .or_insert_with(|| (target, target_prefix, Arc::downgrade(&rc)));
if entry.0 != target { if entry.0 != target {
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; rc = existing;
} else { } else {
entry.1 = Arc::downgrade(&rc); entry.2 = Arc::downgrade(&rc);
} }
self.update(ip_info, port_forward).await?; self.update(ip_info, port_forward).await?;
@@ -416,7 +430,7 @@ impl InterfaceForwardEntry {
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>, ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
port_forward: &PortForwardController, port_forward: &PortForwardController,
) -> Result<(), Error> { ) -> 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 self.update(ip_info, port_forward).await
} }
@@ -474,6 +488,7 @@ pub struct ForwardTable(pub BTreeMap<u16, ForwardTarget>);
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ForwardTarget { pub struct ForwardTarget {
pub target: SocketAddrV4, pub target: SocketAddrV4,
pub target_prefix: u8,
pub filter: String, pub filter: String,
} }
@@ -487,12 +502,13 @@ impl From<&InterfaceForwardState> for ForwardTable {
entry entry
.filter .filter
.iter() .iter()
.filter(|(_, (_, rc))| rc.strong_count() > 0) .filter(|(_, (_, _, rc))| rc.strong_count() > 0)
.map(|(filter, (target, _))| { .map(|(filter, (target, target_prefix, _))| {
( (
entry.external, entry.external,
ForwardTarget { ForwardTarget {
target: *target, target: *target,
target_prefix: *target_prefix,
filter: format!("{:#?}", filter), filter: format!("{:#?}", filter),
}, },
) )
@@ -573,6 +589,7 @@ impl InterfacePortForwardController {
external: u16, external: u16,
filter: DynInterfaceFilter, filter: DynInterfaceFilter,
target: SocketAddrV4, target: SocketAddrV4,
target_prefix: u8,
) -> Result<Arc<()>, Error> { ) -> Result<Arc<()>, Error> {
let rc = Arc::new(()); let rc = Arc::new(());
let (send, recv) = oneshot::channel(); let (send, recv) = oneshot::channel();
@@ -581,6 +598,7 @@ impl InterfacePortForwardController {
InterfaceForwardRequest { InterfaceForwardRequest {
external, external,
target, target,
target_prefix,
filter, filter,
rc, 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") Command::new("/usr/lib/startos/scripts/forward-port")
.env("sip", source.ip().to_string()) .env("sip", source.ip().to_string())
.env("dip", target.ip().to_string()) .env("dip", target.ip().to_string())
.env("dprefix", target_prefix.to_string())
.env("sport", source.port().to_string()) .env("sport", source.port().to_string())
.env("dport", target.port().to_string()) .env("dport", target.port().to_string())
.invoke(ErrorKind::Network) .invoke(ErrorKind::Network)
@@ -620,11 +643,16 @@ async fn forward(source: SocketAddrV4, target: SocketAddrV4) -> Result<(), Error
Ok(()) 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") Command::new("/usr/lib/startos/scripts/forward-port")
.env("UNDO", "1") .env("UNDO", "1")
.env("sip", source.ip().to_string()) .env("sip", source.ip().to_string())
.env("dip", target.ip().to_string()) .env("dip", target.ip().to_string())
.env("dprefix", target_prefix.to_string())
.env("sport", source.port().to_string()) .env("sport", source.port().to_string())
.env("dport", target.port().to_string()) .env("dport", target.port().to_string())
.invoke(ErrorKind::Network) .invoke(ErrorKind::Network)

View File

@@ -1,5 +1,5 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
@@ -693,7 +693,24 @@ impl NetServiceData {
( (
internal, internal,
filter.clone(), 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?,
) )
}, },
); );

View File

@@ -1,5 +1,6 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::{Arc, LazyLock, OnceLock}; use std::sync::{Arc, LazyLock, OnceLock};
use clap::Parser; use clap::Parser;
@@ -39,6 +40,7 @@ pub static CONTAINER_TOOL: LazyLock<&'static str> = LazyLock::new(|| {
if *PREFER_DOCKER.get_or_init(|| false) { if *PREFER_DOCKER.get_or_init(|| false) {
if std::process::Command::new("which") if std::process::Command::new("which")
.arg("docker") .arg("docker")
.stdout(Stdio::null())
.status() .status()
.map_or(false, |o| o.success()) .map_or(false, |o| o.success())
{ {

View File

@@ -435,7 +435,22 @@ pub async fn add_forward(
ctx: TunnelContext, ctx: TunnelContext,
AddPortForwardParams { source, target }: AddPortForwardParams, AddPortForwardParams { source, target }: AddPortForwardParams,
) -> Result<(), Error> { ) -> 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| { ctx.active_forwards.mutate(|m| {
m.insert(source, rc); m.insert(source, rc);
}); });

View File

@@ -185,7 +185,21 @@ impl TunnelContext {
let mut active_forwards = BTreeMap::new(); let mut active_forwards = BTreeMap::new();
for (from, to) in peek.as_port_forwards().de()?.0 { 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 { Ok(Self(Arc::new(TunnelContextSeed {