From 7bbc89e1c086f07a62a70be6b6ad004a40517b31 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Mon, 28 Sep 2026 14:24:21 +0700 Subject: [PATCH] feat(socket): support advanced routing configuration for client socket Enable binding to a specific network interface, applying a firewall mark, or selecting a routing table (FIB) for outgoing packets on supported platforms. --- crates/irtt-client/src/config.rs | 9 ++++ crates/irtt-client/src/socket.rs | 15 +++++++ crates/irtt-client/src/socket_options.rs | 53 ++++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/crates/irtt-client/src/config.rs b/crates/irtt-client/src/config.rs index 4f3a002..d136f22 100644 --- a/crates/irtt-client/src/config.rs +++ b/crates/irtt-client/src/config.rs @@ -231,6 +231,15 @@ pub struct SocketConfig { /// `None` binds to an unspecified address with an ephemeral port matching /// the selected remote address family. pub bind_addr: Option, + /// Network interface to bind this socket to (`SO_BINDTODEVICE`). + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + pub bind_to_device: Option, + /// Firewall mark applied to outgoing packets (`SO_MARK`). + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + pub mark: Option, + /// FreeBSD FIB used for routing this socket (`SO_SETFIB`). + #[cfg(target_os = "freebsd")] + pub fib: Option, /// Optional IPv4 TTL or IPv6 hop limit applied to sent packets. /// /// `None` leaves the platform default unchanged. Values must fit in the diff --git a/crates/irtt-client/src/socket.rs b/crates/irtt-client/src/socket.rs index b1ed560..9a15b1a 100644 --- a/crates/irtt-client/src/socket.rs +++ b/crates/irtt-client/src/socket.rs @@ -12,6 +12,14 @@ use crate::{ socket_options::apply_ttl_to_socket, }; +#[cfg(any( + target_os = "android", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "linux" +))] +use crate::socket_options::apply_routing_options; + pub(crate) fn validate_open_timeouts(timeouts: &[Duration]) -> Result<(), ClientError> { if timeouts.is_empty() { return Err(ClientError::NoOpenTimeouts); @@ -115,6 +123,13 @@ fn create_connected_udp_socket( remote: SocketAddr, ) -> Result { let socket = create_prebind_udp_socket(config, remote)?; + #[cfg(any( + target_os = "android", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "linux" + ))] + apply_routing_options(&socket, config, remote)?; let bind_addr = config.bind_addr.unwrap_or_else(|| { if remote.is_ipv4() { SocketAddr::from(([0, 0, 0, 0], 0)) diff --git a/crates/irtt-client/src/socket_options.rs b/crates/irtt-client/src/socket_options.rs index 489cfc9..74f6e38 100644 --- a/crates/irtt-client/src/socket_options.rs +++ b/crates/irtt-client/src/socket_options.rs @@ -5,11 +5,64 @@ use std::{ use socket2::SockRef; +#[cfg(any( + target_os = "android", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "linux" +))] +use socket2::Socket; + use crate::{ config::{MAX_DSCP_CODEPOINT, MAX_TTL}, error::ClientError, }; +#[cfg(any( + target_os = "android", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "linux" +))] +pub(crate) fn apply_routing_options( + socket: &Socket, + config: &crate::SocketConfig, + remote: SocketAddr, +) -> Result<(), ClientError> { + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + { + if let Some(device) = &config.bind_to_device { + socket + .bind_device(Some(device.as_bytes())) + .map_err(|source| ClientError::SocketOption { + operation: "bind socket to device", + remote, + source, + })?; + } + if let Some(mark) = config.mark { + socket + .set_mark(mark) + .map_err(|source| ClientError::SocketOption { + operation: "set firewall mark", + remote, + source, + })?; + } + } + #[cfg(target_os = "freebsd")] + if let Some(fib) = config.fib { + socket + .set_fib(fib) + .map_err(|source| ClientError::SocketOption { + operation: "set FIB", + remote, + source, + })?; + } + Ok(()) +} + /// Converts a public DSCP codepoint (`0..=`[`MAX_DSCP_CODEPOINT`]) into the /// raw IP TOS / Traffic Class byte it occupies the upper six bits of. ///