xref: /third_party/toybox/toys/net/sntp.c (revision 0f66f451)
1/* sntp.c - sntp client and server
2 *
3 * Copyright 2019 Rob Landley <rob@landley.net>
4 *
5 * See https://www.ietf.org/rfc/rfc4330.txt
6
7  modes: oneshot display, oneshot set, persist, serve, multi
8
9USE_SNTP(NEWTOY(sntp, "M:m:Sp:asdDqr#<4>17=10[!as]", TOYFLAG_USR|TOYFLAG_BIN))
10
11config SNTP
12  bool "sntp"
13  default y
14  help
15    usage: sntp [-saSdDqm] [-r SHIFT] [-m ADDRESS] [-p PORT] [SERVER]
16
17    Simple Network Time Protocol client. Query SERVER and display time.
18
19    -p	Use PORT (default 123)
20    -s	Set system clock suddenly
21    -a	Adjust system clock gradually
22    -S	Serve time instead of querying (bind to SERVER address if specified)
23    -m	Wait for updates from multicast ADDRESS (RFC 4330 says use 224.0.1.1)
24    -M	Multicast server on ADDRESS
25    -d	Daemonize (run in background re-querying )
26    -D	Daemonize but stay in foreground: re-query time every 1000 seconds
27    -r	Retry shift (every 1<<SHIFT seconds)
28    -q	Quiet (don't display time)
29*/
30
31#define FOR_sntp
32#include "toys.h"
33
34GLOBALS(
35  long r;
36  char *p, *m, *M;
37)
38
39// Seconds from 1900 to 1970, including appropriate leap days
40#define SEVENTIES 2208988800L
41
42// Get time and return ntptime (saving timespec in pointer if not null)
43// NTP time is high 32 bits = seconds since 1970 (blame RFC 868), low 32 bits
44// fraction of a second.
45// diff is how far off we think our clock is from reality (in nanoseconds)
46static unsigned long long lunchtime(struct timespec *television, long long diff)
47{
48  struct timespec tv;
49
50  clock_gettime(CLOCK_REALTIME, &tv);
51  if (diff) nanomove(&tv, diff);
52
53  if (television) *television = tv;
54
55  // Unix time is 1970 but RFCs 868 and 958 said 1900 so add seconds 1900->1970
56  // If they'd done a 34/30 bit split the Y2036 problem would be centuries
57  // from now and still give us nanosecond accuracy, but no...
58  return ((tv.tv_sec+SEVENTIES)<<32)+(((long long)tv.tv_nsec)<<32)/1000000000;
59}
60
61// convert ntptime back to struct timespec.
62static void doublyso(unsigned long long now, struct timespec *tv)
63{
64  // Y2036 fixup: if time wrapped, it's in the future
65  tv->tv_sec = (now>>32) + (1LL<<32)*!(now&(1LL<<63));
66  tv->tv_sec -= SEVENTIES; // Force signed math for Y2038 fixup
67  tv->tv_nsec = ((now&0xFFFFFFFF)*1000000000)>>32;
68}
69
70void sntp_main(void)
71{
72  struct timespec tv, tv2;
73  unsigned long long *pktime = (void *)toybuf, now, then, before = before;
74  long long diff = 0;
75  struct addrinfo *ai;
76  union socksaddr sa;
77  int fd, tries = 0;
78
79  if (!(FLAG(S)||FLAG(m)) && !*toys.optargs)
80    error_exit("Need -Sm or SERVER address");
81
82  // Lookup address and open server or client UDP socket
83  if (!TT.p || !*TT.p) TT.p = "123";
84  ai = xgetaddrinfo(*toys.optargs, TT.p, AF_UNSPEC, SOCK_DGRAM, IPPROTO_UDP,
85    AI_PASSIVE*!*toys.optargs);
86
87  if (FLAG(d) && daemon(0, 0)) perror_exit("daemonize");
88
89  // Act as server if necessary
90  if (FLAG(S)|FLAG(m)) {
91    fd = xbindany(ai);
92    if (TT.m) {
93      struct ip_mreq group;
94
95      // subscribe to multicast group
96      memset(&group, 0, sizeof(group));
97      group.imr_multiaddr.s_addr = inet_addr(TT.m);
98      xsetsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group));
99    }
100  } else fd = xsocket(ai->ai_family, SOCK_DGRAM, IPPROTO_UDP);
101
102  // -Sm = loop waiting for input
103  // -Dd = loop polling time and waiting until next poll period
104  // Otherwise poll up to 3 times to get 2 responses, then exit
105
106  // loop sending/receiving packets
107  for (;;) {
108    now = millitime();
109
110    // Figure out if we're in server and multicast modes don't poll
111    if (FLAG(m) || FLAG(S)) then = -1;
112
113    // daemon and oneshot modes send a packet each time through outer loop
114    else {
115      then = now + 3000;
116      if (FLAG(d) || FLAG(D)) then = now + (1<<TT.r)*1000;
117
118      // Send NTP query packet
119      memset(toybuf, 0, 48);
120      *toybuf = 0xe3; // li = 3 (unsynchronized), version = 4, mode = 3 (client)
121      toybuf[2] = 8;  // poll frequency 1<<8 = 256 seconds
122      pktime[5] = SWAP_BE64(before = lunchtime(&tv, diff));
123      xsendto(fd, toybuf, 48, ai->ai_addr);
124    }
125
126    // Loop receiving packets until it's time to send the next one.
127    for (;;) {
128      int strike;
129
130      // Wait to receive a packet
131
132      if (then>0 && then<(now = millitime())) break;;
133      strike = xrecvwait(fd, toybuf, sizeof(toybuf), &sa, then-now);
134      if (strike<1) {
135        if (!(FLAG(S)||FLAG(m)||FLAG(D)||FLAG(d)) && ++tries == 3)
136          error_exit("no reply from %s", *toys.optargs);
137        break;
138      }
139      if (strike<48) continue;
140
141      // Validate packet
142      if (!FLAG(S) || FLAG(m)) {
143        char buf[128];
144        int mode = 7&*toybuf;
145
146        // Is source address what we expect?
147        xstrncpy(buf, ntop(ai->ai_addr), 128);
148        strike = strcmp(buf, ntop((void *)&sa));
149        // Does this reply's originate timestamp match the packet we sent?
150        if (!FLAG(S) && !FLAG(m) && before != SWAP_BE64(pktime[3])) continue;
151        // Ignore packets from wrong address or with wrong mode
152        if (strike && !FLAG(S)) continue;
153        if (!((FLAG(m) && mode==5) || (FLAG(S) && mode==3) ||
154            (!FLAG(m) && !FLAG(S) && mode==4))) continue;
155      }
156
157      // If received a -S request packet, send server packet
158      if (strike) {
159        char *buf = toybuf+48;
160
161        *buf = 0x24;  // LI 0 VN 4 mode 4.
162        buf[1] = 3;   // stratum 3
163        buf[2] = 10;  // recommended retry every 1<<10=1024 seconds
164        buf[3] = 250; // precision -6, minimum allowed
165        strcpy(buf+12, "LOCL");
166        pktime[6+3] = pktime[5]; // send back reference time they sent us
167        // everything else is current time
168        pktime[6+2] = pktime[6+4] = pktime[6+5] = SWAP_BE64(lunchtime(0, 0));
169        xsendto(fd, buf, 48, (void *)&sa);
170
171      // Got a time packet from a recognized server
172      } else {
173        int unset = !diff;
174
175        // First packet: figure out how far off our clock is from what server
176        // said and try again. Don't set clock, just record offset to use
177        // generating second reuest. (We know this time is in the past
178        // because transmission took time, but it's a start. And if time is
179        // miraculously exact, don't loop.)
180
181        lunchtime(&tv2, diff);
182        diff = nanodiff(&tv, &tv2);
183        if (unset && diff) break;
184
185        // Second packet: determine midpoint of packet transit time according
186        // to local clock, assuming each direction took same time so midpoint
187        // is time server reported. The first television was the adjusted time
188        // we sent the packet at, tv2 is what server replied, so now diff
189        // is round trip time.
190
191        // What time did the server say and how far off are we?
192        nanomove(&tv, diff/2);
193        doublyso(SWAP_BE64(pktime[5]), &tv2);
194        diff = nanodiff(&tv, &tv2);
195
196        if (FLAG(s)) {
197          // Do read/adjust/set to lose as little time as possible.
198          clock_gettime(CLOCK_REALTIME, &tv2);
199          nanomove(&tv2, diff);
200          if (clock_settime(CLOCK_REALTIME, &tv2))
201            perror_exit("clock_settime");
202        } else if (FLAG(a)) {
203          struct timeval why;
204
205          // call adjtime() to move the clock gradually, copying nanoseconds
206          // into gratuitous microseconds structure for sad historical reasons
207          memset(&tv2, 0, sizeof(tv2));
208          nanomove(&tv2, diff);
209          why.tv_sec = tv2.tv_sec;
210          why.tv_usec = tv2.tv_nsec/1000;
211          if (adjtime(&why, 0)) perror_exit("adjtime");
212        }
213
214        // Display the time and offset
215        if (!FLAG(q)) {
216          format_iso_time(toybuf, sizeof(toybuf)-1, &tv2);
217          printf("%s offset %c%lld.%09lld secs\n", toybuf, (diff<0) ? '-' : '+',
218            llabs(diff/1000000000), llabs(diff%1000000000));
219        }
220
221        // If we're not in daemon mode, we're done. (Can't get here for -S.)
222        if (!FLAG(d) && !FLAG(D)) return;
223      }
224    }
225  }
226}
227