/* * Copyright (c) 2020, Nico Weber * * SPDX-License-Identifier: BSD-2-Clause */ #define _BSD_SOURCE #define _DEFAULT_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // An NtpTimestamp is a 64-bit integer that's a 32.32 binary-fixed point number. // The integral part in the upper 32 bits represents seconds since 1900-01-01. // The fractional part in the lower 32 bits stores fractional bits times 2 ** 32. using NtpTimestamp = uint64_t; struct [[gnu::packed]] NtpPacket { uint8_t li_vn_mode; uint8_t stratum; int8_t poll; int8_t precision; uint32_t root_delay; uint32_t root_dispersion; uint32_t reference_id; NtpTimestamp reference_timestamp; NtpTimestamp origin_timestamp; NtpTimestamp receive_timestamp; NtpTimestamp transmit_timestamp; uint8_t leap_information() const { return li_vn_mode >> 6; } uint8_t version_number() const { return (li_vn_mode >> 3) & 7; } uint8_t mode() const { return li_vn_mode & 7; } }; static_assert(AssertSize()); // NTP measures time in seconds since 1900-01-01, POSIX in seconds since 1970-01-01. // 1900 wasn't a leap year, so there are 70/4 leap years between 1900 and 1970. // Overflows a 32-bit signed int, but not a 32-bit unsigned int. unsigned const SecondsFrom1900To1970 = (70u * 365u + 70u / 4u) * 24u * 60u * 60u; static NtpTimestamp ntp_timestamp_from_timeval(timeval const& t) { VERIFY(t.tv_usec >= 0 && t.tv_usec < 1'000'000); // Fits in 20 bits when normalized. // Seconds just need translation to the different origin. uint32_t seconds = t.tv_sec + SecondsFrom1900To1970; // Fractional bits are decimal fixed point (*1'000'000) in timeval, but binary fixed-point (* 2**32) in NTP timestamps. uint32_t fractional_bits = static_cast((static_cast(t.tv_usec) << 32) / 1'000'000); return (static_cast(seconds) << 32) | fractional_bits; } static timeval timeval_from_ntp_timestamp(NtpTimestamp const& ntp_timestamp) { timeval t; t.tv_sec = static_cast(ntp_timestamp >> 32) - SecondsFrom1900To1970; t.tv_usec = static_cast((static_cast(ntp_timestamp & 0xFFFFFFFFu) * 1'000'000) >> 32); return t; } static String format_ntp_timestamp(NtpTimestamp ntp_timestamp) { char buffer[28]; // YYYY-MM-DDTHH:MM:SS.UUUUUUZ is 27 characters long. timeval t = timeval_from_ntp_timestamp(ntp_timestamp); struct tm tm; gmtime_r(&t.tv_sec, &tm); size_t written = strftime(buffer, sizeof(buffer), "%Y-%m-%dT%T.", &tm); VERIFY(written == 20); written += snprintf(buffer + written, sizeof(buffer) - written, "%06d", (int)t.tv_usec); VERIFY(written == 26); buffer[written++] = 'Z'; buffer[written] = '\0'; return buffer; } #ifdef __serenity__ ErrorOr serenity_main(Main::Arguments arguments) #else int main(int argc, char** argv) #endif { #ifdef __serenity__ TRY(Core::System::pledge("stdio inet unix settime")); #endif bool adjust_time = false; bool set_time = false; bool verbose = false; // FIXME: Change to serenityos.pool.ntp.org once https://manage.ntppool.org/manage/vendor/zone?a=km5a8h&id=vz-14154g is approved. // Other NTP servers: // - time.nist.gov // - time.apple.com // - time.cloudflare.com (has NTS), https://blog.cloudflare.com/secure-time/ // - time.windows.com // // Leap seconds smearing NTP servers: // - time.facebook.com , https://engineering.fb.com/production-engineering/ntp-service/ , sine-smears over 18 hours // - time.google.com , https://developers.google.com/time/smear , linear-smears over 24 hours char const* host = "time.google.com"; Core::ArgsParser args_parser; args_parser.add_option(adjust_time, "Gradually adjust system time (requires root)", "adjust", 'a'); args_parser.add_option(set_time, "Immediately set system time (requires root)", "set", 's'); args_parser.add_option(verbose, "Verbose output", "verbose", 'v'); args_parser.add_positional_argument(host, "NTP server", "host", Core::ArgsParser::Required::No); #ifdef __serenity__ args_parser.parse(arguments); #else args_parser.parse(argc, argv); #endif if (adjust_time && set_time) { warnln("-a and -s are mutually exclusive"); return 1; } #ifdef __serenity__ if (!adjust_time && !set_time) { TRY(Core::System::pledge("stdio inet unix")); } #endif auto* hostent = gethostbyname(host); if (!hostent) { warnln("Lookup failed for '{}'", host); return 1; } #ifdef __serenity__ TRY(Core::System::pledge((adjust_time || set_time) ? "stdio inet settime"sv : "stdio inet"sv)); TRY(Core::System::unveil(nullptr, nullptr)); #endif int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (fd < 0) { perror("socket"); return 1; } struct timeval timeout { 5, 0 }; if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { perror("setsockopt"); return 1; } int enable = 1; if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &enable, sizeof(enable)) < 0) { perror("setsockopt"); return 1; } sockaddr_in peer_address; memset(&peer_address, 0, sizeof(peer_address)); peer_address.sin_family = AF_INET; peer_address.sin_port = htons(123); peer_address.sin_addr.s_addr = *(in_addr_t const*)hostent->h_addr_list[0]; NtpPacket packet; memset(&packet, 0, sizeof(packet)); packet.li_vn_mode = (4 << 3) | 3; // Version 4, client connection. // The server will copy the transmit_timestamp to origin_timestamp in the reply. // To not leak the local time, keep the time we sent the packet locally and // send random bytes to the server. auto random_transmit_timestamp = get_random(); timeval local_transmit_time; gettimeofday(&local_transmit_time, nullptr); packet.transmit_timestamp = random_transmit_timestamp; ssize_t rc; rc = sendto(fd, &packet, sizeof(packet), 0, (const struct sockaddr*)&peer_address, sizeof(peer_address)); if (rc < 0) { perror("sendto"); return 1; } if ((size_t)rc < sizeof(packet)) { warnln("incomplete packet send"); return 1; } iovec iov { &packet, sizeof(packet) }; char control_message_buffer[CMSG_SPACE(sizeof(timeval))]; msghdr msg = { &peer_address, sizeof(peer_address), &iov, 1, control_message_buffer, sizeof(control_message_buffer), 0 }; rc = recvmsg(fd, &msg, 0); if (rc < 0) { perror("recvmsg"); return 1; } timeval userspace_receive_time; gettimeofday(&userspace_receive_time, nullptr); if ((size_t)rc < sizeof(packet)) { warnln("incomplete packet recv"); return 1; } cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); VERIFY(cmsg->cmsg_level == SOL_SOCKET); VERIFY(cmsg->cmsg_type == SCM_TIMESTAMP); VERIFY(!CMSG_NXTHDR(&msg, cmsg)); timeval kernel_receive_time; memcpy(&kernel_receive_time, CMSG_DATA(cmsg), sizeof(kernel_receive_time)); // Checks 3 and 4 from end of section 5 of rfc4330. if (packet.version_number() != 3 && packet.version_number() != 4) { warnln("unexpected version number {}", packet.version_number()); return 1; } if (packet.mode() != 4) { // 4 means "server", which should be the reply to our 3 ("client") request. warnln("unexpected mode {}", packet.mode()); return 1; } if (packet.stratum == 0 || packet.stratum >= 16) { warnln("unexpected stratum value {}", packet.stratum); return 1; } if (packet.origin_timestamp != random_transmit_timestamp) { warnln("expected {:#016x} as origin timestamp, got {:#016x}", random_transmit_timestamp, packet.origin_timestamp); return 1; } if (packet.transmit_timestamp == 0) { warnln("got transmit_timestamp 0"); return 1; } NtpTimestamp origin_timestamp = ntp_timestamp_from_timeval(local_transmit_time); NtpTimestamp receive_timestamp = be64toh(packet.receive_timestamp); NtpTimestamp transmit_timestamp = be64toh(packet.transmit_timestamp); NtpTimestamp destination_timestamp = ntp_timestamp_from_timeval(kernel_receive_time); timeval kernel_to_userspace_latency; timersub(&userspace_receive_time, &kernel_receive_time, &kernel_to_userspace_latency); if (set_time) { // FIXME: Do all the time filtering described in 5905, or at least correct for time of flight. timeval t = timeval_from_ntp_timestamp(transmit_timestamp); if (settimeofday(&t, nullptr) < 0) { perror("settimeofday"); return 1; } } if (verbose) { outln("NTP response from {}:", inet_ntoa(peer_address.sin_addr)); outln("Leap Information: {}", packet.leap_information()); outln("Version Number: {}", packet.version_number()); outln("Mode: {}", packet.mode()); outln("Stratum: {}", packet.stratum); outln("Poll: {}", packet.stratum); outln("Precision: {}", packet.precision); outln("Root delay: {:x}", ntohl(packet.root_delay)); outln("Root dispersion: {:x}", ntohl(packet.root_dispersion)); u32 ref_id = ntohl(packet.reference_id); out("Reference ID: {:x}", ref_id); if (packet.stratum == 1) { out(" ('{:c}{:c}{:c}{:c}')", (ref_id & 0xff000000) >> 24, (ref_id & 0xff0000) >> 16, (ref_id & 0xff00) >> 8, ref_id & 0xff); } outln(); outln("Reference timestamp: {:#016x} ({})", be64toh(packet.reference_timestamp), format_ntp_timestamp(be64toh(packet.reference_timestamp)).characters()); outln("Origin timestamp: {:#016x} ({})", origin_timestamp, format_ntp_timestamp(origin_timestamp).characters()); outln("Receive timestamp: {:#016x} ({})", receive_timestamp, format_ntp_timestamp(receive_timestamp).characters()); outln("Transmit timestamp: {:#016x} ({})", transmit_timestamp, format_ntp_timestamp(transmit_timestamp).characters()); outln("Destination timestamp: {:#016x} ({})", destination_timestamp, format_ntp_timestamp(destination_timestamp).characters()); // When the system isn't under load, user-space t and packet_t are identical. If a shell with `yes` is running, it can be as high as 30ms in this program, // which gets user-space time immediately after the recvmsg() call. In programs that have an event loop reading from multiple sockets, it could be higher. outln("Receive latency: {}.{:06} s", (i64)kernel_to_userspace_latency.tv_sec, (int)kernel_to_userspace_latency.tv_usec); } // Parts of the "Clock Filter" computations, https://tools.ietf.org/html/rfc5905#section-10 NtpTimestamp T1 = origin_timestamp; NtpTimestamp T2 = receive_timestamp; NtpTimestamp T3 = transmit_timestamp; NtpTimestamp T4 = destination_timestamp; auto timestamp_difference_in_seconds = [](NtpTimestamp from, NtpTimestamp to) { return static_cast(to - from) >> 32; }; // The network round-trip time of the request. // T4-T1 is the wall clock roundtrip time, in local ticks. // T3-T2 is the server side processing time, in server ticks. double delay_s = timestamp_difference_in_seconds(T1, T4) - timestamp_difference_in_seconds(T2, T3); // The offset from local time to server time, ignoring network delay. // Both T2-T1 and T3-T4 estimate this; this takes the average of both. // Or, equivalently, (T1+T4)/2 estimates local time, (T2+T3)/2 estimate server time, this is the difference. double offset_s = 0.5 * (timestamp_difference_in_seconds(T1, T2) + timestamp_difference_in_seconds(T4, T3)); if (verbose) outln("Delay: {}", delay_s); outln("Offset: {}", offset_s); if (adjust_time) { long delta_us = static_cast(round(offset_s * 1'000'000)); timeval delta_timeval; delta_timeval.tv_sec = delta_us / 1'000'000; delta_timeval.tv_usec = delta_us % 1'000'000; if (delta_timeval.tv_usec < 0) { delta_timeval.tv_sec--; delta_timeval.tv_usec += 1'000'000; } if (adjtime(&delta_timeval, nullptr) < 0) { perror("adjtime set"); return 1; } } return 0; }