Skip to content

Commit 8de2e94

Browse files
committedMar 14, 2025
Add support for SO_TXTIME / SCM_TXTIME
This is an undocumented linux feature that lets you time outgoing packets.
1 parent 1cd4f00 commit 8de2e94

File tree

11 files changed

+301
-1
lines changed

11 files changed

+301
-1
lines changed
 

‎Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ compiler_builtins = { version = '0.1.49', optional = true }
3030
# libc backend can be selected via adding `--cfg=rustix_use_libc` to
3131
# `RUSTFLAGS` or enabling the `use-libc` cargo feature.
3232
[target.'cfg(all(not(rustix_use_libc), not(miri), target_os = "linux", any(target_endian = "little", any(target_arch = "s390x", target_arch = "powerpc")), any(target_arch = "arm", all(target_arch = "aarch64", target_pointer_width = "64"), target_arch = "riscv64", all(rustix_use_experimental_asm, target_arch = "powerpc"), all(rustix_use_experimental_asm, target_arch = "powerpc64"), all(rustix_use_experimental_asm, target_arch = "s390x"), all(rustix_use_experimental_asm, target_arch = "mips"), all(rustix_use_experimental_asm, target_arch = "mips32r6"), all(rustix_use_experimental_asm, target_arch = "mips64"), all(rustix_use_experimental_asm, target_arch = "mips64r6"), target_arch = "x86", all(target_arch = "x86_64", target_pointer_width = "64"))))'.dependencies]
33-
linux-raw-sys = { version = "0.9.2", default-features = false, features = ["general", "errno", "ioctl", "no_std", "elf"] }
33+
linux-raw-sys = { version = "0.9.3", default-features = false, features = ["general", "errno", "ioctl", "no_std", "elf"] }
3434
libc_errno = { package = "errno", version = "0.3.10", default-features = false, optional = true }
3535
libc = { version = "0.2.168", default-features = false, optional = true }
3636

‎src/backend/libc/net/sockopt.rs

+33
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,13 @@ use crate::net::Protocol;
5050
use crate::net::RawProtocol;
5151
#[cfg(any(linux_kernel, target_os = "fuchsia"))]
5252
use crate::net::SocketAddrV4;
53+
#[cfg(all(target_os = "linux", feature = "time"))]
54+
use crate::net::TxTimeFlags;
5355
use crate::net::{Ipv4Addr, Ipv6Addr, SocketType};
5456
#[cfg(linux_kernel)]
5557
use crate::net::{SocketAddrV6, UCred};
58+
#[cfg(all(target_os = "linux", feature = "time"))]
59+
use crate::time::ClockId;
5660
use crate::utils::as_mut_ptr;
5761
#[cfg(feature = "alloc")]
5862
#[cfg(any(
@@ -1048,6 +1052,35 @@ pub(crate) fn socket_peercred(fd: BorrowedFd<'_>) -> io::Result<UCred> {
10481052
getsockopt(fd, c::SOL_SOCKET, c::SO_PEERCRED)
10491053
}
10501054

1055+
#[cfg(all(target_os = "linux", feature = "time"))]
1056+
#[inline]
1057+
pub(crate) fn set_txtime(
1058+
fd: BorrowedFd<'_>,
1059+
clockid: ClockId,
1060+
flags: TxTimeFlags,
1061+
) -> io::Result<()> {
1062+
setsockopt(
1063+
fd,
1064+
c::SOL_SOCKET,
1065+
c::SO_TXTIME,
1066+
c::sock_txtime {
1067+
clockid: clockid as _,
1068+
flags: flags.bits(),
1069+
},
1070+
)
1071+
}
1072+
1073+
#[cfg(all(target_os = "linux", feature = "time"))]
1074+
#[inline]
1075+
pub(crate) fn get_txtime(fd: BorrowedFd<'_>) -> io::Result<(ClockId, TxTimeFlags)> {
1076+
let txtime: c::sock_txtime = getsockopt(fd, c::SOL_SOCKET, c::SO_TXTIME)?;
1077+
1078+
Ok((
1079+
txtime.clockid.try_into().map_err(|_| io::Errno::RANGE)?,
1080+
TxTimeFlags::from_bits(txtime.flags).ok_or(io::Errno::RANGE)?,
1081+
))
1082+
}
1083+
10511084
#[cfg(target_os = "linux")]
10521085
#[inline]
10531086
pub(crate) fn set_xdp_umem_reg(fd: BorrowedFd<'_>, value: XdpUmemReg) -> io::Result<()> {

‎src/backend/linux_raw/c.rs

+13
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ pub(crate) use linux_raw_sys::{
114114
},
115115
};
116116

