ntp-udp-1.1.2/.cargo_vcs_info.json0000644000000001450000000000100124240ustar { "git": { "sha1": "6751bef01a2469c0824f1db8710b7a3418b824a8" }, "path_in_vcs": "ntp-udp" }ntp-udp-1.1.2/COPYING000064400000000000000000000005111046102023000122440ustar 00000000000000Copyright (c) 2022-2023 Tweede Golf and Contributors Except as otherwise noted (below and/or in individual files), ntpd-rs is licensed under the Apache License, Version 2.0 or or the MIT license or , at your option. ntp-udp-1.1.2/Cargo.toml0000644000000022440000000000100104240ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.67" name = "ntp-udp" version = "1.1.2" publish = true description = "ntpd-rs networking and timestamping layer" homepage = "https://github.com/pendulum-project/ntpd-rs" readme = "README.md" license = "Apache-2.0 OR MIT" repository = "https://github.com/pendulum-project/ntpd-rs" [dependencies.libc] version = "0.2.145" [dependencies.ntp-proto] version = "1.1.2" features = ["__internal-api"] [dependencies.serde] version = "1.0.145" features = ["derive"] [dependencies.tokio] version = "1.32" features = [ "net", "time", ] [dependencies.tracing] version = "0.1.21" [dev-dependencies.tokio] version = "1.32" features = [ "net", "time", "test-util", ] ntp-udp-1.1.2/Cargo.toml.orig000064400000000000000000000012101046102023000140750ustar 00000000000000[package] name = "ntp-udp" description = "ntpd-rs networking and timestamping layer" readme = "README.md" version.workspace = true edition.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true publish.workspace = true rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] ntp-proto.workspace = true tokio = { workspace = true, features = ["net", "time"] } libc.workspace = true tracing.workspace = true serde.workspace = true [dev-dependencies] tokio = { workspace = true, features = ["net", "time", "test-util"] } ntp-udp-1.1.2/LICENSE-APACHE000064400000000000000000000227731046102023000131530ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ntp-udp-1.1.2/LICENSE-MIT000064400000000000000000000020651046102023000126530ustar 00000000000000Copyright (c) 2022-2023 Tweede Golf and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ntp-udp-1.1.2/README.md000064400000000000000000000005711046102023000124760ustar 00000000000000# ntp-udp This crate contains networking and timestamping code for ntpd-rs and is not intended as a public interface at this time. It follows the same version as the main ntpd-rs crate, but that version is not intended to give any stability guarantee. Use at your own risk. Please visit the [ntpd-rs](https://github.com/pendulum-project/ntpd-rs) project for more information. ntp-udp-1.1.2/src/hwtimestamp.rs000064400000000000000000000062271046102023000147220ustar 00000000000000use std::os::unix::io::AsRawFd; use crate::{interface::InterfaceName, raw_socket::cerr}; const fn standard_hwtstamp_config() -> libc::hwtstamp_config { libc::hwtstamp_config { flags: 0, tx_type: libc::HWTSTAMP_TX_ON as _, rx_filter: libc::HWTSTAMP_FILTER_ALL as _, } } pub fn driver_enable_hardware_timestamping( udp_socket: &std::net::UdpSocket, ) -> std::io::Result<()> { set_hardware_timestamp(udp_socket, standard_hwtstamp_config()) } fn set_hardware_timestamp( udp_socket: &std::net::UdpSocket, mut config: libc::hwtstamp_config, ) -> std::io::Result<()> { let mut ifreq: libc::ifreq = libc::ifreq { ifr_name: socket_interface_name(udp_socket)?, ifr_ifru: libc::__c_anonymous_ifr_ifru { ifru_data: (&mut config as *mut _) as *mut libc::c_char, }, }; let fd = udp_socket.as_raw_fd(); cerr(unsafe { libc::ioctl(fd, libc::SIOCSHWTSTAMP as _, &mut ifreq) })?; Ok(()) } #[allow(unused)] fn get_hardware_timestamp( udp_socket: &std::net::UdpSocket, ) -> std::io::Result { let mut tstamp_config = libc::hwtstamp_config { flags: 0, tx_type: 0, rx_filter: 0, }; let mut ifreq: libc::ifreq = libc::ifreq { ifr_name: socket_interface_name(udp_socket)?, ifr_ifru: libc::__c_anonymous_ifr_ifru { ifru_data: (&mut tstamp_config as *mut _) as *mut libc::c_char, }, }; let fd = udp_socket.as_raw_fd(); cerr(unsafe { libc::ioctl(fd, libc::SIOCGHWTSTAMP as _, &mut ifreq) })?; Ok(tstamp_config) } fn socket_interface_name( udp_socket: &std::net::UdpSocket, ) -> std::io::Result<[libc::c_char; libc::IFNAMSIZ]> { use std::io::{Error, ErrorKind}; match InterfaceName::from_socket_addr(udp_socket.local_addr()?)? { Some(interface_name) => Ok(interface_name.to_ifr_name()), None => Err(Error::new(ErrorKind::Other, "socket has no interface name")), } } #[cfg(test)] mod tests { use super::*; #[test] fn get_hwtimestamp() -> std::io::Result<()> { let udp_socket = std::net::UdpSocket::bind(("0.0.0.0", 9000))?; udp_socket.connect(("10.0.0.18", 9001))?; if let Err(e) = get_hardware_timestamp(&udp_socket) { assert!( e.to_string().contains("Operation not supported") || e.to_string().contains("Not supported") ); } Ok(()) } #[test] #[ignore = "requires elevated permissions to run"] fn get_set_hwtimestamp() -> std::io::Result<()> { let udp_socket = std::net::UdpSocket::bind(("0.0.0.0", 9002))?; udp_socket.connect(("10.0.0.18", 9003))?; let old = get_hardware_timestamp(&udp_socket)?; let custom = standard_hwtstamp_config(); set_hardware_timestamp(&udp_socket, custom)?; let new = get_hardware_timestamp(&udp_socket)?; let custom = standard_hwtstamp_config(); assert_eq!(new.flags, custom.flags); assert_eq!(new.tx_type, custom.tx_type); assert_eq!(new.rx_filter, custom.rx_filter); set_hardware_timestamp(&udp_socket, old)?; Ok(()) } } ntp-udp-1.1.2/src/interface.rs000064400000000000000000000216621046102023000143200ustar 00000000000000//! taken from https://docs.rs/nix/latest/src/nix/ifaddrs.rs.html //! stripped to just the parts that we need. //! //! Query network interface addresses //! //! Uses the Linux and/or BSD specific function `getifaddrs` to query the list //! of interfaces and their associated addresses. use std::iter::Iterator; use std::net::SocketAddr; use std::option::Option; use crate::raw_socket::interface_iterator::InterfaceIterator; #[derive(Clone, Copy, PartialEq, Eq)] pub struct InterfaceName { bytes: [u8; libc::IFNAMSIZ], } impl std::ops::Deref for InterfaceName { type Target = [u8]; fn deref(&self) -> &Self::Target { self.bytes.as_slice() } } impl<'de> serde::Deserialize<'de> for InterfaceName { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { use std::str::FromStr; use InterfaceNameParseError::*; let name: String = serde::Deserialize::deserialize(deserializer)?; match Self::from_str(&name) { Ok(v) => Ok(v), Err(Empty) => Err(serde::de::Error::custom("interface name empty")), Err(TooLong) => Err(serde::de::Error::custom("interface name too long")), } } } #[derive(Debug)] pub enum InterfaceNameParseError { Empty, TooLong, } impl std::str::FromStr for InterfaceName { type Err = InterfaceNameParseError; fn from_str(name: &str) -> Result { if name.is_empty() { return Err(InterfaceNameParseError::Empty); } let mut it = name.bytes(); let bytes = std::array::from_fn(|_| it.next().unwrap_or_default()); if it.next().is_some() { Err(InterfaceNameParseError::TooLong) } else { Ok(InterfaceName { bytes }) } } } impl InterfaceName { pub const DEFAULT: Option = None; #[cfg(test)] pub const LOOPBACK: Self = Self { bytes: *b"lo\0\0\0\0\0\0\0\0\0\0\0\0\0\0", }; #[cfg(test)] pub const INVALID: Self = Self { bytes: *b"123412341234123\0", }; pub fn as_str(&self) -> &str { std::str::from_utf8(self.bytes.as_slice()) .unwrap_or_default() .trim_end_matches('\0') } pub fn as_cstr(&self) -> &std::ffi::CStr { // TODO: in rust 1.69.0, use // std::ffi::CStr::from_bytes_until_nul(&self.bytes[..]).unwrap() // it is an invariant of InterfaceName that the bytes are null-terminated let first_null = self.bytes.iter().position(|b| *b == 0).unwrap(); std::ffi::CStr::from_bytes_with_nul(&self.bytes[..=first_null]).unwrap() } pub fn to_ifr_name(self) -> [libc::c_char; libc::IFNAMSIZ] { let mut it = self.bytes.iter().copied(); [0; libc::IFNAMSIZ].map(|_| it.next().unwrap_or(0) as libc::c_char) } pub fn from_socket_addr(local_addr: SocketAddr) -> std::io::Result> { let matches_inferface = |interface: &InterfaceData| match interface.socket_addr { None => false, Some(address) => address.ip() == local_addr.ip(), }; match InterfaceIterator::new()?.find(matches_inferface) { Some(interface) => Ok(Some(interface.name)), None => Ok(None), } } } impl std::fmt::Debug for InterfaceName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("InterfaceName") .field(&self.as_str()) .finish() } } impl std::fmt::Display for InterfaceName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.as_str().fmt(f) } } pub fn sockaddr_storage_to_socket_addr( sockaddr_storage: &libc::sockaddr_storage, ) -> Option { // Safety: // // sockaddr_storage always has enough space to store either a sockaddr_in or sockaddr_in6 unsafe { sockaddr_to_socket_addr(sockaddr_storage as *const _ as *const libc::sockaddr) } } /// Convert a libc::sockaddr to a rust std::net::SocketAddr /// /// # Safety /// /// According to the posix standard, `sockaddr` does not have a defined size: the size depends on /// the value of the `ss_family` field. We assume this to be correct. /// /// In practice, types in rust/c need a statically-known stack size, so they pick some value. In /// practice it can be (and is) larger than the `sizeof` value. pub unsafe fn sockaddr_to_socket_addr(sockaddr: *const libc::sockaddr) -> Option { // Most (but not all) of the fields in a socket addr are in network byte ordering. // As such, when doing conversions here, we should start from the NATIVE // byte representation, as this will actualy be the big-endian representation // of the underlying value regardless of platform. match unsafe { (*sockaddr).sa_family as libc::c_int } { libc::AF_INET => { // SAFETY: we cast from a libc::sockaddr (alignment 2) to a libc::sockaddr_in (alignment 4) // that means that the pointer is now potentially unaligned. We must used read_unaligned! let inaddr: libc::sockaddr_in = unsafe { std::ptr::read_unaligned(sockaddr as *const libc::sockaddr_in) }; let socketaddr = std::net::SocketAddrV4::new( std::net::Ipv4Addr::from(inaddr.sin_addr.s_addr.to_ne_bytes()), u16::from_be_bytes(inaddr.sin_port.to_ne_bytes()), ); Some(std::net::SocketAddr::V4(socketaddr)) } libc::AF_INET6 => { // SAFETY: we cast from a libc::sockaddr (alignment 2) to a libc::sockaddr_in6 (alignment 4) // that means that the pointer is now potentially unaligned. We must used read_unaligned! let inaddr: libc::sockaddr_in6 = unsafe { std::ptr::read_unaligned(sockaddr as *const libc::sockaddr_in6) }; let sin_addr = inaddr.sin6_addr.s6_addr; let segment_bytes: [u8; 16] = unsafe { std::ptr::read_unaligned(&sin_addr as *const _ as *const _) }; let socketaddr = std::net::SocketAddrV6::new( std::net::Ipv6Addr::from(segment_bytes), u16::from_be_bytes(inaddr.sin6_port.to_ne_bytes()), inaddr.sin6_flowinfo, // NOTE: Despite network byte order, no conversion is needed (see https://github.com/rust-lang/rust/issues/101605) inaddr.sin6_scope_id, ); Some(std::net::SocketAddr::V6(socketaddr)) } _ => None, } } pub struct InterfaceData { pub name: InterfaceName, pub mac: Option<[u8; 6]>, pub socket_addr: Option, } #[cfg(test)] mod tests { use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use super::*; #[test] fn find_interface() { let socket = std::net::UdpSocket::bind("127.0.0.1:8014").unwrap(); let name = InterfaceName::from_socket_addr(socket.local_addr().unwrap()).unwrap(); assert!(name.is_some()); } #[test] fn find_interface_ipv6() { let socket = std::net::UdpSocket::bind("::1:8015").unwrap(); let name = InterfaceName::from_socket_addr(socket.local_addr().unwrap()).unwrap(); assert!(name.is_some()); } #[test] fn decode_socket_addr_v4() { let sockaddr = libc::sockaddr { sa_family: libc::AF_INET as libc::sa_family_t, sa_data: [0, 0, 127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], #[cfg(any(target_os = "macos", target_os = "freebsd"))] sa_len: 14u8, }; let socket_addr = unsafe { sockaddr_to_socket_addr(&sockaddr) }.unwrap(); assert_eq!( socket_addr, SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0) ); // let sockaddr = libc::sockaddr { sa_family: libc::AF_INET as libc::sa_family_t, sa_data: [0, 42, -84 as _, 23, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], #[cfg(any(target_os = "macos", target_os = "freebsd"))] sa_len: 14u8, }; let socket_addr = unsafe { sockaddr_to_socket_addr(&sockaddr) }.unwrap(); assert_eq!( socket_addr, SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 23, 0, 1)), 42) ); } #[test] fn decode_socket_addr_v6() { let raw = [ 0x20, 0x01, 0x08, 0x88, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, ]; let sockaddr = libc::sockaddr_in6 { sin6_family: libc::AF_INET6 as libc::sa_family_t, sin6_port: u16::from_ne_bytes([0, 32]), sin6_flowinfo: 0, sin6_addr: libc::in6_addr { s6_addr: raw }, sin6_scope_id: 0, #[cfg(any(target_os = "macos", target_os = "freebsd"))] sin6_len: 14u8, }; let socket_addr = unsafe { sockaddr_to_socket_addr(&sockaddr as *const _ as *const _) }.unwrap(); assert_eq!(socket_addr, "[2001:888:0:2::2]:32".parse().unwrap()); } } ntp-udp-1.1.2/src/lib.rs000064400000000000000000000064611046102023000131260ustar 00000000000000//! This crate contains networking and timestamping code for ntpd-rs and is not //! intended as a public interface at this time. It follows the same version as the //! main ntpd-rs crate, but that version is not intended to give any stability //! guarantee. Use at your own risk. //! //! Please visit the [ntpd-rs](https://github.com/pendulum-project/ntpd-rs) project //! for more information. #![forbid(unsafe_op_in_unsafe_fn)] mod interface; mod raw_socket; mod socket; #[cfg(target_os = "linux")] mod hwtimestamp; use ntp_proto::NtpTimestamp; pub use interface::InterfaceName; use serde::Deserialize; pub use socket::UdpSocket; /// Enable the given timestamps. This is a hint! /// /// Your OS or hardware might not actually support some timestamping modes. /// Unsupported timestamping modes are ignored. #[derive(Debug, Clone, Copy, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct EnableTimestamps { #[serde(default = "bool_true")] pub rx_software: bool, #[serde(default = "bool_true")] pub tx_software: bool, #[serde(default)] // defaults to `false` pub rx_hardware: bool, #[serde(default)] // defaults to `false` pub tx_hardware: bool, } impl Default for EnableTimestamps { fn default() -> Self { Self { rx_software: true, tx_software: false, rx_hardware: false, tx_hardware: false, } } } #[derive(Clone, Copy)] pub(crate) enum LibcTimestamp { #[cfg_attr(any(target_os = "macos", target_os = "freebsd"), allow(unused))] TimeSpec { seconds: i64, nanos: i64, }, TimeVal { seconds: i64, micros: i64, }, } impl LibcTimestamp { #[cfg_attr(any(target_os = "macos", target_os = "freebsd"), allow(unused))] fn from_timespec(timespec: libc::timespec) -> Self { Self::TimeSpec { seconds: timespec.tv_sec as _, nanos: timespec.tv_nsec as _, } } #[cfg_attr(target_os = "linux", allow(unused))] fn from_timeval(timespec: libc::timeval) -> Self { Self::TimeVal { seconds: timespec.tv_sec as _, micros: timespec.tv_usec as _, } } } impl LibcTimestamp { pub(crate) fn into_ntp_timestamp(self) -> NtpTimestamp { // Unix uses an epoch located at 1/1/1970-00:00h (UTC) and NTP uses 1/1/1900-00:00h. // This leads to an offset equivalent to 70 years in seconds // there are 17 leap years between the two dates so the offset is const EPOCH_OFFSET: u32 = (70 * 365 + 17) * 86400; match self { LibcTimestamp::TimeSpec { seconds, nanos } => { // truncates the higher bits of the i64 let seconds = (seconds as u32).wrapping_add(EPOCH_OFFSET); // tv_nsec is always within [0, 1e10) let nanos = nanos as u32; NtpTimestamp::from_seconds_nanos_since_ntp_era(seconds, nanos) } LibcTimestamp::TimeVal { seconds, micros } => { // truncates the higher bits of the i64 let seconds = (seconds as u32).wrapping_add(EPOCH_OFFSET); let nanos = micros as u32 * 1000; NtpTimestamp::from_seconds_nanos_since_ntp_era(seconds, nanos) } } } } fn bool_true() -> bool { true } ntp-udp-1.1.2/src/raw_socket.rs000064400000000000000000000547251046102023000145270ustar 00000000000000/// This file contains safe wrappers for the socket-related system calls /// needed to implement the UdpSocket in socket.rs /// /// Since the safety of a rust unsafe block depends not only on its /// contents, but also the context within which it is called, the code /// here is split up in submodules that are individually as small as /// possible while still having each a fully safe API interface. This /// should reduce the amount of context which needs to be considered /// when reasoning about safety, significantly simplifying the checking /// of this code. /// /// All unsafe blocks are preceded with a comment explaining why that /// specific unsafe code should be safe within the context in which it /// is used. pub(crate) use recv_message::{ control_message_space, receive_message, ControlMessage, MessageQueue, }; pub(crate) use set_timestamping_options::set_timestamping_options; /// Turn a C failure (-1 is returned) into a rust Result pub(crate) fn cerr(t: libc::c_int) -> std::io::Result { match t { -1 => Err(std::io::Error::last_os_error()), _ => Ok(t), } } #[derive(Debug, Clone, Copy)] #[repr(i32)] #[allow(clippy::enum_variant_names)] pub(crate) enum TimestampMethod { /// Original timestamping for unix (linux, freebsd, macos) /// /// - microsecond precision (on freebsd, can be configured to get nanoseconds) /// - only receive timestamps #[allow(dead_code)] SoTimestamp = libc::SO_TIMESTAMP, /// Standard timestamping on linux. It gives us /// /// - nanosecond precision /// - send & receive timestamps #[cfg(target_os = "linux")] SoTimestamping = libc::SO_TIMESTAMPING, /// Legacy timestamping for linux /// /// - nanosecond precision /// - only receive timestamps #[cfg(target_os = "linux")] #[allow(dead_code)] SoTimestampns = libc::SO_TIMESTAMPNS, } mod set_timestamping_options { use std::os::unix::prelude::AsRawFd; use crate::EnableTimestamps; use super::{cerr, TimestampMethod}; enum SockOpt { Method(TimestampMethod), #[cfg(target_os = "freebsd")] Clock, } fn configure_timestamping_socket( udp_socket: &std::net::UdpSocket, option: SockOpt, value: u32, ) -> std::io::Result { // Documentation on the timestamping calls: // // - linux: https://www.kernel.org/doc/Documentation/networking/timestamping.txt // - freebsd: https://man.freebsd.org/cgi/man.cgi?setsockopt // // SAFETY: // // - the socket is provided by (safe) rust, and will outlive the call // - method is guaranteed to be a valid "name" argument // - the options pointer outlives the call // - the `option_len` corresponds with the options pointer // // Only some bits are valid to set in `options`, but setting invalid bits is perfectly safe // // > Setting other bit returns EINVAL and does not change the current state. unsafe { cerr(libc::setsockopt( udp_socket.as_raw_fd(), libc::SOL_SOCKET, match option { SockOpt::Method(m) => m as i32 as libc::c_int, #[cfg(target_os = "freebsd")] SockOpt::Clock => libc::SO_TS_CLOCK, }, &value as *const _ as *const libc::c_void, std::mem::size_of_val(&value) as libc::socklen_t, )) } } pub(crate) fn set_timestamping_options( udp_socket: &std::net::UdpSocket, method: TimestampMethod, timestamping: EnableTimestamps, ) -> std::io::Result<()> { let options = match method { TimestampMethod::SoTimestamp => { // only receive software timestamps are supported: 0 disables, 1 enables timestamping.rx_software as u32 } #[cfg(target_os = "linux")] TimestampMethod::SoTimestampns => { // only receive software timestamps are supported: 0 disables, 1 enables timestamping.rx_software as u32 } #[cfg(target_os = "linux")] TimestampMethod::SoTimestamping => { // SO_TIMESTAMPING has many more options: it supports receive and send timestamps, and // software and hardware timestamping. Of those, only software send and receive timestamps // are currently supported let mut options = 0; if timestamping.rx_software || timestamping.tx_software { // enable software timestamping options |= libc::SOF_TIMESTAMPING_SOFTWARE; } if timestamping.rx_software { // we want receive timestamps options |= libc::SOF_TIMESTAMPING_RX_SOFTWARE; } if timestamping.tx_software { // - we want send timestamps // - return just the timestamp, don't send the full message along // - tag the timestamp with an ID options |= libc::SOF_TIMESTAMPING_TX_SOFTWARE | libc::SOF_TIMESTAMPING_OPT_TSONLY | libc::SOF_TIMESTAMPING_OPT_ID; } if timestamping.rx_hardware || timestamping.tx_hardware { // enable hardware timestamping options |= libc::SOF_TIMESTAMPING_RAW_HARDWARE; #[cfg(target_os = "linux")] crate::hwtimestamp::driver_enable_hardware_timestamping(udp_socket)?; } if timestamping.rx_hardware { options |= libc::SOF_TIMESTAMPING_RX_HARDWARE; } if timestamping.tx_hardware { options |= libc::SOF_TIMESTAMPING_TX_HARDWARE | libc::SOF_TIMESTAMPING_OPT_TSONLY | libc::SOF_TIMESTAMPING_OPT_ID; // in practice, this is needed to have `SOF_TIMESTAMPING_OPT_ID` work // without it, the reported id is always 0. options |= libc::SOF_TIMESTAMPING_TX_SOFTWARE; } options } }; configure_timestamping_socket(udp_socket, SockOpt::Method(method), options)?; #[cfg(target_os = "freebsd")] configure_timestamping_socket(udp_socket, SockOpt::Clock, libc::SO_TS_REALTIME as u32)?; Ok(()) } } mod recv_message { use std::{ io::IoSliceMut, marker::PhantomData, mem::MaybeUninit, net::SocketAddr, os::unix::prelude::AsRawFd, }; use crate::interface::sockaddr_storage_to_socket_addr; use crate::LibcTimestamp; use super::cerr; pub(crate) enum MessageQueue { Normal, #[cfg(target_os = "linux")] Error, } fn empty_msghdr() -> libc::msghdr { // On `target_env = "musl"`, there are several private padding fields. // the position of these padding fields depends on the system endianness, // so keeping making them public does not really help. // // Safety: // // all fields are either integer or pointer types. For those types, 0 is a valid value unsafe { MaybeUninit::::zeroed().assume_init() } } pub(crate) fn receive_message<'a>( socket: &std::net::UdpSocket, packet_buf: &mut [u8], control_buf: &'a mut [u8], queue: MessageQueue, ) -> std::io::Result<( libc::c_int, impl Iterator + 'a, Option, )> { let mut buf_slice = IoSliceMut::new(packet_buf); let mut addr = zeroed_sockaddr_storage(); let mut mhdr = empty_msghdr(); mhdr.msg_control = control_buf.as_mut_ptr().cast::(); mhdr.msg_controllen = control_buf.len() as _; mhdr.msg_iov = (&mut buf_slice as *mut IoSliceMut).cast::(); mhdr.msg_iovlen = 1; mhdr.msg_flags = 0; mhdr.msg_name = (&mut addr as *mut libc::sockaddr_storage).cast::(); mhdr.msg_namelen = std::mem::size_of::() as u32; let receive_flags = match queue { MessageQueue::Normal => 0, #[cfg(target_os = "linux")] MessageQueue::Error => libc::MSG_ERRQUEUE, }; // Safety: // We have a mutable reference to the control buffer for the duration of the // call, and controllen is also set to it's length. // IoSliceMut is ABI compatible with iovec, and we only have 1 which matches iovlen // msg_name is initialized to point to an owned sockaddr_storage and // msg_namelen is the size of sockaddr_storage // If one of the buffers is too small, recvmsg cuts off data at appropriate boundary let sent_bytes = loop { match cerr(unsafe { libc::recvmsg(socket.as_raw_fd(), &mut mhdr, receive_flags) } as _) { Err(e) if std::io::ErrorKind::Interrupted == e.kind() => { // retry when the recv was interrupted continue; } Err(e) => return Err(e), Ok(sent) => break sent, } }; if mhdr.msg_flags & libc::MSG_TRUNC > 0 { tracing::debug!( max_len = packet_buf.len(), "truncated packet because it was larger than expected", ); } if mhdr.msg_flags & libc::MSG_CTRUNC > 0 { tracing::warn!("truncated control messages"); } // Clear out the fields for which we are giving up the reference mhdr.msg_iov = std::ptr::null_mut(); mhdr.msg_iovlen = 0; mhdr.msg_name = std::ptr::null_mut(); mhdr.msg_namelen = 0; // Safety: // recvmsg ensures that the control buffer contains // a set of valid control messages and that controllen is // the length these take up in the buffer. Ok(( sent_bytes, unsafe { ControlMessageIterator::new(mhdr) }, sockaddr_storage_to_socket_addr(&addr), )) } // Invariants: // self.mhdr points to a valid libc::msghdr with a valid control // message region. // self.current_msg points to one of the control messages // in the region described by self.mhdr or is NULL // // These invariants are guaranteed from the safety conditions on // calling ControlMessageIterator::new, the fact that next preserves // these invariants and that the fields of ControlMessageIterator // are not modified outside these two functions. struct ControlMessageIterator<'a> { mhdr: libc::msghdr, next_msg: *const libc::cmsghdr, phantom: PhantomData<&'a [u8]>, } impl<'a> ControlMessageIterator<'a> { // Safety assumptions: // mhdr has a control and controllen field // that together describe a memory region // with lifetime 'a containing valid control // messages unsafe fn new(mhdr: libc::msghdr) -> Self { // Safety: // mhdr's control and controllen fields are valid and point // to valid control messages. let current_msg = unsafe { libc::CMSG_FIRSTHDR(&mhdr) }; // Invariant preservation: // The safety assumptions guaranteed by the caller ensure // that mhdr points to a valid region with valid control // messages. CMSG_FIRSTHDR is then guaranteed to either // return the pointer to the first valid control message // in that region, or NULL if the region is empty. Self { mhdr, next_msg: current_msg, phantom: PhantomData, } } } pub(crate) enum ControlMessage { Timestamping(crate::LibcTimestamp), #[cfg(target_os = "linux")] ReceiveError(libc::sock_extended_err), Other(libc::cmsghdr), } #[cfg(target_os = "linux")] const SCM_TIMESTAMP_NS: libc::c_int = libc::SCM_TIMESTAMPNS; #[cfg(target_os = "freebsd")] const SCM_TIMESTAMP_NS: libc::c_int = libc::SCM_REALTIME; impl<'a> Iterator for ControlMessageIterator<'a> { type Item = ControlMessage; fn next(&mut self) -> Option { // Safety: // By the invariants, self.current_msg either points to a valid control message // or is NULL let current_msg = unsafe { self.next_msg.as_ref() }?; // Safety: // Invariants ensure that self.mhdr points to a valid libc::msghdr with a valid control // message region, and that self.next_msg either points to a valid control message // or is NULL. // The previous statement would have returned if self.next_msg were NULL, therefore both passed // pointers are valid for use with CMSG_NXTHDR // Invariant preservation: // CMSG_NXTHDR returns either a pointer to the next valid control message in the control // message region described by self.mhdr, or NULL self.next_msg = unsafe { libc::CMSG_NXTHDR(&self.mhdr, self.next_msg) }; Some(match (current_msg.cmsg_level, current_msg.cmsg_type) { #[cfg(target_os = "linux")] (libc::SOL_SOCKET, libc::SCM_TIMESTAMPING) => { // Safety: // current_msg was constructed from a pointer that pointed to a valid control message. // SO_TIMESTAMPING always has 3 timespecs in the data let cmsg_data = unsafe { libc::CMSG_DATA(current_msg) } as *const [libc::timespec; 3]; let [software, _, hardware] = unsafe { std::ptr::read_unaligned(cmsg_data) }; // if defined, we prefer the hardware over the software timestamp let timespec = if hardware.tv_sec != 0 && hardware.tv_nsec != 0 { hardware } else { software }; ControlMessage::Timestamping(LibcTimestamp::from_timespec(timespec)) } #[cfg(any(target_os = "linux", target_os = "freebsd"))] (libc::SOL_SOCKET, SCM_TIMESTAMP_NS) => { // Safety: // current_msg was constructed from a pointer that pointed to a valid control message. // SO_TIMESTAMPNS always has a timespec in the data let cmsg_data = unsafe { libc::CMSG_DATA(current_msg) } as *const libc::timespec; let timespec = unsafe { std::ptr::read_unaligned(cmsg_data) }; ControlMessage::Timestamping(LibcTimestamp::from_timespec(timespec)) } (libc::SOL_SOCKET, libc::SCM_TIMESTAMP) => { // Safety: // current_msg was constructed from a pointer that pointed to a valid control message. // SO_TIMESTAMP always has a timeval in the data let cmsg_data = unsafe { libc::CMSG_DATA(current_msg) } as *const libc::timeval; let timeval = unsafe { std::ptr::read_unaligned(cmsg_data) }; ControlMessage::Timestamping(LibcTimestamp::from_timeval(timeval)) } #[cfg(target_os = "linux")] (libc::SOL_IP, libc::IP_RECVERR) | (libc::SOL_IPV6, libc::IPV6_RECVERR) => { // this is part of how timestamps are reported. // Safety: // current_msg was constructed from a pointer that pointed to a valid // control message. // IP*_RECVERR always has a sock_extended_err in the data let error = unsafe { let ptr = libc::CMSG_DATA(current_msg) as *const libc::sock_extended_err; std::ptr::read_unaligned(ptr) }; ControlMessage::ReceiveError(error) } _ => ControlMessage::Other(*current_msg), }) } } /// The space used to store a control message that contains a value of type T pub(crate) const fn control_message_space() -> usize { // Safety: CMSG_SPACE is safe to call (unsafe { libc::CMSG_SPACE((std::mem::size_of::()) as _) }) as usize } fn zeroed_sockaddr_storage() -> libc::sockaddr_storage { // a zeroed-out sockaddr storage is semantically valid, because a ss_family with value 0 is // libc::AF_UNSPEC. Hence the rest of the data does not come with any constraints // Safety: // the MaybeUninit is zeroed before assumed to be initialized unsafe { std::mem::MaybeUninit::zeroed().assume_init() } } } #[allow(unused)] #[cfg(target_os = "linux")] pub(crate) mod timestamping_config { use std::os::unix::prelude::RawFd; use crate::interface::InterfaceName; #[repr(C)] #[allow(non_camel_case_types)] #[derive(Debug, Default)] struct ethtool_ts_info { cmd: u32, so_timestamping: u32, phc_index: i32, tx_types: u32, tx_reserved: [u32; 3], rx_filters: u32, rx_reserved: [u32; 3], } #[derive(Debug, Clone, Copy)] struct TimestampSupport { rx_software: bool, tx_software: bool, rx_hardware: bool, tx_hardware: bool, #[cfg(test)] phc_index: Option, } impl TimestampSupport { fn for_interface(interface_name: InterfaceName) -> std::io::Result { use libc::{AF_INET, IPPROTO_IP, SOCK_DGRAM}; let socket = super::cerr(unsafe { libc::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP) })?; Self::for_socket(socket, interface_name) } #[cfg(target_os = "linux")] fn for_socket(socket: RawFd, interface_name: InterfaceName) -> std::io::Result { // Get time stamping and PHC info const ETHTOOL_GET_TS_INFO: u32 = 0x00000041; let mut tsi: ethtool_ts_info = ethtool_ts_info { cmd: ETHTOOL_GET_TS_INFO, ..Default::default() }; let ifr: libc::ifreq = libc::ifreq { ifr_name: interface_name.to_ifr_name(), ifr_ifru: libc::__c_anonymous_ifr_ifru { ifru_data: (&mut tsi as *mut _) as *mut libc::c_char, }, }; // SIOCETHTOOL = 0x8946 (Ethtool interface) Linux ioctl request super::cerr(unsafe { libc::ioctl(socket, 0x8946, &ifr) })?; let support = Self { rx_software: tsi.so_timestamping & libc::SOF_TIMESTAMPING_RX_SOFTWARE != 0, tx_software: tsi.so_timestamping & libc::SOF_TIMESTAMPING_TX_SOFTWARE != 0, rx_hardware: tsi.so_timestamping & libc::SOF_TIMESTAMPING_RX_HARDWARE != 0, tx_hardware: tsi.so_timestamping & libc::SOF_TIMESTAMPING_TX_HARDWARE != 0, #[cfg(test)] phc_index: u32::try_from(tsi.phc_index).ok(), }; Ok(support) } #[cfg(test)] fn phc_clock_pathbuf(&self) -> Option { use std::path::PathBuf; self.phc_index .map(|index| PathBuf::from(format!("/dev/ptp{index}"))) } } #[cfg(test)] mod tests { use super::*; #[test] fn loopback_timestamping_support() { let support = TimestampSupport::for_interface(InterfaceName::LOOPBACK).unwrap(); assert!(support.rx_software); assert!(support.tx_software); assert!(!support.rx_hardware); assert!(!support.tx_hardware); assert!(support.phc_clock_pathbuf().is_none()); } } } pub(crate) mod interface_iterator { use crate::interface::{sockaddr_to_socket_addr, InterfaceData, InterfaceName}; use std::str::FromStr; pub struct InterfaceIterator { base: *mut libc::ifaddrs, next: *mut libc::ifaddrs, } impl InterfaceIterator { pub fn new() -> std::io::Result { let mut addrs = core::mem::MaybeUninit::<*mut libc::ifaddrs>::uninit(); unsafe { super::cerr(libc::getifaddrs(addrs.as_mut_ptr()))?; Ok(Self { base: addrs.assume_init(), next: addrs.assume_init(), }) } } #[cfg(target_os = "linux")] fn mac(ifaddr: &libc::ifaddrs) -> Option<[u8; 6]> { let family = unsafe { (*ifaddr.ifa_addr).sa_family }; if family as i32 == libc::AF_PACKET { let sockaddr_ll: libc::sockaddr_ll = unsafe { std::ptr::read_unaligned(ifaddr.ifa_addr as *const _) }; Some([ sockaddr_ll.sll_addr[0], sockaddr_ll.sll_addr[1], sockaddr_ll.sll_addr[2], sockaddr_ll.sll_addr[3], sockaddr_ll.sll_addr[4], sockaddr_ll.sll_addr[5], ]) } else { None } } #[cfg(any(target_os = "macos", target_os = "freebsd"))] fn mac(_ifaddr: &libc::ifaddrs) -> Option<[u8; 6]> { None } } impl Drop for InterfaceIterator { fn drop(&mut self) { unsafe { libc::freeifaddrs(self.base) }; } } impl Iterator for InterfaceIterator { type Item = InterfaceData; fn next(&mut self) -> Option<::Item> { let ifaddr = unsafe { self.next.as_ref() }?; self.next = ifaddr.ifa_next; let ifname = unsafe { std::ffi::CStr::from_ptr(ifaddr.ifa_name) }; let name = match std::str::from_utf8(ifname.to_bytes()) { Err(_) => unreachable!("interface names must be ascii"), Ok(name) => InterfaceName::from_str(name).expect("name from os"), }; let mac = Self::mac(ifaddr); let socket_addr = unsafe { sockaddr_to_socket_addr(ifaddr.ifa_addr) }; let data = InterfaceData { name, mac, socket_addr, }; Some(data) } } } ntp-udp-1.1.2/src/socket.rs000064400000000000000000000456141046102023000136530ustar 00000000000000#![forbid(unsafe_code)] use std::{io, net::SocketAddr}; use ntp_proto::NtpTimestamp; use tokio::io::{unix::AsyncFd, Interest}; use tracing::instrument; use crate::{ interface::InterfaceName, raw_socket::{ control_message_space, receive_message, set_timestamping_options, ControlMessage, MessageQueue, TimestampMethod, }, EnableTimestamps, }; pub struct UdpSocket { io: AsyncFd, send_counter: u32, timestamping: EnableTimestamps, } #[cfg(target_os = "linux")] const DEFAULT_TIMESTAMP_METHOD: TimestampMethod = TimestampMethod::SoTimestamping; #[cfg(all(unix, not(target_os = "linux")))] const DEFAULT_TIMESTAMP_METHOD: TimestampMethod = TimestampMethod::SoTimestamp; impl UdpSocket { #[instrument(level = "debug", skip(peer_addr))] pub async fn client(listen_addr: SocketAddr, peer_addr: SocketAddr) -> io::Result { Self::client_with_timestamping( listen_addr, peer_addr, InterfaceName::DEFAULT, EnableTimestamps::default(), ) .await } pub async fn client_with_timestamping( listen_addr: SocketAddr, peer_addr: SocketAddr, interface: Option, timestamping: EnableTimestamps, ) -> io::Result { Self::client_with_timestamping_internal( listen_addr, peer_addr, interface, DEFAULT_TIMESTAMP_METHOD, timestamping, ) .await } async fn client_with_timestamping_internal( listen_addr: SocketAddr, peer_addr: SocketAddr, interface: Option, method: TimestampMethod, timestamping: EnableTimestamps, ) -> io::Result { let socket = tokio::net::UdpSocket::bind(listen_addr).await?; tracing::debug!( local_addr = ?socket.local_addr().unwrap(), "client socket bound" ); // bind the socket to a specific interface. This is relevant for hardware timestamping, // because the interface determines which clock is used to produce the timestamps. if let Some(_interface) = interface { #[cfg(target_os = "linux")] socket.bind_device(Some(&_interface)).unwrap(); } socket.connect(peer_addr).await?; tracing::debug!( local_addr = ?socket.local_addr().unwrap(), peer_addr = ?socket.peer_addr().unwrap(), "client socket connected" ); let socket = socket.into_std()?; set_timestamping_options(&socket, method, timestamping)?; Ok(UdpSocket { io: AsyncFd::new(socket)?, send_counter: 0, timestamping, }) } #[instrument(level = "debug")] pub async fn server( listen_addr: SocketAddr, interface: Option, ) -> io::Result { let socket = tokio::net::UdpSocket::bind(listen_addr).await?; tracing::debug!( local_addr = ?socket.local_addr().unwrap(), "server socket bound" ); // bind the socket to a specific interface. This is relevant for hardware timestamping, // because the interface determines which clock is used to produce the timestamps. if let Some(_interface) = interface { #[cfg(target_os = "linux")] socket.bind_device(Some(&_interface)).unwrap(); } let socket = socket.into_std()?; // our supported kernel versions always have receive timestamping. Send timestamping for a // server connection is not relevant, so we don't even bother with checking if it is supported let timestamping = EnableTimestamps { rx_software: true, tx_software: false, rx_hardware: false, tx_hardware: false, }; set_timestamping_options(&socket, DEFAULT_TIMESTAMP_METHOD, timestamping)?; Ok(UdpSocket { io: AsyncFd::new(socket)?, send_counter: 0, timestamping, }) } #[instrument(level = "trace", skip(self, buf), fields( local_addr = debug(self.as_ref().local_addr().unwrap()), peer_addr = debug(self.as_ref().peer_addr()), buf_size = buf.len(), ))] pub async fn send(&mut self, buf: &[u8]) -> io::Result<(usize, Option)> { tracing::trace!(size = buf.len(), "sending bytes"); let result = self .io .async_io(Interest::WRITABLE, |inner| inner.send(buf)) .await; let send_size = match result { Ok(size) => { tracing::trace!(sent = size, "sent bytes"); size } Err(e) => { tracing::debug!(error = debug(&e), "error sending data"); return Err(e); } }; debug_assert_eq!(buf.len(), send_size); let expected_counter = self.send_counter; self.send_counter = self.send_counter.wrapping_add(1); if self.timestamping.tx_software { #[cfg(target_os = "linux")] { // the send timestamp may never come set a very short timeout to prevent hanging forever. // We automatically fall back to a less accurate timestamp when this function returns None let timeout = std::time::Duration::from_millis(10); match tokio::time::timeout(timeout, self.fetch_send_timestamp(expected_counter)) .await { Err(_) => { tracing::warn!("Packet without timestamp"); Ok((send_size, None)) } Ok(send_timestamp) => Ok((send_size, Some(send_timestamp?))), } } #[cfg(any(target_os = "macos", target_os = "freebsd"))] { let _ = expected_counter; Ok((send_size, None)) } } else { tracing::trace!("send timestamping not supported"); Ok((send_size, None)) } } #[cfg(target_os = "linux")] async fn fetch_send_timestamp(&self, expected_counter: u32) -> io::Result { let msg = "waiting for timestamp socket to become readable to fetch a send timestamp"; tracing::trace!(msg); let try_read = |udp_socket: &std::net::UdpSocket| { fetch_send_timestamp_help(udp_socket, expected_counter) }; loop { // the timestamp being available triggers the error interest match self.io.async_io(Interest::ERROR, try_read).await? { Some(timestamp) => return Ok(timestamp), None => continue, }; } } #[instrument(level = "trace", skip(self, buf), fields( local_addr = debug(self.as_ref().local_addr().unwrap()), buf_size = buf.len(), ))] pub async fn send_to(&self, buf: &[u8], addr: SocketAddr) -> io::Result { tracing::trace!(size = buf.len(), ?addr, "sending bytes"); let result = self .io .async_io(Interest::WRITABLE, |inner| inner.send_to(buf, addr)) .await; match &result { Ok(size) => tracing::trace!(sent = size, "sent bytes"), Err(e) => tracing::debug!(error = debug(e), "error sending data"), } result } #[instrument(level = "trace", skip(self, buf), fields( local_addr = debug(self.as_ref().local_addr().unwrap()), peer_addr = debug(self.as_ref().peer_addr().ok()), buf_size = buf.len(), ))] pub async fn recv( &self, buf: &mut [u8], ) -> io::Result<(usize, SocketAddr, Option)> { tracing::trace!("waiting for socket to become readable"); let result = self .io .async_io(Interest::READABLE, |inner| recv(inner, buf)) .await; match &result { Ok((size, addr, ts)) => { tracing::trace!(size, ts = ?ts, addr = ?addr, "received message"); } Err(e) => tracing::debug!(error = ?e, "error receiving data"), } result } } impl AsRef for UdpSocket { fn as_ref(&self) -> &std::net::UdpSocket { self.io.get_ref() } } fn recv( socket: &std::net::UdpSocket, buf: &mut [u8], ) -> io::Result<(usize, SocketAddr, Option)> { let mut control_buf = [0; control_message_space::<[libc::timespec; 3]>()]; // loops for when we receive an interrupt during the recv let (bytes_read, control_messages, sock_addr) = receive_message(socket, buf, &mut control_buf, MessageQueue::Normal)?; let sock_addr = sock_addr.unwrap_or_else(|| unreachable!("We never constructed a non-ip socket")); // Loops through the control messages, but we should only get a single message in practice for msg in control_messages { match msg { ControlMessage::Timestamping(libc_timestamp) => { let ntp_timestamp = libc_timestamp.into_ntp_timestamp(); return Ok((bytes_read as usize, sock_addr, Some(ntp_timestamp))); } #[cfg(target_os = "linux")] ControlMessage::ReceiveError(_error) => { tracing::warn!("unexpected error message on the MSG_ERRQUEUE"); } ControlMessage::Other(msg) => { tracing::warn!( "weird control message {:?} {:?}", msg.cmsg_level, msg.cmsg_type ); } } } Ok((bytes_read as usize, sock_addr, None)) } #[cfg(target_os = "linux")] fn fetch_send_timestamp_help( socket: &std::net::UdpSocket, expected_counter: u32, ) -> io::Result> { // we get back two control messages: one with the timestamp (just like a receive timestamp), // and one error message with no error reason. The payload for this second message is kind of // undocumented. // // section 2.1.1 of https://www.kernel.org/doc/Documentation/networking/timestamping.txt says that // a `sock_extended_err` is returned, but in practice we also see a socket address. The linux // kernel also has this https://github.com/torvalds/linux/blob/master/tools/testing/selftests/net/so_txtime.c#L153= // // sockaddr_storage is bigger than we need, but sockaddr is too small for ipv6 const CONTROL_SIZE: usize = control_message_space::<[libc::timespec; 3]>() + control_message_space::<(libc::sock_extended_err, libc::sockaddr_storage)>(); let mut control_buf = [0; CONTROL_SIZE]; let (_, control_messages, _) = receive_message(socket, &mut [], &mut control_buf, MessageQueue::Error)?; let mut send_ts = None; for msg in control_messages { match msg { ControlMessage::Timestamping(timestamp) => { send_ts = Some(timestamp); } ControlMessage::ReceiveError(error) => { // the timestamping does not set a message; if there is a message, that means // something else is wrong, and we want to know about it. if error.ee_errno as libc::c_int != libc::ENOMSG { tracing::warn!( expected_counter, error.ee_data, "error message on the MSG_ERRQUEUE" ); } // Check that this message belongs to the send we are interested in if error.ee_data != expected_counter { tracing::debug!( error.ee_data, expected_counter, "Timestamp for unrelated packet" ); return Ok(None); } } ControlMessage::Other(msg) => { tracing::warn!( msg.cmsg_level, msg.cmsg_type, "unexpected message on the MSG_ERRQUEUE", ); } } } Ok(send_ts.map(|ts| ts.into_ntp_timestamp())) } #[cfg(test)] mod tests { use std::net::Ipv4Addr; use super::*; #[tokio::test] async fn test_client_basic_ipv4() { let mut a = UdpSocket::client( "127.0.0.1:10000".parse().unwrap(), "127.0.0.1:10001".parse().unwrap(), ) .await .unwrap(); let mut b = UdpSocket::client( "127.0.0.1:10001".parse().unwrap(), "127.0.0.1:10000".parse().unwrap(), ) .await .unwrap(); a.send(&[1; 48]).await.unwrap(); let mut buf = [0; 48]; let (size, addr, _) = b.recv(&mut buf).await.unwrap(); assert_eq!(size, 48); assert_eq!(addr, "127.0.0.1:10000".parse().unwrap()); assert_eq!(buf, [1; 48]); b.send(&[2; 48]).await.unwrap(); let (size, addr, _) = a.recv(&mut buf).await.unwrap(); assert_eq!(size, 48); assert_eq!(addr, "127.0.0.1:10001".parse().unwrap()); assert_eq!(buf, [2; 48]); } #[tokio::test] async fn test_client_basic_ipv6() { let mut a = UdpSocket::client( "[::1]:10000".parse().unwrap(), "[::1]:10001".parse().unwrap(), ) .await .unwrap(); let mut b = UdpSocket::client( "[::1]:10001".parse().unwrap(), "[::1]:10000".parse().unwrap(), ) .await .unwrap(); a.send(&[1; 48]).await.unwrap(); let mut buf = [0; 48]; let (size, addr, _) = b.recv(&mut buf).await.unwrap(); assert_eq!(size, 48); assert_eq!(addr, "[::1]:10000".parse().unwrap()); assert_eq!(buf, [1; 48]); b.send(&[2; 48]).await.unwrap(); let (size, addr, _) = a.recv(&mut buf).await.unwrap(); assert_eq!(size, 48); assert_eq!(addr, "[::1]:10001".parse().unwrap()); assert_eq!(buf, [2; 48]); } #[tokio::test] async fn test_server_basic_ipv4() { let a = UdpSocket::server("127.0.0.1:10002".parse().unwrap(), InterfaceName::DEFAULT) .await .unwrap(); let mut b = UdpSocket::client( "127.0.0.1:10003".parse().unwrap(), "127.0.0.1:10002".parse().unwrap(), ) .await .unwrap(); b.send(&[1; 48]).await.unwrap(); let mut buf = [0; 48]; let (size, addr, _) = a.recv(&mut buf).await.unwrap(); assert_eq!(size, 48); assert_eq!(addr, "127.0.0.1:10003".parse().unwrap()); assert_eq!(buf, [1; 48]); a.send_to(&[2; 48], addr).await.unwrap(); let (size, addr, _) = b.recv(&mut buf).await.unwrap(); assert_eq!(size, 48); assert_eq!(addr, "127.0.0.1:10002".parse().unwrap()); assert_eq!(buf, [2; 48]); } #[tokio::test] async fn test_server_basic_ipv6() { let a = UdpSocket::server("[::1]:10002".parse().unwrap(), InterfaceName::DEFAULT) .await .unwrap(); let mut b = UdpSocket::client( "[::1]:10003".parse().unwrap(), "[::1]:10002".parse().unwrap(), ) .await .unwrap(); b.send(&[1; 48]).await.unwrap(); let mut buf = [0; 48]; let (size, addr, _) = a.recv(&mut buf).await.unwrap(); assert_eq!(size, 48); assert_eq!(addr, "[::1]:10003".parse().unwrap()); assert_eq!(buf, [1; 48]); a.send_to(&[2; 48], addr).await.unwrap(); let (size, addr, _) = b.recv(&mut buf).await.unwrap(); assert_eq!(size, 48); assert_eq!(addr, "[::1]:10002".parse().unwrap()); assert_eq!(buf, [2; 48]); } async fn timestamping_reasonable(method: TimestampMethod, p1: u16, p2: u16) { let mut a = UdpSocket::client( SocketAddr::from((Ipv4Addr::LOCALHOST, p1)), SocketAddr::from((Ipv4Addr::LOCALHOST, p2)), ) .await .unwrap(); let b = UdpSocket::client_with_timestamping_internal( SocketAddr::from((Ipv4Addr::LOCALHOST, p2)), SocketAddr::from((Ipv4Addr::LOCALHOST, p1)), InterfaceName::DEFAULT, method, EnableTimestamps { rx_software: true, tx_software: true, rx_hardware: false, tx_hardware: false, }, ) .await .unwrap(); tokio::spawn(async move { a.send(&[1; 48]).await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(200)).await; a.send(&[2; 48]).await.unwrap(); }); let mut buf = [0; 48]; let (s1, _, t1) = b.recv(&mut buf).await.unwrap(); let (s2, _, t2) = b.recv(&mut buf).await.unwrap(); assert_eq!(s1, 48); assert_eq!(s2, 48); let t1 = t1.unwrap(); let t2 = t2.unwrap(); let delta = t2 - t1; // this can be flaky on freebsd assert!( delta.to_seconds() > 0.15 && delta.to_seconds() < 0.25, "delta was {}s", delta.to_seconds() ); } #[tokio::test] #[cfg(target_os = "linux")] async fn timestamping_reasonable_so_timestamping() { timestamping_reasonable(TimestampMethod::SoTimestamping, 8000, 8001).await; } #[tokio::test] #[cfg(target_os = "linux")] async fn timestamping_reasonable_so_timestampns() { timestamping_reasonable(TimestampMethod::SoTimestampns, 8002, 8003).await; } #[tokio::test] #[cfg(unix)] async fn timestamping_reasonable_so_timestamp() { timestamping_reasonable(TimestampMethod::SoTimestamp, 8004, 8005).await; } #[tokio::test] #[cfg_attr( any(target_os = "macos", target_os = "freebsd"), ignore = "send timestamps are not supported" )] async fn test_send_timestamp() { let mut a = UdpSocket::client_with_timestamping( SocketAddr::from((Ipv4Addr::LOCALHOST, 8012)), SocketAddr::from((Ipv4Addr::LOCALHOST, 8013)), InterfaceName::DEFAULT, EnableTimestamps { rx_software: true, tx_software: true, rx_hardware: false, tx_hardware: false, }, ) .await .unwrap(); let b = UdpSocket::client( SocketAddr::from((Ipv4Addr::LOCALHOST, 8013)), SocketAddr::from((Ipv4Addr::LOCALHOST, 8012)), ) .await .unwrap(); let (ssend, tsend) = a.send(&[1; 48]).await.unwrap(); let mut buf = [0; 48]; let (srecv, _, trecv) = b.recv(&mut buf).await.unwrap(); assert_eq!(ssend, 48); assert_eq!(srecv, 48); let tsend = tsend.unwrap(); let trecv = trecv.unwrap(); let delta = trecv - tsend; assert!(delta.to_seconds().abs() < 0.2); } }