forked from FDH2/UxPlay
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathraop_ntp.c
497 lines (419 loc) · 17.2 KB
/
raop_ntp.c
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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
/*
* Copyright (c) 2019 dsafa22 and 2014 Joakim Plate, modified by Florian Draschbacher,
* All Rights Reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
// Some of the code in here comes from https://github.com/juhovh/shairplay/pull/25/files
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include "raop.h"
#include "threads.h"
#include "compat.h"
#include "netutils.h"
#include "byteutils.h"
#include "utils.h"
#define RAOP_NTP_DATA_COUNT 8
#define RAOP_NTP_PHI_PPM 15ull // PPM
#define RAOP_NTP_R_RHO ((1ull << 32) / 1000u) // packet precision
#define RAOP_NTP_S_RHO ((1ull << 32) / 1000u) // system clock precision
#define RAOP_NTP_MAX_DIST ((1500ull << 32) / 1000u) // maximum allowed distance
#define RAOP_NTP_MAX_DISP ((16ull << 32)) // maximum dispersion
#define RAOP_NTP_CLOCK_BASE (2208988800ull << 32)
typedef struct raop_ntp_data_s {
uint64_t time; // The local wall clock time at time of ntp packet arrival
uint64_t dispersion;
int64_t delay; // The round trip delay
int64_t offset; // The difference between remote and local wall clock time
} raop_ntp_data_t;
struct raop_ntp_s {
logger_t *logger;
raop_callbacks_t callbacks;
int max_ntp_timeouts;
thread_handle_t thread;
mutex_handle_t run_mutex;
mutex_handle_t wait_mutex;
cond_handle_t wait_cond;
raop_ntp_data_t data[RAOP_NTP_DATA_COUNT];
int data_index;
// The clock sync params are periodically updated to the AirPlay client's NTP clock
mutex_handle_t sync_params_mutex;
int64_t sync_offset;
int64_t sync_dispersion;
int64_t sync_delay;
// Socket address of the AirPlay client
struct sockaddr_storage remote_saddr;
socklen_t remote_saddr_len;
// The remote port of the NTP server on the AirPlay client
unsigned short timing_rport;
// The local port of the NTP client on the AirPlay server
unsigned short timing_lport;
/* MUTEX LOCKED VARIABLES START */
/* These variables only edited mutex locked */
int running;
int joined;
// UDP socket
int tsock;
};
/*
* Used for sorting the data array by delay
*/
static int
raop_ntp_compare(const void* av, const void* bv)
{
const raop_ntp_data_t* a = (const raop_ntp_data_t*)av;
const raop_ntp_data_t* b = (const raop_ntp_data_t*)bv;
if (a->delay < b->delay) {
return -1;
} else if(a->delay > b->delay) {
return 1;
} else {
return 0;
}
}
static int
raop_ntp_parse_remote_address(raop_ntp_t *raop_ntp, const unsigned char *remote_addr, int remote_addr_len)
{
char current[25];
int family;
int ret;
assert(raop_ntp);
if (remote_addr_len == 4) {
family = AF_INET;
} else if (remote_addr_len == 16) {
family = AF_INET6;
} else {
return -1;
}
memset(current, 0, sizeof(current));
sprintf(current, "%d.%d.%d.%d", remote_addr[0], remote_addr[1], remote_addr[2], remote_addr[3]);
logger_log(raop_ntp->logger, LOGGER_DEBUG, "raop_ntp parse remote ip = %s", current);
ret = netutils_parse_address(family, current,
&raop_ntp->remote_saddr,
sizeof(raop_ntp->remote_saddr));
if (ret < 0) {
return -1;
}
raop_ntp->remote_saddr_len = ret;
return 0;
}
raop_ntp_t *raop_ntp_init(logger_t *logger, raop_callbacks_t *callbacks, const unsigned char *remote_addr,
int remote_addr_len, unsigned short timing_rport) {
raop_ntp_t *raop_ntp;
assert(logger);
assert(callbacks);
raop_ntp = calloc(1, sizeof(raop_ntp_t));
if (!raop_ntp) {
return NULL;
}
raop_ntp->logger = logger;
memcpy(&raop_ntp->callbacks, callbacks, sizeof(raop_callbacks_t));
raop_ntp->timing_rport = timing_rport;
if (raop_ntp_parse_remote_address(raop_ntp, remote_addr, remote_addr_len) < 0) {
free(raop_ntp);
return NULL;
}
// Set port on the remote address struct
((struct sockaddr_in *) &raop_ntp->remote_saddr)->sin_port = htons(timing_rport);
raop_ntp->running = 0;
raop_ntp->joined = 1;
uint64_t time = raop_ntp_get_local_time(raop_ntp);
for (int i = 0; i < RAOP_NTP_DATA_COUNT; ++i) {
raop_ntp->data[i].offset = 0ll;
raop_ntp->data[i].delay = RAOP_NTP_MAX_DISP;
raop_ntp->data[i].dispersion = RAOP_NTP_MAX_DISP;
raop_ntp->data[i].time = time;
}
raop_ntp->sync_delay = 0;
raop_ntp->sync_dispersion = 0;
raop_ntp->sync_offset = 0;
MUTEX_CREATE(raop_ntp->run_mutex);
MUTEX_CREATE(raop_ntp->wait_mutex);
COND_CREATE(raop_ntp->wait_cond);
MUTEX_CREATE(raop_ntp->sync_params_mutex);
return raop_ntp;
}
void
raop_ntp_destroy(raop_ntp_t *raop_ntp)
{
if (raop_ntp) {
raop_ntp_stop(raop_ntp);
MUTEX_DESTROY(raop_ntp->run_mutex);
MUTEX_DESTROY(raop_ntp->wait_mutex);
COND_DESTROY(raop_ntp->wait_cond);
MUTEX_DESTROY(raop_ntp->sync_params_mutex);
free(raop_ntp);
}
}
unsigned short raop_ntp_get_port(raop_ntp_t *raop_ntp) {
return raop_ntp->timing_lport;
}
static int
raop_ntp_init_socket(raop_ntp_t *raop_ntp, int use_ipv6)
{
assert(raop_ntp);
unsigned short tport = raop_ntp->timing_lport;
int tsock = netutils_init_socket(&tport, use_ipv6, 1);
if (tsock == -1) {
goto sockets_cleanup;
}
// We're calling recvfrom without knowing whether there is any data, so we need a timeout
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 300000;
if (setsockopt(tsock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
goto sockets_cleanup;
}
/* Set socket descriptors */
raop_ntp->tsock = tsock;
/* Set port values */
raop_ntp->timing_lport = tport;
logger_log(raop_ntp->logger, LOGGER_DEBUG, "raop_ntp local timing port socket %d port UDP %d", tsock, tport);
return 0;
sockets_cleanup:
if (tsock != -1) closesocket(tsock);
return -1;
}
static void
raop_ntp_flush_socket(int fd)
{
int bytes_available = 0;
while (ioctl(fd, FIONREAD, &bytes_available) == 0 && bytes_available > 0)
{
// We are guaranteed that we won't block, because bytes are available.
// Read 1 byte. Extra bytes in the datagram will be discarded.
char c;
int result = recvfrom(fd, &c, sizeof(c), 0, NULL, NULL);
if (result < 0)
{
break;
}
}
}
static THREAD_RETVAL
raop_ntp_thread(void *arg)
{
raop_ntp_t *raop_ntp = arg;
assert(raop_ntp);
unsigned char response[128];
int response_len;
unsigned char request[32] = {0x80, 0xd2, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
raop_ntp_data_t data_sorted[RAOP_NTP_DATA_COUNT];
const unsigned two_pow_n[RAOP_NTP_DATA_COUNT] = {2, 4, 8, 16, 32, 64, 128, 256};
int timeout_counter = 0;
bool conn_reset = false;
while (1) {
MUTEX_LOCK(raop_ntp->run_mutex);
if (!raop_ntp->running) {
MUTEX_UNLOCK(raop_ntp->run_mutex);
break;
}
MUTEX_UNLOCK(raop_ntp->run_mutex);
// Flush the socket in case a super delayed response arrived or something
raop_ntp_flush_socket(raop_ntp->tsock);
// Send request
uint64_t send_time = raop_ntp_get_local_time(raop_ntp);
byteutils_put_ntp_timestamp(request, 24, send_time);
int send_len = sendto(raop_ntp->tsock, (char *)request, sizeof(request), 0,
(struct sockaddr *) &raop_ntp->remote_saddr, raop_ntp->remote_saddr_len);
logger_log(raop_ntp->logger, LOGGER_DEBUG, "\nraop_ntp send_len = %d, now = %llu", send_len, send_time);
if (send_len < 0) {
logger_log(raop_ntp->logger, LOGGER_ERR, "raop_ntp error sending request");
} else {
// Read response
response_len = recvfrom(raop_ntp->tsock, (char *)response, sizeof(response), 0,
(struct sockaddr *) &raop_ntp->remote_saddr, &raop_ntp->remote_saddr_len);
if (response_len < 0) {
timeout_counter++;
char time[28];
int level = (timeout_counter == 1 ? LOGGER_DEBUG : LOGGER_ERR);
ntp_timestamp_to_time(send_time, time, sizeof(time));
logger_log(raop_ntp->logger, level, "raop_ntp receive timeout %d (limit %d) (request sent %s)",
timeout_counter, raop_ntp->max_ntp_timeouts, time);
if (timeout_counter == raop_ntp->max_ntp_timeouts) {
conn_reset = true; /* client is no longer responding */
break;
}
} else {
//local time of the server when the NTP response packet returns
int64_t t3 = (int64_t) raop_ntp_get_local_time(raop_ntp);
timeout_counter = 0;
char *str = utils_data_to_string(response, response_len, 16);
logger_log(raop_ntp->logger, LOGGER_DEBUG, "raop_ntp receive time type_t=%d packetlen = %d\n%s",
response[1] &~0x80, response_len, str);
free(str);
// Local time of the server when the NTP request packet leaves the server
int64_t t0 = (int64_t) byteutils_get_ntp_timestamp(response, 8);
// Local time of the client when the NTP request packet arrives at the client
int64_t t1 = (int64_t) byteutils_get_ntp_timestamp(response, 16);
// Local time of the client when the response message leaves the client
int64_t t2 = (int64_t) byteutils_get_ntp_timestamp(response, 24);
// The iOS client device sends its time in micro seconds relative to an arbitrary Epoch (the last boot).
// For a little bonus confusion, they add SECONDS_FROM_1900_TO_1970 * 1000000 us.
// This means we have to expect some rather huge offset, but its growth or shrink over time should be small.
raop_ntp->data_index = (raop_ntp->data_index + 1) % RAOP_NTP_DATA_COUNT;
raop_ntp->data[raop_ntp->data_index].time = t3;
raop_ntp->data[raop_ntp->data_index].offset = ((t1 - t0) + (t2 - t3)) / 2;
raop_ntp->data[raop_ntp->data_index].delay = ((t3 - t0) - (t2 - t1));
raop_ntp->data[raop_ntp->data_index].dispersion = RAOP_NTP_R_RHO + RAOP_NTP_S_RHO + (t3 - t0) * RAOP_NTP_PHI_PPM / 1000000u;
// Sort by delay
memcpy(data_sorted, raop_ntp->data, sizeof(data_sorted));
qsort(data_sorted, RAOP_NTP_DATA_COUNT, sizeof(data_sorted[0]), raop_ntp_compare);
uint64_t dispersion = 0ull;
int64_t offset = data_sorted[0].offset;
int64_t delay = data_sorted[RAOP_NTP_DATA_COUNT - 1].delay;
// Calculate dispersion
for(int i = 0; i < RAOP_NTP_DATA_COUNT; ++i) {
unsigned long long disp = raop_ntp->data[i].dispersion + (t3 - raop_ntp->data[i].time) * RAOP_NTP_PHI_PPM / 1000000u;
dispersion += disp / two_pow_n[i];
}
MUTEX_LOCK(raop_ntp->sync_params_mutex);
int64_t correction = offset - raop_ntp->sync_offset;
raop_ntp->sync_offset = offset;
raop_ntp->sync_dispersion = dispersion;
raop_ntp->sync_delay = delay;
MUTEX_UNLOCK(raop_ntp->sync_params_mutex);
logger_log(raop_ntp->logger, LOGGER_DEBUG, "raop_ntp sync correction = %lld", correction);
}
}
// Sleep for 3 seconds
struct timeval now;
struct timespec wait_time;
MUTEX_LOCK(raop_ntp->wait_mutex);
gettimeofday(&now, NULL);
wait_time.tv_sec = now.tv_sec + 3;
wait_time.tv_nsec = now.tv_usec * 1000;
pthread_cond_timedwait(&raop_ntp->wait_cond, &raop_ntp->wait_mutex, &wait_time);
MUTEX_UNLOCK(raop_ntp->wait_mutex);
}
// Ensure running reflects the actual state
MUTEX_LOCK(raop_ntp->run_mutex);
raop_ntp->running = false;
MUTEX_UNLOCK(raop_ntp->run_mutex);
logger_log(raop_ntp->logger, LOGGER_DEBUG, "raop_ntp exiting thread");
if (conn_reset && raop_ntp->callbacks.conn_reset) {
const bool video_reset = false; /* leave "frozen video" in place */
raop_ntp->callbacks.conn_reset(raop_ntp->callbacks.cls, timeout_counter, video_reset);
}
return 0;
}
void
raop_ntp_start(raop_ntp_t *raop_ntp, unsigned short *timing_lport, int max_ntp_timeouts)
{
logger_log(raop_ntp->logger, LOGGER_DEBUG, "raop_ntp starting time");
int use_ipv6 = 0;
assert(raop_ntp);
assert(timing_lport);
raop_ntp->max_ntp_timeouts = max_ntp_timeouts;
raop_ntp->timing_lport = *timing_lport;
MUTEX_LOCK(raop_ntp->run_mutex);
if (raop_ntp->running || !raop_ntp->joined) {
MUTEX_UNLOCK(raop_ntp->run_mutex);
return;
}
/* Initialize ports and sockets */
if (raop_ntp->remote_saddr.ss_family == AF_INET6) {
use_ipv6 = 1;
}
use_ipv6 = 0;
if (raop_ntp_init_socket(raop_ntp, use_ipv6) < 0) {
logger_log(raop_ntp->logger, LOGGER_ERR, "raop_ntp initializing timing socket failed");
MUTEX_UNLOCK(raop_ntp->run_mutex);
return;
}
*timing_lport = raop_ntp->timing_lport;
/* Create the thread and initialize running values */
raop_ntp->running = 1;
raop_ntp->joined = 0;
THREAD_CREATE(raop_ntp->thread, raop_ntp_thread, raop_ntp);
MUTEX_UNLOCK(raop_ntp->run_mutex);
}
void
raop_ntp_stop(raop_ntp_t *raop_ntp)
{
assert(raop_ntp);
/* Check that we are running and thread is not
* joined (should never be while still running) */
MUTEX_LOCK(raop_ntp->run_mutex);
if (!raop_ntp->running || raop_ntp->joined) {
MUTEX_UNLOCK(raop_ntp->run_mutex);
return;
}
raop_ntp->running = 0;
MUTEX_UNLOCK(raop_ntp->run_mutex);
logger_log(raop_ntp->logger, LOGGER_DEBUG, "raop_ntp stopping time thread");
MUTEX_LOCK(raop_ntp->wait_mutex);
COND_SIGNAL(raop_ntp->wait_cond);
MUTEX_UNLOCK(raop_ntp->wait_mutex);
if (raop_ntp->tsock != -1) {
closesocket(raop_ntp->tsock);
raop_ntp->tsock = -1;
}
THREAD_JOIN(raop_ntp->thread);
logger_log(raop_ntp->logger, LOGGER_DEBUG, "raop_ntp stopped time thread");
/* Mark thread as joined */
MUTEX_LOCK(raop_ntp->run_mutex);
raop_ntp->joined = 1;
MUTEX_UNLOCK(raop_ntp->run_mutex);
}
/**
* Converts from a little endian ntp timestamp to micro seconds since the Unix epoch.
* Does the same thing as byteutils_get_ntp_timestamp, except its input is an uint64_t
* and expected to already be in little endian.
* Please note this just converts to a different representation, the clock remains the
* same.
*/
uint64_t raop_ntp_timestamp_to_micro_seconds(uint64_t ntp_timestamp, bool account_for_epoch_diff) {
uint64_t seconds = ((ntp_timestamp >> 32) & 0xffffffff) - (account_for_epoch_diff ? SECONDS_FROM_1900_TO_1970 : 0);
uint64_t fraction = (ntp_timestamp & 0xffffffff);
return (seconds * 1000000) + ((fraction * 1000000) >> 32);
}
/**
* Returns the current time in micro seconds according to the local wall clock.
* The system Unix time is used as the local wall clock.
*/
uint64_t raop_ntp_get_local_time(raop_ntp_t *raop_ntp) {
struct timespec time;
clock_gettime(CLOCK_REALTIME, &time);
return (uint64_t)time.tv_sec * 1000000L + (uint64_t)(time.tv_nsec / 1000);
}
/**
* Returns the current time in micro seconds according to the remote wall clock.
*/
uint64_t raop_ntp_get_remote_time(raop_ntp_t *raop_ntp) {
MUTEX_LOCK(raop_ntp->sync_params_mutex);
int64_t offset = raop_ntp->sync_offset;
MUTEX_UNLOCK(raop_ntp->sync_params_mutex);
return (uint64_t) ((int64_t) raop_ntp_get_local_time(raop_ntp)) + ((int64_t) offset);
}
/**
* Returns the local wall clock time in micro seconds for the given point in remote clock time
*/
uint64_t raop_ntp_convert_remote_time(raop_ntp_t *raop_ntp, uint64_t remote_time) {
MUTEX_LOCK(raop_ntp->sync_params_mutex);
uint64_t offset = raop_ntp->sync_offset;
MUTEX_UNLOCK(raop_ntp->sync_params_mutex);
return (uint64_t) ((int64_t) remote_time) - ((int64_t) offset);
}
/**
* Returns the remote wall clock time in micro seconds for the given point in local clock time
*/
uint64_t raop_ntp_convert_local_time(raop_ntp_t *raop_ntp, uint64_t local_time) {
MUTEX_LOCK(raop_ntp->sync_params_mutex);
uint64_t offset = raop_ntp->sync_offset;
MUTEX_UNLOCK(raop_ntp->sync_params_mutex);
return (uint64_t) ((int64_t) local_time) + ((int64_t) offset);
}