Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
528 changes: 521 additions & 7 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 2 additions & 10 deletions patchbay-runner/src/sim/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1039,16 +1039,8 @@ fn parse_step_failure(raw: &str) -> Option<StepFailureInfo> {
match k {
"index" => index = v.parse::<usize>().ok(),
"action" => action = Some(v.to_string()),
"id" => {
if !v.is_empty() {
id = Some(v.to_string());
}
}
"device" => {
if !v.is_empty() {
device = Some(v.to_string());
}
}
"id" if !v.is_empty() => id = Some(v.to_string()),
"device" if !v.is_empty() => device = Some(v.to_string()),
_ => {}
}
}
Expand Down
1 change: 1 addition & 0 deletions patchbay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ ctor = "0.6"
futures-buffered = "0.2"
hickory-resolver = { version = "0.25", default-features = false, features = ["system-config", "tokio"] }
n0-tracing-test = "0.3.0"
portmapper = { version = "0.16", default-features = false }
testdir = "0.9"
7 changes: 7 additions & 0 deletions patchbay/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ pub(crate) struct RouterConfig {
pub ra_interval_secs: u64,
/// Router Advertisement lifetime in seconds.
pub ra_lifetime_secs: u64,
/// Port mapping server protocols enabled on this router.
pub portmap: crate::portmap::PortmapConfig,
}

impl RouterConfig {
Expand Down Expand Up @@ -280,6 +282,9 @@ pub(crate) struct RouterData {
pub ra_runtime: Arc<RaRuntimeCfg>,
/// Per-router operation lock — serializes multi-step mutations.
pub op: Arc<tokio::sync::Mutex<()>>,
/// Running portmap server, if any. Started at build time when the
/// builder enables any protocol; dropped on router removal.
pub portmap_server: Option<crate::portmap::server::PortmapServer>,
}

impl RouterData {
Expand Down Expand Up @@ -784,6 +789,7 @@ impl NetworkCore {
ra_enabled: RA_DEFAULT_ENABLED,
ra_interval_secs: RA_DEFAULT_INTERVAL_SECS,
ra_lifetime_secs: RA_DEFAULT_LIFETIME_SECS,
portmap: crate::portmap::PortmapConfig::default(),
},
downlink_bridge,
uplink: None,
Expand All @@ -802,6 +808,7 @@ impl NetworkCore {
RA_DEFAULT_LIFETIME_SECS,
)),
op: Arc::new(tokio::sync::Mutex::new(())),
portmap_server: None,
},
);
id
Expand Down
1 change: 1 addition & 0 deletions patchbay/src/lab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,7 @@ impl Lab {
ra_enabled: RA_DEFAULT_ENABLED,
ra_interval_secs: RA_DEFAULT_INTERVAL_SECS,
ra_lifetime_secs: RA_DEFAULT_LIFETIME_SECS,
portmap: crate::portmap::PortmapConfig::default(),
result: Ok(()),
}
}
Expand Down
3 changes: 3 additions & 0 deletions patchbay/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ mod netns;
pub(crate) mod nft;
#[path = "tracing.rs"]
mod ns_tracing;
/// Port mapping server: UPnP IGD, NAT-PMP, and PCP.
pub mod portmap;
mod qdisc;
/// Router handle, builder, and presets.
pub(crate) mod router;
Expand All @@ -246,6 +248,7 @@ pub use lab::{
NatV6Mode, OutDir, Region, RegionLink, TestGuard,
};
pub use metrics::MetricsBuilder;
pub use portmap::{PortmapConfig, PortmapMode};
pub use router::{Router, RouterBuilder, RouterIface, RouterPreset};

