use std::io;
#[derive(Debug, Clone)]
pub struct TcpFingerprint {
pub window_size: u32,
pub ttl: u8,
pub mss: u16,
pub window_scale: u8,
pub sack_permitted: bool,
pub timestamps: bool,
pub tcp_notsent_lowat: Option<u32>,
}
impl Default for TcpFingerprint {
fn default() -> Self {
Self {
window_size: 65535,
ttl: 64, mss: 1460, window_scale: 6,
sack_permitted: true,
timestamps: true,
tcp_notsent_lowat: None,
}
}
}
impl TcpFingerprint {
pub fn chrome() -> Self {
Self::default()
}
pub fn firefox() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TcpSocketBuffers {
recv: Option<usize>,
send: Option<usize>,
}
impl TcpSocketBuffers {
pub fn none() -> Self {
Self::default()
}
pub fn symmetric(bytes: usize) -> Self {
Self {
recv: Some(bytes),
send: Some(bytes),
}
}
pub fn new(recv: Option<usize>, send: Option<usize>) -> Self {
Self { recv, send }
}
pub fn is_configured(&self) -> bool {
self.recv.is_some() || self.send.is_some()
}
}
pub fn configure_tcp_socket(socket: &socket2::Socket, fp: &TcpFingerprint) -> io::Result<()> {
configure_tcp_socket_with_buffers(socket, fp, TcpSocketBuffers::none())
}
pub fn configure_tcp_socket_with_buffers(
socket: &socket2::Socket,
fp: &TcpFingerprint,
buffers: TcpSocketBuffers,
) -> io::Result<()> {
if let Some(bytes) = buffers.recv {
socket.set_recv_buffer_size(bytes)?;
}
if let Some(bytes) = buffers.send {
socket.set_send_buffer_size(bytes)?;
}
socket.set_ttl_v4(fp.ttl as u32)?;
if let Some(bytes) = fp.tcp_notsent_lowat {
set_tcp_notsent_lowat(socket, bytes)?;
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "ios"))]
fn set_tcp_notsent_lowat(socket: &socket2::Socket, bytes: u32) -> io::Result<()> {
use std::mem;
use std::os::fd::AsRawFd;
#[cfg(target_os = "linux")]
use libc::TCP_NOTSENT_LOWAT;
#[cfg(any(target_os = "macos", target_os = "ios"))]
const TCP_NOTSENT_LOWAT: i32 = 0x201;
let fd = socket.as_raw_fd();
let value = bytes;
let rc = unsafe {
libc::setsockopt(
fd,
libc::IPPROTO_TCP,
TCP_NOTSENT_LOWAT,
(&raw const value).cast(),
mem::size_of::<u32>() as libc::socklen_t,
)
};
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "ios")))]
fn set_tcp_notsent_lowat(_socket: &socket2::Socket, _bytes: u32) -> io::Result<()> {
tracing::debug!("TCP_NOTSENT_LOWAT is not supported on this platform");
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "ios"))]
#[cfg(test)]
fn read_tcp_notsent_lowat(socket: &socket2::Socket) -> io::Result<u32> {
use std::mem;
use std::os::fd::AsRawFd;
#[cfg(target_os = "linux")]
use libc::TCP_NOTSENT_LOWAT;
#[cfg(any(target_os = "macos", target_os = "ios"))]
const TCP_NOTSENT_LOWAT: i32 = 0x201;
let fd = socket.as_raw_fd();
let mut value: u32 = 0;
let mut len = mem::size_of::<u32>() as libc::socklen_t;
let rc = unsafe {
libc::getsockopt(
fd,
libc::IPPROTO_TCP,
TCP_NOTSENT_LOWAT,
(&raw mut value).cast(),
&raw mut len,
)
};
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
use socket2::{Domain, Socket, Type};
#[test]
fn test_tcp_fingerprint_defaults() {
let fp = TcpFingerprint::default();
assert_eq!(fp.window_size, 65535);
assert_eq!(fp.ttl, 64);
assert_eq!(fp.mss, 1460);
assert_eq!(fp.window_scale, 6);
assert!(fp.sack_permitted);
assert!(fp.timestamps);
assert!(fp.tcp_notsent_lowat.is_none());
}
#[test]
fn test_chrome_firefox_similar() {
let chrome = TcpFingerprint::chrome();
let firefox = TcpFingerprint::firefox();
assert_eq!(chrome.window_size, firefox.window_size);
assert_eq!(chrome.ttl, firefox.ttl);
}
#[test]
fn default_tcp_configuration_leaves_socket_buffers_untouched() {
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(socket2::Protocol::TCP)).unwrap();
let initial_recv = socket.recv_buffer_size().unwrap();
let initial_send = socket.send_buffer_size().unwrap();
configure_tcp_socket(&socket, &TcpFingerprint::default()).unwrap();
assert_eq!(socket.recv_buffer_size().unwrap(), initial_recv);
assert_eq!(socket.send_buffer_size().unwrap(), initial_send);
}
#[test]
fn explicit_tcp_socket_buffers_apply_overrides() {
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(socket2::Protocol::TCP)).unwrap();
let requested = 262_144;
configure_tcp_socket_with_buffers(
&socket,
&TcpFingerprint::default(),
TcpSocketBuffers::symmetric(requested),
)
.unwrap();
assert!(socket.recv_buffer_size().unwrap() >= requested);
assert!(socket.send_buffer_size().unwrap() >= requested);
}
#[test]
fn tcp_socket_buffers_can_configure_one_direction() {
assert!(TcpSocketBuffers::new(Some(65_536), None).is_configured());
assert!(TcpSocketBuffers::new(None, Some(65_536)).is_configured());
assert!(!TcpSocketBuffers::none().is_configured());
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "ios"))]
#[test]
fn tcp_notsent_lowat_round_trips_via_getsockopt() {
let socket = Socket::new(Domain::IPV4, Type::STREAM, None).unwrap();
let fp = TcpFingerprint {
tcp_notsent_lowat: Some(16_384),
..TcpFingerprint::default()
};
configure_tcp_socket(&socket, &fp).unwrap();
assert_eq!(read_tcp_notsent_lowat(&socket).unwrap(), 16_384);
}
}