117+
#[cfg(feature = "time")]
118+
pub use linux_raw_sys::general::__kernel_clockid_t as clockid_t;
119+
120+
#[cfg(all(feature = "net", feature = "time"))]
121+
pub use linux_raw_sys::net::{sock_txtime, SCM_TXTIME, SO_TXTIME};
122+
123+
#[cfg(all(feature = "net", feature = "time"))]
124+
pub(crate) const SOF_TXTIME_DEADLINE_MODE: u32 =
125+
linux_raw_sys::net::txtime_flags::SOF_TXTIME_DEADLINE_MODE as _;
126+
#[cfg(all(feature = "net", feature = "time"))]
127+
pub(crate) const SOF_TXTIME_REPORT_ERRORS: u32 =
128+
linux_raw_sys::net::txtime_flags::SOF_TXTIME_REPORT_ERRORS as _;
129+
117130
// Cast away bindgen's `enum` type to make these consistent with the other
118131
// `setsockopt`/`getsockopt` level values.
119132
#[cfg(feature = "net")]

‎src/backend/linux_raw/net/sockopt.rs

+33
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77

88
use crate::backend::c;
99
use crate::backend::conv::{by_mut, c_uint, ret, socklen_t};
10+
#[cfg(all(target_os = "linux", feature = "time"))]
11+
use crate::clockid::ClockId;
1012
use crate::fd::BorrowedFd;
1113
#[cfg(feature = "alloc")]
1214
use crate::ffi::CStr;
1315
use crate::io;
1416
use crate::net::sockopt::Timeout;
1517
#[cfg(target_os = "linux")]
1618
use crate::net::xdp::{XdpMmapOffsets, XdpOptionsFlags, XdpRingOffset, XdpStatistics, XdpUmemReg};
19+
#[cfg(all(target_os = "linux", feature = "time"))]
20+
use crate::net::TxTimeFlags;
1721
use crate::net::{
1822
AddressFamily, Ipv4Addr, Ipv6Addr, Protocol, RawProtocol, SocketAddrBuf, SocketAddrV4,
1923
SocketAddrV6, SocketType, UCred,
@@ -848,6 +852,35 @@ pub(crate) fn socket_peercred(fd: BorrowedFd<'_>) -> io::Result<UCred> {
848852
getsockopt(fd, c::SOL_SOCKET, linux_raw_sys::net::SO_PEERCRED)
849853
}
850854

855+
#[cfg(all(target_os = "linux", feature = "time"))]
856+
#[inline]
857+
pub(crate) fn set_txtime(
858+
fd: BorrowedFd<'_>,
859+
clockid: ClockId,
860+
flags: TxTimeFlags,
861+
) -> io::Result<()> {
862+
setsockopt(
863+
fd,
864+
c::SOL_SOCKET,
865+
c::SO_TXTIME,
866+
c::sock_txtime {
867+
clockid: clockid as _,
868+
flags: flags.bits(),
869+
},
870+
)
871+
}
872+
873+
#[cfg(all(target_os = "linux", feature = "time"))]
874+
#[inline]
875+
pub(crate) fn get_txtime(fd: BorrowedFd<'_>) -> io::Result<(ClockId, TxTimeFlags)> {
876+
let txtime: c::sock_txtime = getsockopt(fd, c::SOL_SOCKET, c::SO_TXTIME)?;
877+
878+
Ok((
879+
txtime.clockid.try_into().map_err(|_| io::Errno::RANGE)?,
880+
TxTimeFlags::from_bits(txtime.flags).ok_or(io::Errno::RANGE)?,
881+
))
882+
}
883+
851884
#[cfg(target_os = "linux")]
852885
#[inline]
853886
pub(crate) fn set_xdp_umem_reg(fd: BorrowedFd<'_>, value: XdpUmemReg) -> io::Result<()> {

‎src/clockid.rs

+61
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::backend::c;
22
use crate::fd::BorrowedFd;
3+
use crate::io;
34

45
/// `CLOCK_*` constants for use with [`clock_gettime`].
56
///
@@ -97,6 +98,51 @@ pub enum ClockId {
9798
BoottimeAlarm = bitcast!(c::CLOCK_BOOTTIME_ALARM),
9899
}
99100

101+
#[cfg(not(any(apple, target_os = "wasi")))]
102+
impl TryFrom<c::clockid_t> for ClockId {
103+
type Error = io::Errno;
104+
105+
fn try_from(value: c::clockid_t) -> Result<Self, Self::Error> {
106+
match value {
107+
c::CLOCK_REALTIME => Ok(ClockId::Realtime),
108+
c::CLOCK_MONOTONIC => Ok(ClockId::Monotonic),
109+
#[cfg(any(freebsdlike, target_os = "openbsd"))]
110+
c::CLOCK_UPTIME => Ok(ClockId::Uptime),
111+
#[cfg(not(any(
112+
solarish,
113+
target_os = "horizon",
114+
target_os = "netbsd",
115+
target_os = "redox",
116+
target_os = "vita"
117+
)))]
118+
c::CLOCK_PROCESS_CPUTIME_ID => Ok(ClockId::ProcessCPUTime),
119+
#[cfg(not(any(
120+
solarish,
121+
target_os = "horizon",
122+
target_os = "netbsd",
123+
target_os = "redox",
124+
target_os = "vita"
125+
)))]
126+
c::CLOCK_THREAD_CPUTIME_ID => Ok(ClockId::ThreadCPUTime),
127+
#[cfg(any(linux_kernel, target_os = "freebsd"))]
128+
c::CLOCK_REALTIME_COARSE => Ok(ClockId::RealtimeCoarse),
129+
#[cfg(any(linux_kernel, target_os = "freebsd"))]
130+
c::CLOCK_MONOTONIC_COARSE => Ok(ClockId::MonotonicCoarse),
131+
#[cfg(linux_kernel)]
132+
c::CLOCK_MONOTONIC_RAW => Ok(ClockId::MonotonicRaw),
133+
#[cfg(linux_kernel)]
134+
c::CLOCK_REALTIME_ALARM => Ok(ClockId::RealtimeAlarm),
135+
#[cfg(all(linux_kernel, feature = "linux_4_11"))]
136+
c::CLOCK_TAI => Ok(ClockId::Tai),
137+
#[cfg(any(linux_kernel, target_os = "fuchsia", target_os = "openbsd"))]
138+
c::CLOCK_BOOTTIME => Ok(ClockId::Boottime),
139+
#[cfg(any(linux_kernel, target_os = "fuchsia"))]
140+
c::CLOCK_BOOTTIME_ALARM => Ok(ClockId::BoottimeAlarm),
141+
_ => Err(io::Errno::RANGE),
142+
}
143+
}
144+
}
145+
100146
/// `CLOCK_*` constants for use with [`clock_gettime`].
101147
///
102148
/// These constants are always supported at runtime, so `clock_gettime` never
@@ -127,6 +173,21 @@ pub enum ClockId {
127173
ThreadCPUTime = c::CLOCK_THREAD_CPUTIME_ID,
128174
}
129175

