diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ce2005..d813963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # CHANGELOG +- [Feature] Preserve client source port on outgoing UDP connections. The endpoint now attempts to bind outgoing UDP sockets to the same source port the client application originally used, falling back to an OS-assigned ephemeral port when unavailable. This reduces NAT rebinding issues for protocols sensitive to source port changes. - [Feature] Added destination port filtering to rules config - Added `[inbound]` section for client filtering - Added `[outbound]` section for destination filtering - Rules in legacy configs are treated as `[inbound]` - - [Feature] SIGHUP credential reload support - Credentials can now be reloaded without restarting the endpoint via `systemctl reload` or SIGHUP - Added `ExecReload` directive to systemd service template diff --git a/lib/src/net_utils.rs b/lib/src/net_utils.rs index 69f8f77..89ab7a2 100644 --- a/lib/src/net_utils.rs +++ b/lib/src/net_utils.rs @@ -86,6 +86,25 @@ pub(crate) fn make_udp_socket(is_v4: bool) -> io::Result { } } +/// Try to bind a UDP socket to `preferred_port`. If the port is unavailable, +/// fall back to an OS-assigned ephemeral port. +pub(crate) fn make_udp_socket_with_preferred_port( + is_v4: bool, + preferred_port: u16, +) -> io::Result { + let bind_addr = if is_v4 { + SocketAddr::from((Ipv4Addr::UNSPECIFIED, preferred_port)) + } else { + SocketAddr::from((Ipv6Addr::UNSPECIFIED, preferred_port)) + }; + + match UdpSocket::bind(bind_addr) { + Ok(socket) => Ok(socket), + Err(_) if preferred_port != 0 => make_udp_socket(is_v4), + Err(e) => Err(e), + } +} + /// https://www.rfc-editor.org/rfc/rfc9000.html#section-16 pub(crate) const fn varint_len(x: usize) -> usize { if x <= 63 { @@ -946,4 +965,44 @@ mod tests { let compat: IpAddr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0xc0a8, 0x0101)); assert_eq!(super::unmap_ipv6(compat), IpAddr::from([192, 168, 1, 1])); } + + #[test] + fn preferred_port_binds_when_free() { + use super::make_udp_socket_with_preferred_port; + + let socket = make_udp_socket_with_preferred_port(true, 0).unwrap(); + let port = socket.local_addr().unwrap().port(); + // Port 0 means OS picks an ephemeral port, so the actual port must be non-zero. + assert_ne!(port, 0); + drop(socket); + + // Bind to a specific port that we just confirmed is available. + let socket = make_udp_socket_with_preferred_port(true, port).unwrap(); + assert_eq!(socket.local_addr().unwrap().port(), port); + } + + #[test] + fn preferred_port_falls_back_when_taken() { + use super::make_udp_socket_with_preferred_port; + + // Occupy a port. + let holder = make_udp_socket_with_preferred_port(true, 0).unwrap(); + let occupied_port = holder.local_addr().unwrap().port(); + + // Request the same port -- should fall back to an ephemeral port. + let socket = make_udp_socket_with_preferred_port(true, occupied_port).unwrap(); + assert_ne!(socket.local_addr().unwrap().port(), occupied_port); + } + + #[test] + fn preferred_port_ipv6() { + use super::make_udp_socket_with_preferred_port; + + let socket = make_udp_socket_with_preferred_port(false, 0).unwrap(); + let port = socket.local_addr().unwrap().port(); + drop(socket); + + let socket = make_udp_socket_with_preferred_port(false, port).unwrap(); + assert_eq!(socket.local_addr().unwrap().port(), port); + } } diff --git a/lib/src/udp_forwarder.rs b/lib/src/udp_forwarder.rs index c551f59..5f68642 100644 --- a/lib/src/udp_forwarder.rs +++ b/lib/src/udp_forwarder.rs @@ -206,7 +206,7 @@ impl forwarder::UdpDatagramPipeShared for MultiplexerShared { } let metrics_guard = self.context.metrics.clone().outbound_udp_socket_counter(); e.insert(Connection { - socket: Arc::new(make_udp_socket(&meta.destination)?), + socket: Arc::new(make_udp_socket(&meta.destination, meta.source.port())?), being_listened: false, _metrics_guard: metrics_guard, }); @@ -289,8 +289,9 @@ impl datagram_pipe::Sink for MultiplexerSink { } } -fn make_udp_socket(peer: &SocketAddr) -> io::Result { - let socket = net_utils::make_udp_socket(peer.is_ipv4())?; +fn make_udp_socket(peer: &SocketAddr, preferred_src_port: u16) -> io::Result { + let socket = + net_utils::make_udp_socket_with_preferred_port(peer.is_ipv4(), preferred_src_port)?; socket.connect(peer)?; socket.set_nonblocking(true)?; UdpSocket::from_std(socket)