summaryrefslogtreecommitdiff
path: root/Userland/Utilities/ntpquery.cpp
blob: 089c294fc55ae15c68b0cb77432568fee74fead1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/*
 * Copyright (c) 2020, Nico Weber <thakis@chromium.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#define _BSD_SOURCE
#define _DEFAULT_SOURCE
#include <AK/Endian.h>
#include <AK/Random.h>
#include <LibCore/ArgsParser.h>
#include <arpa/inet.h>
#include <inttypes.h>
#include <math.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/uio.h>
#include <time.h>

// 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.
typedef uint64_t NtpTimestamp;

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(sizeof(NtpPacket) == 48);

// 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.
const unsigned SecondsFrom1900To1970 = (70u * 365u + 70u / 4u) * 24u * 60u * 60u;

static NtpTimestamp ntp_timestamp_from_timeval(const timeval& 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<uint32_t>((static_cast<uint64_t>(t.tv_usec) << 32) / 1'000'000);

    return (static_cast<NtpTimestamp>(seconds) << 32) | fractional_bits;
}

static timeval timeval_from_ntp_timestamp(const NtpTimestamp& ntp_timestamp)
{
    timeval t;
    t.tv_sec = static_cast<time_t>(ntp_timestamp >> 32) - SecondsFrom1900To1970;
    t.tv_usec = static_cast<suseconds_t>((static_cast<uint64_t>(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;
}

int main(int argc, char** argv)
{
#ifdef __serenity__
    if (pledge("stdio inet unix settime", nullptr) < 0) {
        perror("pledge");
        return 1;
    }
#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
    const char* 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);
    args_parser.parse(argc, argv);

    if (adjust_time && set_time) {
        fprintf(stderr, "-a and -s are mutually exclusive\n");
        return 1;
    }

#ifdef __serenity__
    if (!adjust_time && !set_time) {
        if (pledge("stdio inet unix", nullptr) < 0) {
            perror("pledge");
            return 1;
        }
    }
#endif

    auto* hostent = gethostbyname(host);
    if (!hostent) {
        fprintf(stderr, "Lookup failed for '%s'\n", host);
        return 1;
    }

#ifdef __serenity__
    if (pledge((adjust_time || set_time) ? "stdio inet settime" : "stdio inet", nullptr) < 0) {
        perror("pledge");
        return 1;
    }
    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 = *(const in_addr_t*)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<NtpTimestamp>();
    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)) {
        fprintf(stderr, "incomplete packet send\n");
        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)) {
        fprintf(stderr, "incomplete packet recv\n");
        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) {
        fprintf(stderr, "unexpected version number %d\n", packet.version_number());
        return 1;
    }
    if (packet.mode() != 4) { // 4 means "server", which should be the reply to our 3 ("client") request.
        fprintf(stderr, "unexpected mode %d\n", packet.mode());
        return 1;
    }
    if (packet.stratum == 0 || packet.stratum >= 16) {
        fprintf(stderr, "unexpected stratum value %d\n", packet.stratum);
        return 1;
    }
    if (packet.origin_timestamp != random_transmit_timestamp) {
        fprintf(stderr, "expected %#016" PRIx64 " as origin timestamp, got %#016" PRIx64 "\n", random_transmit_timestamp, packet.origin_timestamp);
        return 1;
    }
    if (packet.transmit_timestamp == 0) {
        fprintf(stderr, "got transmit_timestamp 0\n");
        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) {
        printf("NTP response from %s:\n", inet_ntoa(peer_address.sin_addr));
        printf("Leap Information: %d\n", packet.leap_information());
        printf("Version Number: %d\n", packet.version_number());
        printf("Mode: %d\n", packet.mode());
        printf("Stratum: %d\n", packet.stratum);
        printf("Poll: %d\n", packet.stratum);
        printf("Precision: %d\n", packet.precision);
        printf("Root delay: %#x\n", ntohl(packet.root_delay));
        printf("Root dispersion: %#x\n", ntohl(packet.root_dispersion));

        u32 ref_id = ntohl(packet.reference_id);
        printf("Reference ID: %#x", ref_id);
        if (packet.stratum == 1) {
            printf(" ('%c%c%c%c')", (ref_id & 0xff000000) >> 24, (ref_id & 0xff0000) >> 16, (ref_id & 0xff00) >> 8, ref_id & 0xff);
        }
        printf("\n");

        printf("Reference timestamp:   %#016" PRIx64 " (%s)\n", be64toh(packet.reference_timestamp), format_ntp_timestamp(be64toh(packet.reference_timestamp)).characters());
        printf("Origin timestamp:      %#016" PRIx64 " (%s)\n", origin_timestamp, format_ntp_timestamp(origin_timestamp).characters());
        printf("Receive timestamp:     %#016" PRIx64 " (%s)\n", receive_timestamp, format_ntp_timestamp(receive_timestamp).characters());
        printf("Transmit timestamp:    %#016" PRIx64 " (%s)\n", transmit_timestamp, format_ntp_timestamp(transmit_timestamp).characters());
        printf("Destination timestamp: %#016" PRIx64 " (%s)\n", 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.
        printf("Receive latency: %" PRId64 ".%06d s\n", (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<int64_t>(to - from) / pow(2.0, 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)
        printf("Delay: %f\n", delay_s);
    printf("Offset: %f\n", offset_s);

    if (adjust_time) {
        long delta_us = static_cast<long>(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;
        }
    }
}