176+
#[cfg(apple)]
177+
impl TryFrom<c::clockid_t> for ClockId {
178+
type Error = io::Errno;
179+
180+
fn try_from(value: c::clockid_t) -> Result<Self, Self::Error> {
181+
match value {
182+
c::CLOCK_REALTIME => Ok(ClockId::Realtime),
183+
c::CLOCK_MONOTONIC => Ok(ClockId::Monotonic),
184+
c::CLOCK_PROCESS_CPUTIME_ID => Ok(ClockId::ProcessCPUTime),
185+
c::CLOCK_THREAD_CPUTIME_ID => Ok(ClockId::ThreadCPUTime),
186+
_ => Err(io::Errno::RANGE),
187+
}
188+
}
189+
}
190+
130191
/// `CLOCK_*` constants for use with [`clock_gettime_dynamic`].
131192
///
132193
/// These constants may be unsupported at runtime, depending on the OS version,

‎src/net/send_recv/msg.rs

+26
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ macro_rules! cmsg_space {
6666
$len * ::core::mem::size_of::<$crate::net::UCred>(),
6767
)
6868
};
69+
(TxTime($len:expr)) => {
70+
$crate::net::__cmsg_space(
71+
$len * ::core::mem::size_of::<u64>(),
72+
)
73+
};
6974

