diff --git a/apps/neshub/src/main.rs b/apps/neshub/src/main.rs index a2810ef1..ab5b9c0a 100644 --- a/apps/neshub/src/main.rs +++ b/apps/neshub/src/main.rs @@ -10,6 +10,7 @@ mod screenshot; mod session; mod ticket; +use std::net::{Ipv4Addr, SocketAddrV4}; use std::path::PathBuf; use std::sync::Arc; @@ -28,6 +29,16 @@ struct Args { #[arg(long, env = "NESTRI_RELAY", default_value = "default")] relay: String, + /// The UDP port to bind for IPv4, which the host forwards to this box. + /// + /// Set by the guest init from `nestri.port=`. The host installs its forward + /// and its pinned masquerade for this port before the box boots, so every + /// flow, the relay's included, leaves on one public port, and the address + /// the relay reports is the one peers can punch through to. Unset means a + /// port of the hub's own choosing, which only works where nothing NATs it. + #[arg(long, env = "NESTRI_BIND_PORT")] + bind_port: Option, + /// Path for the video IPC socket (nescapture → neshub) #[arg( long, @@ -172,6 +183,19 @@ async fn main() -> Result<()> { .alpns(ALPNS.iter().map(|a| a.to_vec()).collect::>()) .transport_config(crate::dgram::media_transport_config()); + // Replaces iroh's own `0.0.0.0:0` default rather than adding a second + // IPv4 socket; IPv6 keeps its default. + match args.bind_port { + Some(port) => { + builder = builder.bind_addr(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port))?; + tracing::info!(port, "binding the host's forwarded port"); + } + None => tracing::info!( + "no NESTRI_BIND_PORT, so this hub picks its own port; behind the host's NAT, \ + peers behind a NAT of their own will stay on the relay" + ), + } + match args.relay.as_str() { "default" | "" => { builder = builder.relay_mode(iroh::endpoint::RelayMode::Default); @@ -247,11 +271,17 @@ async fn main() -> Result<()> { let mut waited = 0u32; while sock.send_to(&bytes, &cmd_path).is_err() { if waited == 0 { - tracing::info!("nescapture is not listening at {} yet; waiting", cmd_path.display()); + tracing::info!( + "nescapture is not listening at {} yet; waiting", + cmd_path.display() + ); } waited += 1; if waited > 600 { - tracing::warn!("nescapture cmd send failed at {} for a minute; dropped", cmd_path.display()); + tracing::warn!( + "nescapture cmd send failed at {} for a minute; dropped", + cmd_path.display() + ); break; } tokio::time::sleep(std::time::Duration::from_millis(100)).await; @@ -433,7 +463,8 @@ async fn main() -> Result<()> { let stats_ipc = args.stats_ipc.clone(); let stats_tx_clone = nescope_stats_tx.clone(); tokio::spawn({ - let (mgr, controller, cmd_tx) = (session_manager.clone(), controller.clone(), cmd_tx.clone()); + let (mgr, controller, cmd_tx) = + (session_manager.clone(), controller.clone(), cmd_tx.clone()); async move { ipc_listener::run_video_listener(video_ipc, mgr, controller, cmd_tx).await } }); tokio::spawn({ diff --git a/apps/nesinit/src/services.rs b/apps/nesinit/src/services.rs index 1e5c1027..1bc36792 100644 --- a/apps/nesinit/src/services.rs +++ b/apps/nesinit/src/services.rs @@ -494,6 +494,12 @@ impl Stack { if let Some(kbps) = self.video.bitrate_kbps { command.env("NESTRI_MAX_BITRATE", kbps.to_string()); } + // The port the host forwarded, from the command line, by the same + // route: `neshub` binds it so its first packet already leaves through + // the host's pinned masquerade. See `system::media_port`. + if let Some(port) = crate::system::media_port() { + command.env("NESTRI_BIND_PORT", port.to_string()); + } // The service's own entry last, so a service that states one of these // for itself wins over the defaults above. command.envs(service.env.iter().copied()); @@ -911,7 +917,10 @@ mod tests { #[test] fn the_system_bus_is_waited_for_before_what_connects_to_it() { let bus = STACK.iter().find(|s| s.name == "dbus-system").unwrap(); - assert!(bus.ready.is_some(), "polkitd and rtkit would find no socket"); + assert!( + bus.ready.is_some(), + "polkitd and rtkit would find no socket" + ); for name in ["polkitd", "rtkit"] { let svc = STACK.iter().find(|s| s.name == name).unwrap(); assert!(svc.ready_name.is_some(), "{name} is reached by name"); diff --git a/apps/nesinit/src/system.rs b/apps/nesinit/src/system.rs index 8246b58a..2f78bc88 100644 --- a/apps/nesinit/src/system.rs +++ b/apps/nesinit/src/system.rs @@ -463,6 +463,36 @@ fn resolver(cmdline: &str) { tracing::info!(%server, "the box resolves through this"); } +/// The UDP port the host forwarded to this box, which `neshub` binds. +/// +/// The host has to choose it, not the hub: the host installs its forward and +/// the masquerade that keeps the box's flows on one public port before the box +/// boots, and both name this port. A hub on a port of its own choosing sends +/// its first packets past them, and from then on the relay sees one public port +/// while peers see another, so hole punching fails against any NAT. +/// +/// `None` when the command line says nothing, or says something that is not a +/// port; the hub then picks its own, as it did before the host chose one. +pub fn media_port() -> Option { + let cmdline = std::fs::read_to_string("/proc/cmdline").unwrap_or_default(); + port(&cmdline) +} + +fn port(cmdline: &str) -> Option { + let value = parameter(cmdline, "port")?; + match value.parse::() { + Ok(port) if port != 0 => Some(port), + _ => { + tracing::warn!( + %value, + "nestri.port= is not a port, so the hub picks its own and peers behind a NAT \ + stay on the relay" + ); + None + } + } +} + /// Read one `nestri.=` from a kernel command line. /// /// Split out because this is the part worth asserting: the rest of `network` @@ -638,6 +668,19 @@ mod tests { assert_eq!(parameter("othernestri.ip=1.2.3.4", "ip"), None); } + /// The host's port is read as a port, and anything else leaves the hub + /// to pick its own rather than binding something the host did not forward. + #[test] + fn the_media_port_is_the_hosts_or_none() { + let cmdline = "console=hvc0 nestri.ip=10.213.0.2/24 nestri.gw=10.213.0.1 \ + nestri.dns=1.1.1.1 nestri.port=47900"; + assert_eq!(port(cmdline), Some(47900)); + assert_eq!(port("console=hvc0 nestri.ip=10.213.0.2/24"), None); + assert_eq!(port("nestri.port=0"), None); + assert_eq!(port("nestri.port=70000"), None); + assert_eq!(port("nestri.port=hub"), None); + } + /// Each of these is a thing that stops working, said in those terms. The /// same rule the early filesystems hold themselves to. #[test]