feat(app/client): expose socket bind options

This commit is contained in:
Nils Andreas Svee committed 2026-10-21 00:33:46 +07:00
1 parent 38e912632d
commit ab241c4a2b
5 files changed
+87 -4

No files matched your search

Generated
+10
View File
@@ -251,6 +251,15 @@ dependencies = [
"clap_derive",
]
[[package]]
name = "clap-num"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "822c4000301ac390e65995c62207501e3ef800a1fc441df913a5e8e4dc374816"
dependencies = [
"num-traits",
]
[[package]]
name = "clap_builder"
version = "4.6.7"
@@ -819,6 +828,7 @@ version = "0.8.0"
dependencies = [
"chrono",
"clap",
"clap-num",
"crossterm",
"ctrlc",
"futures-core",
+1
View File
@@ -49,6 +49,7 @@ tui =["dep:tokio", "dep:futures-core", "dep:irtt-client", "irtt-client/ancillary
[dependencies]
chrono = { version = "0.4.45", default-features = false, features = ["clock"], optional = true }
clap = { version = "4.6.7", features = ["derive", "env"] }
clap-num = "=1.2.0"
crossterm = { version = "0.29.0", features = ["event-stream"], optional = true }
futures-core = { version = "0.3.34", default-features = false, optional = true }
ctrlc = { version = "3.5.2", features = ["termination"] }
+50 -4
View File
@@ -1,4 +1,4 @@
use std::{fmt, time::Duration};
use std::{fmt, net::IpAddr, net::SocketAddr, time::Duration};
use clap::ValueEnum;
use irtt_client::{MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH};
@@ -70,6 +70,38 @@ pub struct CommonClientArgs {
#[arg(long, value_name = "1..=255", value_parser = parse_ttl)]
pub ttl: Option<u32>,
#[arg(
long = "bind-addr",
value_name = "IP[:PORT]",
value_parser = parse_bind_addr,
long_help = "Local IP address and optional UDP port to bind. \
If no port is specified, the operating system selects \
an ephemeral port. IPv6 addresses with an explicit \
port must use [ADDR]:PORT syntax."
)]
pub bind_addr: Option<SocketAddr>,
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
#[arg(
long = "bind-device",
value_name = "DEVICE",
help = "Bind the socket to a network device"
)]
pub bind_to_device: Option<String>,
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
#[arg(
long,
value_name = "MARK",
value_parser = clap_num::maybe_hex::<u32>,
help = "Set the outgoing socket firewall mark"
)]
pub mark: Option<u32>,
#[cfg(target_os = "freebsd")]
#[arg(long, value_name = "FIB", help = "Set the socket routing table (FIB)")]
pub fib: Option<u32>,
/// Accept safe server restrictions during negotiation.
#[arg(long)]
pub loose: bool,
@@ -77,7 +109,8 @@ pub struct CommonClientArgs {
impl fmt::Debug for CommonClientArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CommonClientArgs")
let mut debug = f.debug_struct("CommonClientArgs");
debug
.field("ipv4", &self.ipv4)
.field("ipv6", &self.ipv6)
.field("dual_stack", &self.dual_stack)
@@ -90,8 +123,14 @@ impl fmt::Debug for CommonClientArgs {
.field("server_fill", &self.server_fill)
.field("dscp", &self.dscp)
.field("ttl", &self.ttl)
.field("loose", &self.loose)
.finish()
.field("bind_addr", &self.bind_addr);
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
debug
.field("bind_to_device", &self.bind_to_device)
.field("mark", &self.mark);
#[cfg(target_os = "freebsd")]
debug.field("fib", &self.fib);
debug.field("loose", &self.loose).finish()
}
}
@@ -152,6 +191,13 @@ impl From<ReceivedStatsArg> for ReceivedStats {
}
}
fn parse_bind_addr(input: &str) -> Result<SocketAddr, String> {
input
.parse::<SocketAddr>()
.or_else(|_| input.parse::<IpAddr>().map(|ip| SocketAddr::new(ip, 0)))
.map_err(|_| "expected an IP address, optionally with a port".to_owned())
}
pub fn parse_duration(input: &str) -> Result<Duration, String> {
let (number, unit) = split_duration(input)?;
let value: u64 = number
@@ -50,6 +50,13 @@ impl CommonClientArgs {
Authentication::Hmac(HmacKey::new(key.as_bytes()))
}),
socket: SocketConfig {
bind_addr: self.bind_addr,
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
bind_to_device: self.bind_to_device.clone(),
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
mark: self.mark,
#[cfg(target_os = "freebsd")]
fib: self.fib,
ttl: self.ttl,
..SocketConfig::default()
},
+19
View File
@@ -93,6 +93,25 @@ labels; choose another alias if one collides with an explicit label.
ordinary multi-target pacing and each sends its own probes, so dual-stack can
double the number of probes per interval compared with one target per hostname.
## LOCAL SOCKET CONFIGURATION
The client supports configuring its local UDP sockets using the following options:
- `--bind-addr IP[:PORT]` binds sockets to the specified local IP address and optional port. If no port is specified, or the port is `0`, the operating system assigns an ephemeral port. IPv6 addresses with an explicit port must use `[ADDR]:PORT` syntax. The address family must match the target's resolved address family.
- `--bind-device DEVICE` (Linux, Android, Fuchsia) binds sockets to a network interface using `SO_BINDTODEVICE`.
- `--mark MARK` (Linux, Android, Fuchsia) sets the socket's firewall mark using `SO_MARK`, allowing traffic to be selected by policy routing or firewall rules. Accepts a decimal or 0x-prefixed hexadecimal unsigned 32-bit integer.
- `--fib FIB` (FreeBSD) selects a routing table using `SO_SETFIB`. The FIB is specified as a decimal unsigned 32-bit integer.
For example, bind IPv4 probes to a specific source address and interface, with a firewall mark:
```sh
irtt-client -4 --bind-addr 192.0.2.10 --bind-device wan0 --mark 1234 host.example
```
Setting a firewall mark or binding to a network device may require elevated privileges or appropriate capabilities. If the operating system rejects a socket option, socket setup fails.
A fixed local port may cause binding conflicts when multiple target sockets are created. Use an ephemeral port unless a specific source port is required. A family-specific bind address is also incompatible with targets using the other address family.
## DYNAMIC TARGET SETS FROM STANDARD INPUT
`--targets-stdin` is available only with continuous mode (`--duration 0`).