7075
// Combo Rules
7176
($firstid:ident($firstex:expr), $($restid:ident($restex:expr)),*) => {{
@@ -94,6 +99,11 @@ macro_rules! cmsg_aligned_space {
9499
$len * ::core::mem::size_of::<$crate::net::UCred>(),
95100
)
96101
};
102+
(TxTime($len:expr)) => {
103+
$crate::net::__cmsg_aligned_space(
104+
$len * ::core::mem::size_of::<u64>(),
105+
)
106+
};
97107

98108
// Combo Rules
99109
($firstid:ident($firstex:expr), $($restid:ident($restex:expr)),*) => {{
@@ -138,6 +148,13 @@ pub enum SendAncillaryMessage<'slice, 'fd> {
138148
#[cfg(linux_kernel)]
139149
#[doc(alias = "SCM_CREDENTIAL")]
140150
ScmCredentials(UCred),
151+
/// Transmission time, in nanoseconds. The value will be interpreted by
152+
/// whichever clock was configured on the socket with [`set_txtime`].
153+
///
154+
/// [`set_txtime`]: crate::net::sockopt::set_txtime
155+
#[cfg(target_os = "linux")]
156+
#[doc(alias = "SCM_TXTIME")]
157+
TxTime(u64),
141158
}
142159

143160
impl SendAncillaryMessage<'_, '_> {
@@ -149,6 +166,8 @@ impl SendAncillaryMessage<'_, '_> {
149166
Self::ScmRights(slice) => cmsg_space!(ScmRights(slice.len())),
150167
#[cfg(linux_kernel)]
151168
Self::ScmCredentials(_) => cmsg_space!(ScmCredentials(1)),
169+
#[cfg(target_os = "linux")]
170+
Self::TxTime(_) => cmsg_space!(TxTime(1)),
152171
}
153172
}
154173
}
@@ -290,6 +309,13 @@ impl<'buf, 'slice, 'fd> SendAncillaryBuffer<'buf, 'slice, 'fd> {
290309
};
291310
self.push_ancillary(ucred_bytes, c::SOL_SOCKET as _, c::SCM_CREDENTIALS as _)
292311
}
312+
#[cfg(target_os = "linux")]
313+
SendAncillaryMessage::TxTime(tx_time) => {
314+
let tx_time_bytes = unsafe {
315+
slice::from_raw_parts(addr_of!(tx_time).cast::<u8>(), size_of_val(&tx_time))
316+
};
317+
self.push_ancillary(tx_time_bytes, c::SOL_SOCKET as _, c::SO_TXTIME as _)
318+
}
293319
}
294320
}
295321

‎src/net/sockopt.rs

+18
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@
143143
#![doc(alias = "getsockopt")]
144144
#![doc(alias = "setsockopt")]
145145

146+
#[cfg(all(target_os = "linux", feature = "time"))]
147+
use crate::clockid::ClockId;
146148
#[cfg(target_os = "linux")]
147149
use crate::net::xdp::{XdpMmapOffsets, XdpOptionsFlags, XdpStatistics, XdpUmemReg};
148150
#[cfg(not(any(
@@ -172,6 +174,8 @@ use crate::net::Protocol;
172174
use crate::net::SocketAddrV4;
173175
#[cfg(linux_kernel)]
174176
use crate::net::SocketAddrV6;
177+
#[cfg(all(target_os = "linux", feature = "time"))]
178+
use crate::net::TxTimeFlags;
175179
use crate::net::{Ipv4Addr, Ipv6Addr, SocketType};
176180
use crate::{backend, io};
177181
#[cfg(feature = "alloc")]
@@ -1524,6 +1528,20 @@ pub fn socket_peercred<Fd: AsFd>(fd: Fd) -> io::Result<super::UCred> {
15241528
backend::net::sockopt::socket_peercred(fd.as_fd())
15251529
}
15261530

