103 lines
3.4 KiB
Rust
103 lines
3.4 KiB
Rust
//! Best-effort DSCP marking of the endpoint's UDP sockets.
|
|||
|
|
//!
|
||
|
|
//! Background transfer traffic is marked CS1 (low priority) so routers
|
||
|
|
//! that honor DSCP deprioritize it. iroh does not expose its socket fds,
|
||
|
|
//! so this walks `/proc/self/fd` for the UDP sockets bound to the
|
||
|
|
//! endpoint's ports — Linux-only by construction, like the daemon.
|
||
|
|
//! Failures are logged at debug and ignored: some environments strip or
|
||
|
|
//! forbid DSCP (may require CAP_NET_ADMIN), and that's fine.
|
||
|
|
|
||
|
|
use std::net::SocketAddr;
|
||
|
|
use std::os::fd::RawFd;
|
||
|
|
|
||
|
|
use tracing::debug;
|
||
|
|
|
||
|
|
/// DSCP CS1 (001000) in the TOS byte.
|
||
|
|
const TOS_CS1: u32 = 0x20;
|
||
|
|
|
||
|
|
extern "C" {
|
||
|
|
fn setsockopt(fd: i32, level: i32, name: i32, value: *const u32, len: u32) -> i32;
|
||
|
|
}
|
||
|
|
|
||
|
|
const IPPROTO_IP: i32 = 0;
|
||
|
|
const IP_TOS: i32 = 1;
|
||
|
|
const IPPROTO_IPV6: i32 = 41;
|
||
|
|
const IPV6_TCLASS: i32 = 67;
|
||
|
|
|
||
|
|
/// Mark every UDP socket bound to one of `addrs` with DSCP CS1.
|
||
|
|
pub fn mark_endpoint_sockets(addrs: &[SocketAddr]) {
|
||
|
|
let ports: Vec<u16> = addrs.iter().map(|a| a.port()).collect();
|
||
|
|
let inodes = udp_socket_inodes(&ports);
|
||
|
|
if inodes.is_empty() {
|
||
|
|
debug!("dscp: no matching udp sockets found");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let mut marked = 0;
|
||
|
|
for fd in socket_fds(&inodes) {
|
||
|
|
// Set both levels; one of them will apply depending on family.
|
||
|
|
let v4 = unsafe { setsockopt(fd, IPPROTO_IP, IP_TOS, &TOS_CS1, 4) };
|
||
|
|
let v6 = unsafe { setsockopt(fd, IPPROTO_IPV6, IPV6_TCLASS, &TOS_CS1, 4) };
|
||
|
|
if v4 == 0 || v6 == 0 {
|
||
|
|
marked += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
debug!(marked, "dscp: marked endpoint sockets CS1");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Inodes of UDP sockets locally bound to one of `ports`, from
|
||
|
|
/// /proc/net/udp{,6}. Format: whitespace columns, local_address is
|
||
|
|
/// `HEXIP:HEXPORT` in column 1, inode in column 9.
|
||
|
|
fn udp_socket_inodes(ports: &[u16]) -> Vec<u64> {
|
||
|
|
let mut inodes = Vec::new();
|
||
|
|
for table in ["/proc/net/udp", "/proc/net/udp6"] {
|
||
|
|
let Ok(text) = std::fs::read_to_string(table) else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
for line in text.lines().skip(1) {
|
||
|
|
let fields: Vec<&str> = line.split_whitespace().collect();
|
||
|
|
let (Some(local), Some(inode)) = (fields.get(1), fields.get(9)) else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
let Some((_, port_hex)) = local.rsplit_once(':') else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
let Ok(port) = u16::from_str_radix(port_hex, 16) else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
if ports.contains(&port) {
|
||
|
|
if let Ok(inode) = inode.parse() {
|
||
|
|
inodes.push(inode);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
inodes
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Our process's fds that are sockets with one of the given inodes.
|
||
|
|
fn socket_fds(inodes: &[u64]) -> Vec<RawFd> {
|
||
|
|
let mut fds = Vec::new();
|
||
|
|
let Ok(entries) = std::fs::read_dir("/proc/self/fd") else {
|
||
|
|
return fds;
|
||
|
|
};
|
||
|
|
for entry in entries.flatten() {
|
||
|
|
let Ok(target) = std::fs::read_link(entry.path()) else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
let target = target.to_string_lossy();
|
||
|
|
let Some(inode) = target
|
||
|
|
.strip_prefix("socket:[")
|
||
|
|
.and_then(|s| s.strip_suffix(']'))
|
||
|
|
.and_then(|s| s.parse::<u64>().ok())
|
||
|
|
else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
if inodes.contains(&inode) {
|
||
|
|
if let Ok(fd) = entry.file_name().to_string_lossy().parse::<RawFd>() {
|
||
|
|
fds.push(fd);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
fds
|
||
|
|
}
|