Count served timestamps in all combinations of RX/TX and daemon/kernel/hardware. Repurpose CLG_LogAuthNtpRequest() to update all NTP-specific stats in one call per accepted request and response.
1111 lines
29 KiB
C
1111 lines
29 KiB
C
/*
|
|
chronyd/chronyc - Programs for keeping computer clocks accurate.
|
|
|
|
**********************************************************************
|
|
* Copyright (C) Richard P. Curnow 1997-2003
|
|
* Copyright (C) Miroslav Lichvar 2009, 2015-2017, 2021
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of version 2 of the GNU General Public License as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program 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
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
**********************************************************************
|
|
|
|
=======================================================================
|
|
|
|
This module keeps a count of the number of successful accesses by
|
|
clients, and the times of the last accesses.
|
|
|
|
This can be used for status reporting, and (in the case of a
|
|
server), if it needs to know which clients have made use of its data
|
|
recently.
|
|
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include "sysincl.h"
|
|
|
|
#include "array.h"
|
|
#include "clientlog.h"
|
|
#include "conf.h"
|
|
#include "local.h"
|
|
#include "memory.h"
|
|
#include "ntp.h"
|
|
#include "reports.h"
|
|
#include "util.h"
|
|
#include "logging.h"
|
|
|
|
#define MAX_SERVICES 3
|
|
|
|
typedef struct {
|
|
IPAddr ip_addr;
|
|
uint32_t last_hit[MAX_SERVICES];
|
|
uint32_t hits[MAX_SERVICES];
|
|
uint16_t drops[MAX_SERVICES];
|
|
uint16_t tokens[MAX_SERVICES];
|
|
int8_t rate[MAX_SERVICES];
|
|
int8_t ntp_timeout_rate;
|
|
uint8_t drop_flags;
|
|
} Record;
|
|
|
|
/* Hash table of records, there is a fixed number of records per slot */
|
|
static ARR_Instance records;
|
|
|
|
#define SLOT_BITS 4
|
|
|
|
/* Number of records in one slot of the hash table */
|
|
#define SLOT_SIZE (1U << SLOT_BITS)
|
|
|
|
/* Minimum number of slots */
|
|
#define MIN_SLOTS 1
|
|
|
|
/* Maximum number of slots, this is a hard limit */
|
|
#define MAX_SLOTS (1U << (24 - SLOT_BITS))
|
|
|
|
/* Number of slots in the hash table */
|
|
static unsigned int slots;
|
|
|
|
/* Maximum number of slots given memory allocation limit */
|
|
static unsigned int max_slots;
|
|
|
|
/* Times of last hits are saved as 32-bit fixed point values */
|
|
#define TS_FRAC 4
|
|
#define INVALID_TS 0
|
|
|
|
/* Static offset included in conversion to the fixed-point timestamps to
|
|
randomise their alignment */
|
|
static uint32_t ts_offset;
|
|
|
|
/* Request rates are saved in the record as 8-bit scaled log2 values */
|
|
#define RATE_SCALE 4
|
|
#define MIN_RATE (-14 * RATE_SCALE)
|
|
#define INVALID_RATE -128
|
|
|
|
/* Response rates are controlled by token buckets. The capacity and
|
|
number of tokens spent on response are determined from configured
|
|
minimum inverval between responses (in log2) and burst length. */
|
|
|
|
#define MIN_LIMIT_INTERVAL (-15 - TS_FRAC)
|
|
#define MAX_LIMIT_INTERVAL 12
|
|
#define MIN_LIMIT_BURST 1
|
|
#define MAX_LIMIT_BURST 255
|
|
|
|
static uint16_t max_tokens[MAX_SERVICES];
|
|
static uint16_t tokens_per_hit[MAX_SERVICES];
|
|
|
|
/* Reduction of token rates to avoid overflow of 16-bit counters. Negative
|
|
shift is used for coarse limiting with intervals shorter than -TS_FRAC. */
|
|
static int token_shift[MAX_SERVICES];
|
|
|
|
/* Rates at which responses are randomly allowed (in log2) when the
|
|
buckets don't have enough tokens. This is necessary in order to
|
|
prevent an attacker sending requests with spoofed source address
|
|
from blocking responses to the address completely. */
|
|
|
|
#define MIN_LEAK_RATE 1
|
|
#define MAX_LEAK_RATE 4
|
|
|
|
static int leak_rate[MAX_SERVICES];
|
|
|
|
/* Limit intervals in log2 */
|
|
static int limit_interval[MAX_SERVICES];
|
|
|
|
/* Flag indicating whether facility is turned on or not */
|
|
static int active;
|
|
|
|
/* RX and TX timestamp saved for clients using interleaved mode */
|
|
typedef struct {
|
|
uint64_t rx_ts;
|
|
uint8_t flags;
|
|
uint8_t tx_ts_source;
|
|
uint16_t slew_epoch;
|
|
int32_t tx_ts_offset;
|
|
} NtpTimestamps;
|
|
|
|
/* Flags for NTP timestamps */
|
|
#define NTPTS_DISABLED 1
|
|
#define NTPTS_VALID_TX 2
|
|
|
|
/* RX->TX map using a circular buffer with ordered timestamps */
|
|
typedef struct {
|
|
ARR_Instance timestamps;
|
|
uint32_t first;
|
|
uint32_t size;
|
|
uint32_t max_size;
|
|
uint32_t cached_index;
|
|
uint64_t cached_rx_ts;
|
|
uint16_t slew_epoch;
|
|
double slew_offset;
|
|
} NtpTimestampMap;
|
|
|
|
static NtpTimestampMap ntp_ts_map;
|
|
|
|
/* Maximum interval of NTP timestamps in future after a backward step */
|
|
#define NTPTS_FUTURE_LIMIT (1LL << 32) /* 1 second */
|
|
|
|
/* Maximum number of timestamps moved in the array to insert a new timestamp */
|
|
#define NTPTS_INSERT_LIMIT 64
|
|
|
|
/* Maximum expected value of the timestamp source */
|
|
#define MAX_NTP_TS NTP_TS_HARDWARE
|
|
|
|
/* Global statistics */
|
|
static uint32_t total_hits[MAX_SERVICES];
|
|
static uint32_t total_drops[MAX_SERVICES];
|
|
static uint32_t total_ntp_auth_hits;
|
|
static uint32_t total_ntp_interleaved_hits;
|
|
static uint32_t total_record_drops;
|
|
static uint32_t total_ntp_rx_timestamps[MAX_NTP_TS + 1];
|
|
static uint32_t total_ntp_tx_timestamps[MAX_NTP_TS + 1];
|
|
|
|
#define NSEC_PER_SEC 1000000000U
|
|
|
|
/* ================================================== */
|
|
|
|
static int expand_hashtable(void);
|
|
static void handle_slew(struct timespec *raw, struct timespec *cooked, double dfreq,
|
|
double doffset, LCL_ChangeType change_type, void *anything);
|
|
|
|
/* ================================================== */
|
|
|
|
static int
|
|
compare_ts(uint32_t x, uint32_t y)
|
|
{
|
|
if (x == y)
|
|
return 0;
|
|
if (y == INVALID_TS)
|
|
return 1;
|
|
return (int32_t)(x - y) > 0 ? 1 : -1;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static int
|
|
compare_total_hits(Record *x, Record *y)
|
|
{
|
|
uint32_t x_hits, y_hits;
|
|
int i;
|
|
|
|
for (i = 0, x_hits = y_hits = 0; i < MAX_SERVICES; i++) {
|
|
x_hits += x->hits[i];
|
|
y_hits += y->hits[i];
|
|
}
|
|
|
|
return x_hits > y_hits ? 1 : -1;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static Record *
|
|
get_record(IPAddr *ip)
|
|
{
|
|
uint32_t last_hit = 0, oldest_hit = 0;
|
|
Record *record, *oldest_record;
|
|
unsigned int first, i, j;
|
|
|
|
if (!active || (ip->family != IPADDR_INET4 && ip->family != IPADDR_INET6))
|
|
return NULL;
|
|
|
|
while (1) {
|
|
/* Get index of the first record in the slot */
|
|
first = UTI_IPToHash(ip) % slots * SLOT_SIZE;
|
|
|
|
for (i = 0, oldest_record = NULL; i < SLOT_SIZE; i++) {
|
|
record = ARR_GetElement(records, first + i);
|
|
|
|
if (!UTI_CompareIPs(ip, &record->ip_addr, NULL))
|
|
return record;
|
|
|
|
if (record->ip_addr.family == IPADDR_UNSPEC)
|
|
break;
|
|
|
|
for (j = 0; j < MAX_SERVICES; j++) {
|
|
if (j == 0 || compare_ts(last_hit, record->last_hit[j]) < 0)
|
|
last_hit = record->last_hit[j];
|
|
}
|
|
|
|
if (!oldest_record || compare_ts(oldest_hit, last_hit) > 0 ||
|
|
(oldest_hit == last_hit && compare_total_hits(oldest_record, record) > 0)) {
|
|
oldest_record = record;
|
|
oldest_hit = last_hit;
|
|
}
|
|
}
|
|
|
|
/* If the slot still has an empty record, use it */
|
|
if (record->ip_addr.family == IPADDR_UNSPEC)
|
|
break;
|
|
|
|
/* Resize the table if possible and try again as the new slot may
|
|
have some empty records */
|
|
if (expand_hashtable())
|
|
continue;
|
|
|
|
/* There is no other option, replace the oldest record */
|
|
record = oldest_record;
|
|
total_record_drops++;
|
|
break;
|
|
}
|
|
|
|
record->ip_addr = *ip;
|
|
for (i = 0; i < MAX_SERVICES; i++)
|
|
record->last_hit[i] = INVALID_TS;
|
|
for (i = 0; i < MAX_SERVICES; i++)
|
|
record->hits[i] = 0;
|
|
for (i = 0; i < MAX_SERVICES; i++)
|
|
record->drops[i] = 0;
|
|
for (i = 0; i < MAX_SERVICES; i++)
|
|
record->tokens[i] = max_tokens[i];
|
|
for (i = 0; i < MAX_SERVICES; i++)
|
|
record->rate[i] = INVALID_RATE;
|
|
record->ntp_timeout_rate = INVALID_RATE;
|
|
record->drop_flags = 0;
|
|
|
|
return record;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static int
|
|
expand_hashtable(void)
|
|
{
|
|
ARR_Instance old_records;
|
|
Record *old_record, *new_record;
|
|
unsigned int i;
|
|
|
|
old_records = records;
|
|
|
|
if (2 * slots > max_slots)
|
|
return 0;
|
|
|
|
records = ARR_CreateInstance(sizeof (Record));
|
|
|
|
slots = MAX(MIN_SLOTS, 2 * slots);
|
|
assert(slots <= max_slots);
|
|
|
|
ARR_SetSize(records, slots * SLOT_SIZE);
|
|
|
|
/* Mark all new records as empty */
|
|
for (i = 0; i < slots * SLOT_SIZE; i++) {
|
|
new_record = ARR_GetElement(records, i);
|
|
new_record->ip_addr.family = IPADDR_UNSPEC;
|
|
}
|
|
|
|
if (!old_records)
|
|
return 1;
|
|
|
|
/* Copy old records to the new hash table */
|
|
for (i = 0; i < ARR_GetSize(old_records); i++) {
|
|
old_record = ARR_GetElement(old_records, i);
|
|
if (old_record->ip_addr.family == IPADDR_UNSPEC)
|
|
continue;
|
|
|
|
new_record = get_record(&old_record->ip_addr);
|
|
|
|
assert(new_record);
|
|
*new_record = *old_record;
|
|
}
|
|
|
|
ARR_DestroyInstance(old_records);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static void
|
|
set_bucket_params(int interval, int burst, uint16_t *max_tokens,
|
|
uint16_t *tokens_per_packet, int *token_shift)
|
|
{
|
|
interval = CLAMP(MIN_LIMIT_INTERVAL, interval, MAX_LIMIT_INTERVAL);
|
|
burst = CLAMP(MIN_LIMIT_BURST, burst, MAX_LIMIT_BURST);
|
|
|
|
if (interval >= -TS_FRAC) {
|
|
/* Find the smallest shift with which the maximum number fits in 16 bits */
|
|
for (*token_shift = 0; *token_shift < interval + TS_FRAC; (*token_shift)++) {
|
|
if (burst << (TS_FRAC + interval - *token_shift) < 1U << 16)
|
|
break;
|
|
}
|
|
} else {
|
|
/* Coarse rate limiting */
|
|
*token_shift = interval + TS_FRAC;
|
|
*tokens_per_packet = 1;
|
|
burst = MAX(1U << -*token_shift, burst);
|
|
}
|
|
|
|
*tokens_per_packet = 1U << (TS_FRAC + interval - *token_shift);
|
|
*max_tokens = *tokens_per_packet * burst;
|
|
|
|
DEBUG_LOG("Tokens max %d packet %d shift %d",
|
|
*max_tokens, *tokens_per_packet, *token_shift);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
void
|
|
CLG_Initialise(void)
|
|
{
|
|
int i, interval, burst, lrate, slots2;
|
|
|
|
for (i = 0; i < MAX_SERVICES; i++) {
|
|
max_tokens[i] = 0;
|
|
tokens_per_hit[i] = 0;
|
|
token_shift[i] = 0;
|
|
leak_rate[i] = 0;
|
|
limit_interval[i] = MIN_LIMIT_INTERVAL;
|
|
|
|
switch (i) {
|
|
case CLG_NTP:
|
|
if (!CNF_GetNTPRateLimit(&interval, &burst, &lrate))
|
|
continue;
|
|
break;
|
|
case CLG_NTSKE:
|
|
if (!CNF_GetNtsRateLimit(&interval, &burst, &lrate))
|
|
continue;
|
|
break;
|
|
case CLG_CMDMON:
|
|
if (!CNF_GetCommandRateLimit(&interval, &burst, &lrate))
|
|
continue;
|
|
break;
|
|
default:
|
|
assert(0);
|
|
}
|
|
|
|
set_bucket_params(interval, burst, &max_tokens[i], &tokens_per_hit[i], &token_shift[i]);
|
|
leak_rate[i] = CLAMP(MIN_LEAK_RATE, lrate, MAX_LEAK_RATE);
|
|
limit_interval[i] = CLAMP(MIN_LIMIT_INTERVAL, interval, MAX_LIMIT_INTERVAL);
|
|
}
|
|
|
|
active = !CNF_GetNoClientLog();
|
|
if (!active) {
|
|
for (i = 0; i < MAX_SERVICES; i++) {
|
|
if (leak_rate[i] != 0)
|
|
LOG_FATAL("Rate limiting cannot be enabled with noclientlog");
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* Calculate the maximum number of slots that can be allocated in the
|
|
configured memory limit. Take into account expanding of the hash
|
|
table where two copies exist at the same time. */
|
|
max_slots = CNF_GetClientLogLimit() /
|
|
((sizeof (Record) + sizeof (NtpTimestamps)) * SLOT_SIZE * 3 / 2);
|
|
max_slots = CLAMP(MIN_SLOTS, max_slots, MAX_SLOTS);
|
|
for (slots2 = 0; 1U << (slots2 + 1) <= max_slots; slots2++)
|
|
;
|
|
|
|
DEBUG_LOG("Max records %u", 1U << (slots2 + SLOT_BITS));
|
|
|
|
slots = 0;
|
|
records = NULL;
|
|
|
|
expand_hashtable();
|
|
|
|
UTI_GetRandomBytes(&ts_offset, sizeof (ts_offset));
|
|
ts_offset %= NSEC_PER_SEC / (1U << TS_FRAC);
|
|
|
|
ntp_ts_map.timestamps = NULL;
|
|
ntp_ts_map.first = 0;
|
|
ntp_ts_map.size = 0;
|
|
ntp_ts_map.max_size = 1U << (slots2 + SLOT_BITS);
|
|
ntp_ts_map.cached_index = 0;
|
|
ntp_ts_map.cached_rx_ts = 0ULL;
|
|
ntp_ts_map.slew_epoch = 0;
|
|
ntp_ts_map.slew_offset = 0.0;
|
|
|
|
LCL_AddParameterChangeHandler(handle_slew, NULL);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
void
|
|
CLG_Finalise(void)
|
|
{
|
|
if (!active)
|
|
return;
|
|
|
|
ARR_DestroyInstance(records);
|
|
if (ntp_ts_map.timestamps)
|
|
ARR_DestroyInstance(ntp_ts_map.timestamps);
|
|
|
|
LCL_RemoveParameterChangeHandler(handle_slew, NULL);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static uint32_t
|
|
get_ts_from_timespec(struct timespec *ts)
|
|
{
|
|
uint32_t sec = ts->tv_sec, nsec = ts->tv_nsec;
|
|
|
|
nsec += ts_offset;
|
|
if (nsec >= NSEC_PER_SEC) {
|
|
nsec -= NSEC_PER_SEC;
|
|
sec++;
|
|
}
|
|
|
|
/* This is fast and accurate enough */
|
|
return sec << TS_FRAC | (140740U * (nsec >> 15)) >> (32 - TS_FRAC);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static void
|
|
update_record(CLG_Service service, Record *record, struct timespec *now)
|
|
{
|
|
uint32_t interval, now_ts, prev_hit, tokens;
|
|
int interval2, tshift, mtokens;
|
|
int8_t *rate;
|
|
|
|
now_ts = get_ts_from_timespec(now);
|
|
|
|
prev_hit = record->last_hit[service];
|
|
record->last_hit[service] = now_ts;
|
|
record->hits[service]++;
|
|
|
|
interval = now_ts - prev_hit;
|
|
|
|
if (prev_hit == INVALID_TS || (int32_t)interval < 0)
|
|
return;
|
|
|
|
tshift = token_shift[service];
|
|
mtokens = max_tokens[service];
|
|
|
|
if (tshift >= 0)
|
|
tokens = (now_ts >> tshift) - (prev_hit >> tshift);
|
|
else if (now_ts - prev_hit > mtokens)
|
|
tokens = mtokens;
|
|
else
|
|
tokens = (now_ts - prev_hit) << -tshift;
|
|
record->tokens[service] = MIN(record->tokens[service] + tokens, mtokens);
|
|
|
|
/* Convert the interval to scaled and rounded log2 */
|
|
if (interval) {
|
|
interval += interval >> 1;
|
|
for (interval2 = -RATE_SCALE * TS_FRAC; interval2 < -MIN_RATE;
|
|
interval2 += RATE_SCALE) {
|
|
if (interval <= 1)
|
|
break;
|
|
interval >>= 1;
|
|
}
|
|
} else {
|
|
interval2 = -RATE_SCALE * (TS_FRAC + 1);
|
|
}
|
|
|
|
/* For the NTP service, update one of the two rates depending on whether
|
|
the previous request of the client had a reply or it timed out */
|
|
rate = service == CLG_NTP && record->drop_flags & (1U << service) ?
|
|
&record->ntp_timeout_rate : &record->rate[service];
|
|
|
|
/* Update the rate in a rough approximation of exponential moving average */
|
|
if (*rate == INVALID_RATE) {
|
|
*rate = -interval2;
|
|
} else {
|
|
if (*rate < -interval2) {
|
|
(*rate)++;
|
|
} else if (*rate > -interval2) {
|
|
if (*rate > RATE_SCALE * 5 / 2 - interval2)
|
|
*rate = RATE_SCALE * 5 / 2 - interval2;
|
|
else
|
|
*rate = (*rate - interval2 - 1) / 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static int
|
|
get_index(Record *record)
|
|
{
|
|
return record - (Record *)ARR_GetElements(records);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
CLG_GetClientIndex(IPAddr *client)
|
|
{
|
|
Record *record;
|
|
|
|
record = get_record(client);
|
|
if (record == NULL)
|
|
return -1;
|
|
|
|
return get_index(record);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static void
|
|
check_service_number(CLG_Service service)
|
|
{
|
|
assert(service >= 0 && service <= MAX_SERVICES);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
CLG_LogServiceAccess(CLG_Service service, IPAddr *client, struct timespec *now)
|
|
{
|
|
Record *record;
|
|
|
|
check_service_number(service);
|
|
|
|
total_hits[service]++;
|
|
|
|
record = get_record(client);
|
|
if (record == NULL)
|
|
return -1;
|
|
|
|
update_record(service, record, now);
|
|
|
|
DEBUG_LOG("service %d hits %"PRIu32" rate %d trate %d tokens %d",
|
|
(int)service, record->hits[service], record->rate[service],
|
|
service == CLG_NTP ? record->ntp_timeout_rate : INVALID_RATE,
|
|
record->tokens[service]);
|
|
|
|
return get_index(record);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static int
|
|
limit_response_random(int leak_rate)
|
|
{
|
|
static uint32_t rnd;
|
|
static int bits_left = 0;
|
|
int r;
|
|
|
|
if (bits_left < leak_rate) {
|
|
UTI_GetRandomBytes(&rnd, sizeof (rnd));
|
|
bits_left = 8 * sizeof (rnd);
|
|
}
|
|
|
|
/* Return zero on average once per 2^leak_rate */
|
|
r = rnd % (1U << leak_rate) ? 1 : 0;
|
|
rnd >>= leak_rate;
|
|
bits_left -= leak_rate;
|
|
|
|
return r;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
CLG_LimitServiceRate(CLG_Service service, int index)
|
|
{
|
|
Record *record;
|
|
int drop;
|
|
|
|
check_service_number(service);
|
|
|
|
if (tokens_per_hit[service] == 0)
|
|
return 0;
|
|
|
|
record = ARR_GetElement(records, index);
|
|
record->drop_flags &= ~(1U << service);
|
|
|
|
if (record->tokens[service] >= tokens_per_hit[service]) {
|
|
record->tokens[service] -= tokens_per_hit[service];
|
|
return 0;
|
|
}
|
|
|
|
drop = limit_response_random(leak_rate[service]);
|
|
|
|
/* Poorly implemented NTP clients can send requests at a higher rate
|
|
when they are not getting replies. If the request rate seems to be more
|
|
than twice as much as when replies are sent, give up on rate limiting to
|
|
reduce the amount of traffic. Invert the sense of the leak to respond to
|
|
most of the requests, but still keep the estimated rate updated. */
|
|
if (service == CLG_NTP && record->ntp_timeout_rate != INVALID_RATE &&
|
|
record->ntp_timeout_rate > record->rate[service] + RATE_SCALE)
|
|
drop = !drop;
|
|
|
|
if (!drop) {
|
|
record->tokens[service] = 0;
|
|
return 0;
|
|
}
|
|
|
|
record->drop_flags |= 1U << service;
|
|
record->drops[service]++;
|
|
total_drops[service]++;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
void
|
|
CLG_UpdateNtpStats(int auth, NTP_Timestamp_Source rx_ts_src, NTP_Timestamp_Source tx_ts_src)
|
|
{
|
|
if (auth)
|
|
total_ntp_auth_hits++;
|
|
if (rx_ts_src >= 0 && rx_ts_src <= MAX_NTP_TS)
|
|
total_ntp_rx_timestamps[rx_ts_src]++;
|
|
if (tx_ts_src >= 0 && tx_ts_src <= MAX_NTP_TS)
|
|
total_ntp_tx_timestamps[tx_ts_src]++;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
CLG_GetNtpMinPoll(void)
|
|
{
|
|
return limit_interval[CLG_NTP];
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static NtpTimestamps *
|
|
get_ntp_tss(uint32_t index)
|
|
{
|
|
return ARR_GetElement(ntp_ts_map.timestamps,
|
|
(ntp_ts_map.first + index) & (ntp_ts_map.max_size - 1));
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static int
|
|
find_ntp_rx_ts(uint64_t rx_ts, uint32_t *index)
|
|
{
|
|
uint64_t rx_x, rx_lo, rx_hi, step;
|
|
uint32_t i, x, lo, hi;
|
|
|
|
if (ntp_ts_map.cached_rx_ts == rx_ts && rx_ts != 0ULL) {
|
|
*index = ntp_ts_map.cached_index;
|
|
return 1;
|
|
}
|
|
|
|
if (ntp_ts_map.size == 0) {
|
|
*index = 0;
|
|
return 0;
|
|
}
|
|
|
|
lo = 0;
|
|
hi = ntp_ts_map.size - 1;
|
|
rx_lo = get_ntp_tss(lo)->rx_ts;
|
|
rx_hi = get_ntp_tss(hi)->rx_ts;
|
|
|
|
/* Check for ts < lo before ts > hi to trim timestamps from "future" later
|
|
if both conditions are true to not break the order of the endpoints.
|
|
Compare timestamps by their difference to allow adjacent NTP eras. */
|
|
if ((int64_t)(rx_ts - rx_lo) < 0) {
|
|
*index = 0;
|
|
return 0;
|
|
} else if ((int64_t)(rx_ts - rx_hi) > 0) {
|
|
*index = ntp_ts_map.size;
|
|
return 0;
|
|
}
|
|
|
|
/* Perform a combined linear interpolation and binary search */
|
|
|
|
for (i = 0; ; i++) {
|
|
if (rx_ts == rx_hi) {
|
|
*index = ntp_ts_map.cached_index = hi;
|
|
ntp_ts_map.cached_rx_ts = rx_ts;
|
|
return 1;
|
|
} else if (rx_ts == rx_lo) {
|
|
*index = ntp_ts_map.cached_index = lo;
|
|
ntp_ts_map.cached_rx_ts = rx_ts;
|
|
return 1;
|
|
} else if (lo + 1 == hi) {
|
|
*index = hi;
|
|
return 0;
|
|
}
|
|
|
|
if (hi - lo > 3 && i % 2 == 0) {
|
|
step = (rx_hi - rx_lo) / (hi - lo);
|
|
if (step == 0)
|
|
step = 1;
|
|
x = lo + (rx_ts - rx_lo) / step;
|
|
} else {
|
|
x = lo + (hi - lo) / 2;
|
|
}
|
|
|
|
if (x <= lo)
|
|
x = lo + 1;
|
|
else if (x >= hi)
|
|
x = hi - 1;
|
|
|
|
rx_x = get_ntp_tss(x)->rx_ts;
|
|
|
|
if ((int64_t)(rx_x - rx_ts) <= 0) {
|
|
lo = x;
|
|
rx_lo = rx_x;
|
|
} else {
|
|
hi = x;
|
|
rx_hi = rx_x;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static uint64_t
|
|
ntp64_to_int64(NTP_int64 *ts)
|
|
{
|
|
return (uint64_t)ntohl(ts->hi) << 32 | ntohl(ts->lo);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static void
|
|
int64_to_ntp64(uint64_t ts, NTP_int64 *ntp_ts)
|
|
{
|
|
ntp_ts->hi = htonl(ts >> 32);
|
|
ntp_ts->lo = htonl(ts);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static uint32_t
|
|
push_ntp_tss(uint32_t index)
|
|
{
|
|
if (ntp_ts_map.size < ntp_ts_map.max_size) {
|
|
ntp_ts_map.size++;
|
|
} else {
|
|
ntp_ts_map.first = (ntp_ts_map.first + 1) % (ntp_ts_map.max_size);
|
|
if (index > 0)
|
|
index--;
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static void
|
|
set_ntp_tx(NtpTimestamps *tss, NTP_int64 *rx_ts, struct timespec *tx_ts,
|
|
NTP_Timestamp_Source tx_src)
|
|
{
|
|
struct timespec ts;
|
|
|
|
if (!tx_ts) {
|
|
tss->flags &= ~NTPTS_VALID_TX;
|
|
return;
|
|
}
|
|
|
|
UTI_Ntp64ToTimespec(rx_ts, &ts);
|
|
UTI_DiffTimespecs(&ts, tx_ts, &ts);
|
|
|
|
if (ts.tv_sec < -2 || ts.tv_sec > 1) {
|
|
tss->flags &= ~NTPTS_VALID_TX;
|
|
return;
|
|
}
|
|
|
|
tss->tx_ts_offset = (int32_t)ts.tv_nsec + (int32_t)ts.tv_sec * (int32_t)NSEC_PER_SEC;
|
|
tss->flags |= NTPTS_VALID_TX;
|
|
tss->tx_ts_source = tx_src;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static void
|
|
get_ntp_tx(NtpTimestamps *tss, struct timespec *tx_ts, NTP_Timestamp_Source *tx_src)
|
|
{
|
|
int32_t offset = tss->tx_ts_offset;
|
|
NTP_int64 ntp_ts;
|
|
|
|
if (tss->flags & NTPTS_VALID_TX) {
|
|
int64_to_ntp64(tss->rx_ts, &ntp_ts);
|
|
UTI_Ntp64ToTimespec(&ntp_ts, tx_ts);
|
|
if (offset >= (int32_t)NSEC_PER_SEC) {
|
|
offset -= NSEC_PER_SEC;
|
|
tx_ts->tv_sec++;
|
|
}
|
|
tx_ts->tv_nsec += offset;
|
|
UTI_NormaliseTimespec(tx_ts);
|
|
} else {
|
|
UTI_ZeroTimespec(tx_ts);
|
|
}
|
|
|
|
*tx_src = tss->tx_ts_source;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
void
|
|
CLG_SaveNtpTimestamps(NTP_int64 *rx_ts, struct timespec *tx_ts, NTP_Timestamp_Source tx_src)
|
|
{
|
|
NtpTimestamps *tss;
|
|
uint32_t i, index;
|
|
uint64_t rx;
|
|
|
|
if (!active)
|
|
return;
|
|
|
|
/* Allocate the array on first use */
|
|
if (!ntp_ts_map.timestamps) {
|
|
ntp_ts_map.timestamps = ARR_CreateInstance(sizeof (NtpTimestamps));
|
|
ARR_SetSize(ntp_ts_map.timestamps, ntp_ts_map.max_size);
|
|
}
|
|
|
|
rx = ntp64_to_int64(rx_ts);
|
|
|
|
if (rx == 0ULL)
|
|
return;
|
|
|
|
/* Disable the RX timestamp if it already exists to avoid responding
|
|
with a wrong TX timestamp */
|
|
if (find_ntp_rx_ts(rx, &index)) {
|
|
get_ntp_tss(index)->flags |= NTPTS_DISABLED;
|
|
return;
|
|
}
|
|
|
|
assert(index <= ntp_ts_map.size);
|
|
|
|
if (index == ntp_ts_map.size) {
|
|
/* Increase the size or drop the oldest timestamp to make room for
|
|
the new timestamp */
|
|
index = push_ntp_tss(index);
|
|
} else {
|
|
/* Trim timestamps in distant future after backward step */
|
|
while (index < ntp_ts_map.size &&
|
|
get_ntp_tss(ntp_ts_map.size - 1)->rx_ts - rx > NTPTS_FUTURE_LIMIT)
|
|
ntp_ts_map.size--;
|
|
|
|
/* Insert the timestamp if it is close to the latest timestamp.
|
|
Otherwise, replace the closest older or the oldest timestamp. */
|
|
if (index + NTPTS_INSERT_LIMIT >= ntp_ts_map.size) {
|
|
index = push_ntp_tss(index);
|
|
for (i = ntp_ts_map.size - 1; i > index; i--)
|
|
*get_ntp_tss(i) = *get_ntp_tss(i - 1);
|
|
} else {
|
|
if (index > 0)
|
|
index--;
|
|
}
|
|
}
|
|
|
|
ntp_ts_map.cached_index = index;
|
|
ntp_ts_map.cached_rx_ts = rx;
|
|
|
|
tss = get_ntp_tss(index);
|
|
tss->rx_ts = rx;
|
|
tss->flags = 0;
|
|
tss->slew_epoch = ntp_ts_map.slew_epoch;
|
|
set_ntp_tx(tss, rx_ts, tx_ts, tx_src);
|
|
|
|
DEBUG_LOG("Saved RX+TX index=%"PRIu32" first=%"PRIu32" size=%"PRIu32,
|
|
index, ntp_ts_map.first, ntp_ts_map.size);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static void
|
|
handle_slew(struct timespec *raw, struct timespec *cooked, double dfreq,
|
|
double doffset, LCL_ChangeType change_type, void *anything)
|
|
{
|
|
/* Drop all timestamps on unknown step */
|
|
if (change_type == LCL_ChangeUnknownStep) {
|
|
ntp_ts_map.size = 0;
|
|
ntp_ts_map.cached_rx_ts = 0ULL;
|
|
}
|
|
|
|
ntp_ts_map.slew_epoch++;
|
|
ntp_ts_map.slew_offset = doffset;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
void
|
|
CLG_UndoNtpTxTimestampSlew(NTP_int64 *rx_ts, struct timespec *tx_ts)
|
|
{
|
|
uint32_t index;
|
|
|
|
if (!ntp_ts_map.timestamps)
|
|
return;
|
|
|
|
if (!find_ntp_rx_ts(ntp64_to_int64(rx_ts), &index))
|
|
return;
|
|
|
|
/* If the RX timestamp was captured before the last correction of the clock,
|
|
remove the adjustment from the TX timestamp */
|
|
if ((uint16_t)(get_ntp_tss(index)->slew_epoch + 1U) == ntp_ts_map.slew_epoch)
|
|
UTI_AddDoubleToTimespec(tx_ts, ntp_ts_map.slew_offset, tx_ts);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
void
|
|
CLG_UpdateNtpTxTimestamp(NTP_int64 *rx_ts, struct timespec *tx_ts,
|
|
NTP_Timestamp_Source tx_src)
|
|
{
|
|
uint32_t index;
|
|
|
|
if (!ntp_ts_map.timestamps)
|
|
return;
|
|
|
|
if (!find_ntp_rx_ts(ntp64_to_int64(rx_ts), &index))
|
|
return;
|
|
|
|
set_ntp_tx(get_ntp_tss(index), rx_ts, tx_ts, tx_src);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
CLG_GetNtpTxTimestamp(NTP_int64 *rx_ts, struct timespec *tx_ts,
|
|
NTP_Timestamp_Source *tx_src)
|
|
{
|
|
NtpTimestamps *tss;
|
|
uint32_t index;
|
|
|
|
if (!ntp_ts_map.timestamps)
|
|
return 0;
|
|
|
|
if (!find_ntp_rx_ts(ntp64_to_int64(rx_ts), &index))
|
|
return 0;
|
|
|
|
tss = get_ntp_tss(index);
|
|
|
|
if (tss->flags & NTPTS_DISABLED)
|
|
return 0;
|
|
|
|
get_ntp_tx(tss, tx_ts, tx_src);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
void
|
|
CLG_DisableNtpTimestamps(NTP_int64 *rx_ts)
|
|
{
|
|
uint32_t index;
|
|
|
|
if (!ntp_ts_map.timestamps)
|
|
return;
|
|
|
|
if (find_ntp_rx_ts(ntp64_to_int64(rx_ts), &index))
|
|
get_ntp_tss(index)->flags |= NTPTS_DISABLED;
|
|
|
|
/* This assumes the function is called only to prevent multiple
|
|
interleaved responses to the same timestamp */
|
|
total_ntp_interleaved_hits++;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
CLG_GetNumberOfIndices(void)
|
|
{
|
|
if (!active)
|
|
return -1;
|
|
|
|
return ARR_GetSize(records);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static int get_interval(int rate)
|
|
{
|
|
if (rate == INVALID_RATE)
|
|
return 127;
|
|
|
|
rate += rate > 0 ? RATE_SCALE / 2 : -RATE_SCALE / 2;
|
|
|
|
return rate / -RATE_SCALE;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static uint32_t get_last_ago(uint32_t x, uint32_t y)
|
|
{
|
|
if (y == INVALID_TS || (int32_t)(x - y) < 0)
|
|
return -1;
|
|
|
|
return (x - y) >> TS_FRAC;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
CLG_GetClientAccessReportByIndex(int index, int reset, uint32_t min_hits,
|
|
RPT_ClientAccessByIndex_Report *report, struct timespec *now)
|
|
{
|
|
Record *record;
|
|
uint32_t now_ts;
|
|
int i, r;
|
|
|
|
if (!active || index < 0 || index >= ARR_GetSize(records))
|
|
return 0;
|
|
|
|
record = ARR_GetElement(records, index);
|
|
|
|
if (record->ip_addr.family == IPADDR_UNSPEC)
|
|
return 0;
|
|
|
|
if (min_hits == 0) {
|
|
r = 1;
|
|
} else {
|
|
for (i = r = 0; i < MAX_SERVICES; i++) {
|
|
if (record->hits[i] >= min_hits) {
|
|
r = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (r) {
|
|
now_ts = get_ts_from_timespec(now);
|
|
|
|
report->ip_addr = record->ip_addr;
|
|
report->ntp_hits = record->hits[CLG_NTP];
|
|
report->nke_hits = record->hits[CLG_NTSKE];
|
|
report->cmd_hits = record->hits[CLG_CMDMON];
|
|
report->ntp_drops = record->drops[CLG_NTP];
|
|
report->nke_drops = record->drops[CLG_NTSKE];
|
|
report->cmd_drops = record->drops[CLG_CMDMON];
|
|
report->ntp_interval = get_interval(record->rate[CLG_NTP]);
|
|
report->nke_interval = get_interval(record->rate[CLG_NTSKE]);
|
|
report->cmd_interval = get_interval(record->rate[CLG_CMDMON]);
|
|
report->ntp_timeout_interval = get_interval(record->ntp_timeout_rate);
|
|
report->last_ntp_hit_ago = get_last_ago(now_ts, record->last_hit[CLG_NTP]);
|
|
report->last_nke_hit_ago = get_last_ago(now_ts, record->last_hit[CLG_NTSKE]);
|
|
report->last_cmd_hit_ago = get_last_ago(now_ts, record->last_hit[CLG_CMDMON]);
|
|
}
|
|
|
|
if (reset) {
|
|
for (i = 0; i < MAX_SERVICES; i++) {
|
|
record->hits[i] = 0;
|
|
record->drops[i] = 0;
|
|
}
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
void
|
|
CLG_GetServerStatsReport(RPT_ServerStatsReport *report)
|
|
{
|
|
report->ntp_hits = total_hits[CLG_NTP];
|
|
report->nke_hits = total_hits[CLG_NTSKE];
|
|
report->cmd_hits = total_hits[CLG_CMDMON];
|
|
report->ntp_drops = total_drops[CLG_NTP];
|
|
report->nke_drops = total_drops[CLG_NTSKE];
|
|
report->cmd_drops = total_drops[CLG_CMDMON];
|
|
report->log_drops = total_record_drops;
|
|
report->ntp_auth_hits = total_ntp_auth_hits;
|
|
report->ntp_interleaved_hits = total_ntp_interleaved_hits;
|
|
report->ntp_timestamps = ntp_ts_map.size;
|
|
report->ntp_span_seconds = ntp_ts_map.size > 1 ?
|
|
(get_ntp_tss(ntp_ts_map.size - 1)->rx_ts -
|
|
get_ntp_tss(0)->rx_ts) >> 32 : 0;
|
|
report->ntp_daemon_rx_timestamps = total_ntp_rx_timestamps[NTP_TS_DAEMON];
|
|
report->ntp_daemon_tx_timestamps = total_ntp_tx_timestamps[NTP_TS_DAEMON];
|
|
report->ntp_kernel_rx_timestamps = total_ntp_rx_timestamps[NTP_TS_KERNEL];
|
|
report->ntp_kernel_tx_timestamps = total_ntp_tx_timestamps[NTP_TS_KERNEL];
|
|
report->ntp_hw_rx_timestamps = total_ntp_rx_timestamps[NTP_TS_HARDWARE];
|
|
report->ntp_hw_tx_timestamps = total_ntp_tx_timestamps[NTP_TS_HARDWARE];
|
|
}
|