1531+
/// `getsockopt(fd, SOL_SOCKET, SO_TXTIME)` — Get transmission timing configuration.
1532+
#[cfg(all(target_os = "linux", feature = "time"))]
1533+
#[doc(alias = "SO_TXTIME")]
1534+
pub fn get_txtime<Fd: AsFd>(fd: Fd) -> io::Result<(ClockId, TxTimeFlags)> {
1535+
backend::net::sockopt::get_txtime(fd.as_fd())
1536+
}
1537+
1538+
/// `setsockopt(fd, SOL_SOCKET, SO_TXTIME)` — Configure transmission timing.
1539+
#[cfg(all(target_os = "linux", feature = "time"))]
1540+
#[doc(alias = "SO_TXTIME")]
1541+
pub fn set_txtime<Fd: AsFd>(fd: Fd, clockid: ClockId, flags: TxTimeFlags) -> io::Result<()> {
1542+
backend::net::sockopt::set_txtime(fd.as_fd(), clockid, flags)
1543+
}
1544+
15271545
/// `setsockopt(fd, SOL_XDP, XDP_UMEM_REG, value)`
15281546
///
15291547
/// On kernel versions only supporting v1, the flags are ignored.

‎src/net/types.rs

+15
Original file line numberDiff line numberDiff line change
@@ -1722,6 +1722,21 @@ bitflags! {
17221722
}
17231723
}
17241724

1725+
#[cfg(all(target_os = "linux", feature = "time"))]
1726+
bitflags! {
1727+
/// Flags for use with [`set_txtime`].
1728+
///
1729+
/// [`set_txtime`]: crate::net::sockopt::set_txtime
1730+
#[repr(transparent)]
1731+
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
1732+
pub struct TxTimeFlags: u32 {
1733+
/// `SOF_TXTIME_DEADLINE_MODE`
1734+
const DEADLINE_MODE = bitcast!(c::SOF_TXTIME_DEADLINE_MODE);
1735+
/// `SOF_TXTIME_REPORT_ERRORS`
1736+
const REPORT_ERRORS = bitcast!(c::SOF_TXTIME_REPORT_ERRORS);
1737+
}
1738+
}
1739+
17251740
/// `AF_XDP` related types and constants.
17261741
#[cfg(target_os = "linux")]
17271742
pub mod xdp {

‎tests/net/sockopt.rs

+25
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ use rustix::io;
99
target_env = "newlib"
1010
))]
1111
use rustix::net::ipproto;
12+
#[cfg(target_os = "linux")]
13+
use rustix::net::TxTimeFlags;
1214
use rustix::net::{sockopt, AddressFamily, SocketType};
15+
#[cfg(target_os = "linux")]
16+
use rustix::time::ClockId;
1317
use std::net::Ipv4Addr;
1418
use std::time::Duration;
1519

@@ -625,3 +629,24 @@ fn test_sockopts_multicast_ifv6() {
625629
Err(e) => panic!("{e}"),
626630
}
627631
}
632+
633+
#[test]
634+
#[cfg(target_os = "linux")]
635+
fn test_sockopts_txtime() {
636+
crate::init();
637+
638+
let s = rustix::net::socket(AddressFamily::INET, SocketType::DGRAM, None).unwrap();
639+
640+
match sockopt::set_txtime(&s, ClockId::Monotonic, TxTimeFlags::DEADLINE_MODE) {
641+
Ok(()) => {
642+
assert_eq!(
643+
sockopt::get_txtime(&s).unwrap(),
644+
(ClockId::Monotonic, TxTimeFlags::DEADLINE_MODE)
645+
);
646+
}
647+
Err(e) if e.to_string().contains("Protocol not available") => {
648+
// Skip test on unsupported platforms
649+
}
650+
Err(e) => panic!("{e}"),
651+
}
652+
}

0 commit comments

Comments
 (0)
Please sign in to comment.