pub use crate::{
Expand Down
7 changes: 6 additions & 1 deletion patchbay/src/nft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,19 @@ fn generate_nat_rules(cfg: &NatConfig, wan_if: &str, wan_ip: Ipv4Addr) -> String

let postrouting_priority = if use_fullcone_map { "srcnat" } else { "100" };

// APDF filter: only forward inbound packets matching existing conntrack flows.
// APDF filter: forward inbound packets only when they match an
// existing conntrack flow, a DNAT that the router itself installed
// (port mapping server), or a related flow. Static port forwards from
// the portmap table create `ct status dnat` packets that would
// otherwise be dropped by the blanket `iif "wan" drop` rule.
let filter_table = if cfg.filtering == NatFiltering::AddressAndPortDependent {
format!(
r#"
table ip filter {{
chain forward {{
type filter hook forward priority 0; policy accept;
iif "{wan}" ct state established,related accept
iif "{wan}" ct status dnat accept
iif "{wan}" drop
}}
}}"#,
Expand Down
109 changes: 109 additions & 0 deletions patchbay/src/portmap/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//! Configuration types for the port mapping server.

/// Which port mapping protocols a router advertises.
///
/// Maps to a [`PortmapConfig`] at build time; `None` leaves the server off.
/// Off by default on every [`RouterPreset`](crate::RouterPreset) to avoid
/// changing the traffic profile of existing tests. Opt in explicitly with
/// [`RouterBuilder::portmap`](crate::RouterBuilder::portmap) or turn on at
/// runtime with [`Router::set_portmap`](crate::Router::set_portmap).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PortmapMode {
/// No port mapping server.
#[default]
None,
/// NAT-PMP only (UDP 5351, RFC 6886).
NatPmpOnly,
/// PCP only (UDP 5351, RFC 6887).
PcpOnly,
/// UPnP IGD:1 only (SSDP + HTTP/SOAP).
UpnpOnly,
/// NAT-PMP and PCP sharing UDP 5351, no UPnP.
NatPmpAndPcp,
/// All three protocols enabled.
All,
}

/// Per-router port mapping server configuration.
///
/// Each flag toggles one protocol. Multiple protocols may be enabled at the
/// same time; NAT-PMP and PCP then share the same UDP 5351 socket as they do
/// on real gateways.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PortmapConfig {
/// Advertise NAT-PMP on UDP 5351.
pub enable_nat_pmp: bool,
/// Advertise PCP on UDP 5351.
pub enable_pcp: bool,
/// Advertise UPnP IGD via SSDP and HTTP/SOAP.
pub enable_upnp: bool,
}

impl PortmapConfig {
/// Returns a [`PortmapConfig`] whose per-protocol flags match `mode`.
///
/// Equivalent to `PortmapConfig::from(mode)`; kept for callers that
/// find the method form clearer at a call site.
pub fn from_mode(mode: PortmapMode) -> Self {
mode.into()
}

/// Returns `true` when any protocol is enabled.
#[must_use]
pub fn any_enabled(&self) -> bool {
self.enable_nat_pmp || self.enable_pcp || self.enable_upnp
}
}

impl From<PortmapMode> for PortmapConfig {
fn from(mode: PortmapMode) -> Self {
match mode {
PortmapMode::None => Self::default(),
PortmapMode::NatPmpOnly => Self {
enable_nat_pmp: true,
..Self::default()
},
PortmapMode::PcpOnly => Self {
enable_pcp: true,
..Self::default()
},
PortmapMode::UpnpOnly => Self {
enable_upnp: true,
..Self::default()
},
PortmapMode::NatPmpAndPcp => Self {
enable_nat_pmp: true,
enable_pcp: true,
enable_upnp: false,
},
PortmapMode::All => Self {
enable_nat_pmp: true,
enable_pcp: true,
enable_upnp: true,
},
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn mode_none_disables_everything() {
assert!(!PortmapConfig::from_mode(PortmapMode::None).any_enabled());
}

#[test]
fn mode_all_enables_everything() {
let cfg = PortmapConfig::from_mode(PortmapMode::All);
assert!(cfg.enable_nat_pmp && cfg.enable_pcp && cfg.enable_upnp);
}

#[test]
fn mode_nat_pmp_and_pcp_leaves_upnp_off() {
let cfg = PortmapConfig::from_mode(PortmapMode::NatPmpAndPcp);
assert!(cfg.enable_nat_pmp && cfg.enable_pcp);
assert!(!cfg.enable_upnp);
}
}
38 changes: 38 additions & 0 deletions patchbay/src/portmap/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! Port mapping server: UPnP IGD, NAT-PMP, and PCP.
//!
//! Patchbay routers can run an in-process port mapping server inside their
//! namespace that implements the three protocols common on consumer routers.
//! Devices on the downstream LAN can request external port mappings with any
//! supported protocol; granted mappings install nftables DNAT rules in a
//! dedicated `ip portmap` table so inbound WAN traffic reaches the device.
//!
//! The module is split into:
//!
//! - [`config`] for user-facing builder types.
//! - [`registry`] for the shared mapping registry and dedup logic.
//! - [`nft`] for the dedicated `ip portmap` nftables table.
//! - [`server`] for the lifecycle handle and the shared [`server::ServerContext`].
//! - [`nat_pmp`], [`pcp`], and [`upnp`] for per-protocol decoders, encoders,
//! and request handlers.
//!
//! Public API surface is intentionally small: [`PortmapMode`] and
//! [`PortmapConfig`] for configuration. Every internal helper is
//! `pub(crate)`.
//!
//! # Threat model
//!
//! All three protocols authorize clients by source IPv4 address. That is
//! trivially spoofable on a real LAN and acceptable only inside the
//! patchbay simulator, where the downstream bridge is populated solely by
//! tests. Do not reuse this code in a production gateway without moving
//! authorization to a stronger primitive.

pub use config::{PortmapConfig, PortmapMode};

mod config;
pub(crate) mod nat_pmp;
pub(crate) mod nft;
pub(crate) mod pcp;
pub(crate) mod registry;
pub(crate) mod server;
pub(crate) mod upnp;
Loading
Loading