Skip to content

Commit d162a2c

Browse files
committed
feat(socket): add TcpSaveSyn and TcpSavedSyn sockopts for Linux
Add typed setsockopt/getsockopt wrappers for TCP_SAVE_SYN (27) and TCP_SAVED_SYN (28), Linux-only socket options used for passive TCP fingerprinting (p0f). TcpSaveSyn is set on a listening socket to instruct the kernel to save a copy of each client SYN packet. TcpSavedSyn retrieves those raw IP + TCP header bytes from the accepted socket, enabling passive OS fingerprinting without active probing.
1 parent e9c43d0 commit d162a2c

3 files changed

Lines changed: 108 additions & 0 deletions

File tree

changelog/2760.added.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added `TcpSaveSyn` and `TcpSavedSyn` socket options for Linux. `TcpSaveSyn`
2+
enables saving a copy of the client SYN packet on a listening socket;
3+
`TcpSavedSyn` retrieves the raw IP+TCP header bytes from the accepted socket.
4+
Useful for passive TCP fingerprinting (p0f).

src/sys/socket/sockopt.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,13 @@ macro_rules! sockopt_impl {
248248
$crate::sys::socket::sockopt::SetOsString);
249249
};
250250

251+
($(#[$attr:meta])* $name:ident, GetOnly, $level:expr, $flag:path,
252+
OsString<$array:ty>) =>
253+
{
254+
sockopt_impl!($(#[$attr])*
255+
$name, GetOnly, $level, $flag, std::ffi::OsString, $crate::sys::socket::sockopt::GetOsString<$array>);
256+
};
257+
251258
/*
252259
* Matchers with generic getter types must be placed at the end, so
253260
* they'll only match _after_ specialized matchers fail
@@ -762,6 +769,42 @@ sockopt_impl!(
762769
libc::TCP_REPAIR,
763770
u32
764771
);
772+
#[cfg(linux_android)]
773+
#[cfg(feature = "net")]
774+
sockopt_impl!(
775+
#[cfg_attr(docsrs, doc(cfg(all(feature = "net", target_os = "linux"))))]
776+
/// If enabled, the kernel saves a copy of the SYN packet for each
777+
/// accepted connection. The saved packet can be retrieved on the
778+
/// accepted socket via [`TcpSavedSyn`]. Must be set on the listening
779+
/// socket before `accept()` is called.
780+
///
781+
/// See `tcp(7)` and `TCP_SAVE_SYN` in the Linux kernel documentation.
782+
TcpSaveSyn,
783+
Both,
784+
libc::IPPROTO_TCP,
785+
libc::TCP_SAVE_SYN,
786+
bool
787+
);
788+
/// Maximum buffer size for a saved SYN packet retrieved via [`TcpSavedSyn`].
789+
/// Theoretically capped at 120 bytes (max IPv4/IPv6 header + max TCP header),
790+
/// but set to 512 to accommodate kernel variations (e.g. Azure-patched kernels)
791+
/// that may store additional bytes.
792+
#[cfg(linux_android)]
793+
pub const TCP_SAVED_SYN_MAX: usize = 512;
794+
795+
#[cfg(linux_android)]
796+
#[cfg(feature = "net")]
797+
sockopt_impl!(
798+
#[cfg_attr(docsrs, doc(cfg(all(feature = "net", target_os = "linux"))))]
799+
/// Retrieves the SYN packet saved by [`TcpSaveSyn`] on the listening
800+
/// socket. Returns the raw IP + TCP headers from the client's initial SYN
801+
/// as bytes. Returns an empty value if no SYN was saved.
802+
TcpSavedSyn,
803+
GetOnly,
804+
libc::IPPROTO_TCP,
805+
libc::TCP_SAVED_SYN,
806+
OsString<[u8; TCP_SAVED_SYN_MAX]>
807+
);
765808
#[cfg(not(any(
766809
target_os = "openbsd",
767810
target_os = "haiku",

test/sys/test_sockopt.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,3 +1270,64 @@ pub fn test_so_attach_reuseport_cbpf() {
12701270
assert_eq!(e, nix::errno::Errno::ENOPROTOOPT);
12711271
});
12721272
}
1273+
1274+
/// Test that TCP_SAVE_SYN can be set and read back on a listening socket, and
1275+
/// that TCP_SAVED_SYN returns the raw SYN bytes on an accepted connection.
1276+
#[test]
1277+
#[cfg(linux_android)]
1278+
#[cfg_attr(qemu, ignore)]
1279+
fn test_tcp_save_syn() {
1280+
use nix::sys::socket::{
1281+
accept, bind, connect, getsockopt, listen, setsockopt, socket, sockopt,
1282+
AddressFamily, Backlog, SockFlag, SockProtocol, SockType, SockaddrIn,
1283+
};
1284+
use std::net::SocketAddrV4;
1285+
use std::os::fd::{FromRawFd, OwnedFd};
1286+
use std::str::FromStr;
1287+
1288+
// Create a listening socket and enable TCP_SAVE_SYN.
1289+
let listener = socket(
1290+
AddressFamily::Inet,
1291+
SockType::Stream,
1292+
SockFlag::empty(),
1293+
SockProtocol::Tcp,
1294+
)
1295+
.unwrap();
1296+
1297+
setsockopt(&listener, sockopt::ReuseAddr, &true).unwrap();
1298+
setsockopt(&listener, sockopt::TcpSaveSyn, &true).unwrap();
1299+
assert!(getsockopt(&listener, sockopt::TcpSaveSyn).unwrap());
1300+
1301+
let addr = SockaddrIn::from(SocketAddrV4::from_str("127.0.0.1:0").unwrap());
1302+
bind(listener.as_raw_fd(), &addr).unwrap();
1303+
listen(&listener, Backlog::new(1).unwrap()).unwrap();
1304+
1305+
// Determine the bound port.
1306+
let bound: SockaddrIn =
1307+
nix::sys::socket::getsockname(listener.as_raw_fd()).unwrap();
1308+
1309+
// Connect a client.
1310+
let client = socket(
1311+
AddressFamily::Inet,
1312+
SockType::Stream,
1313+
SockFlag::empty(),
1314+
SockProtocol::Tcp,
1315+
)
1316+
.unwrap();
1317+
connect(client.as_raw_fd(), &bound).unwrap();
1318+
1319+
// Accept the connection and verify TCP_SAVED_SYN returns SYN bytes.
1320+
let conn_fd = accept(listener.as_raw_fd()).unwrap();
1321+
let conn = unsafe { OwnedFd::from_raw_fd(conn_fd) };
1322+
1323+
let syn_bytes = getsockopt(&conn, sockopt::TcpSavedSyn).unwrap();
1324+
// The saved SYN must contain at least the IP and TCP fixed headers.
1325+
assert!(
1326+
!syn_bytes.is_empty(),
1327+
"TCP_SAVED_SYN should return SYN packet bytes"
1328+
);
1329+
1330+
// Disable TCP_SAVE_SYN and verify it reads back as false.
1331+
setsockopt(&listener, sockopt::TcpSaveSyn, &false).unwrap();
1332+
assert!(!getsockopt(&listener, sockopt::TcpSaveSyn).unwrap());
1333+
}

0 commit comments

Comments
 (0)