Skip to content

Commit 73d3332

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 73d3332

3 files changed

Lines changed: 114 additions & 11 deletions

File tree

changelog/2760.added.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
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.

src/sys/socket/sockopt.rs

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const TCP_CA_NAME_MAX: usize = 16;
4141
/// `libc::IP_ADD_MEMBERSHIP` and others. Will be passed as the third argument (`option_name`)
4242
/// to the `setsockopt` call.
4343
/// * Type of the value that you are going to set.
44-
/// * Type that implements the `Set` trait for the type from the previous item
44+
/// * Type that implements the `Set` trait for the type from the previous item
4545
/// (like `SetBool` for `bool`, `SetUsize` for `usize`, etc.).
4646
#[macro_export]
4747
macro_rules! setsockopt_impl {
@@ -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
@@ -364,9 +371,9 @@ sockopt_impl!(
364371
sockopt_impl!(
365372
#[cfg_attr(docsrs, doc(cfg(feature = "net")))]
366373
/// Used to disable Nagle's algorithm.
367-
///
374+
///
368375
/// Nagle's algorithm:
369-
///
376+
///
370377
/// Under most circumstances, TCP sends data when it is presented; when
371378
/// outstanding data has not yet been acknowledged, it gathers small amounts
372379
/// of output to be sent in a single packet once an acknowledgement is
@@ -762,6 +769,40 @@ 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 size of a saved SYN packet retrieved via [`TcpSavedSyn`]:
789+
/// IPv4/IPv6 header (up to 60 bytes) + TCP header (up to 60 bytes).
790+
#[cfg(linux_android)]
791+
pub const TCP_SAVED_SYN_MAX: usize = 120;
792+
793+
#[cfg(linux_android)]
794+
#[cfg(feature = "net")]
795+
sockopt_impl!(
796+
#[cfg_attr(docsrs, doc(cfg(all(feature = "net", target_os = "linux"))))]
797+
/// Retrieves the SYN packet saved by [`TcpSaveSyn`] on the listening
798+
/// socket. Returns the raw IP + TCP headers from the client's initial SYN
799+
/// as bytes. Returns an empty value if no SYN was saved.
800+
TcpSavedSyn,
801+
GetOnly,
802+
libc::IPPROTO_TCP,
803+
libc::TCP_SAVED_SYN,
804+
OsString<[u8; TCP_SAVED_SYN_MAX]>
805+
);
765806
#[cfg(not(any(
766807
target_os = "openbsd",
767808
target_os = "haiku",
@@ -1216,7 +1257,7 @@ sockopt_impl!(
12161257
#[cfg(any(linux_android, target_os = "freebsd"))]
12171258
#[cfg(feature = "net")]
12181259
sockopt_impl!(
1219-
/// Enables a receiving socket to retrieve the Time-to-Live (TTL) field
1260+
/// Enables a receiving socket to retrieve the Time-to-Live (TTL) field
12201261
/// from incoming IPv4 packets.
12211262
Ipv4RecvTtl,
12221263
Both,
@@ -1236,7 +1277,7 @@ sockopt_impl!(
12361277
#[cfg(any(linux_android, target_os = "freebsd"))]
12371278
#[cfg(feature = "net")]
12381279
sockopt_impl!(
1239-
/// Enables a receiving socket to retrieve the Hop Limit field
1280+
/// Enables a receiving socket to retrieve the Hop Limit field
12401281
/// (similar to TTL in IPv4) from incoming IPv6 packets.
12411282
Ipv6RecvHopLimit,
12421283
Both,
@@ -1268,10 +1309,7 @@ sockopt_impl!(
12681309
libc::IP_DONTFRAG,
12691310
bool
12701311
);
1271-
#[cfg(any(
1272-
all(linux_android, not(target_env = "uclibc")),
1273-
apple_targets
1274-
))]
1312+
#[cfg(any(all(linux_android, not(target_env = "uclibc")), apple_targets))]
12751313
sockopt_impl!(
12761314
/// Set "don't fragment packet" flag on the IPv6 packet.
12771315
Ipv6DontFrag,
@@ -1850,7 +1888,6 @@ impl<'a> Set<'a, usize> for SetUsize {
18501888
}
18511889
}
18521890

1853-
18541891
/// Getter for a `OwnedFd` value.
18551892
// Hide the docs, because it's an implementation detail of `sockopt_impl!`
18561893
#[doc(hidden)]
@@ -1900,7 +1937,9 @@ impl<'a> Set<'a, OwnedFd> for SetOwnedFd {
19001937
fn new(val: &'a OwnedFd) -> SetOwnedFd {
19011938
use std::os::fd::AsRawFd;
19021939

1903-
SetOwnedFd { val: val.as_raw_fd() as c_int }
1940+
SetOwnedFd {
1941+
val: val.as_raw_fd() as c_int,
1942+
}
19041943
}
19051944

19061945
fn ffi_ptr(&self) -> *const c_void {

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)