diff --git a/clientlog.c b/clientlog.c index 5f72fcb..6f66738 100644 --- a/clientlog.c +++ b/clientlog.c @@ -47,8 +47,15 @@ typedef struct { IPAddr ip_addr; uint32_t ntp_hits; uint32_t cmd_hits; + uint16_t ntp_drops; + uint16_t cmd_drops; int8_t ntp_rate; int8_t cmd_rate; + int8_t ntp_timeout_rate; + uint8_t ntp_burst; + uint8_t cmd_burst; + uint8_t flags; + uint16_t _pad; time_t last_ntp_hit; time_t last_cmd_hit; } Record; @@ -78,6 +85,36 @@ static unsigned int max_slots; #define MIN_RATE (-14 * RATE_SCALE) #define INVALID_RATE -128 +/* Thresholds for request rate to activate response rate limiting */ + +#define MIN_THRESHOLD (-10 * RATE_SCALE) +#define MAX_THRESHOLD (0 * RATE_SCALE) + +static int ntp_threshold; +static int cmd_threshold; + +/* Numbers of responses after the rate exceeded the threshold before + actually dropping requests */ + +#define MIN_LEAK_BURST 0 +#define MAX_LEAK_BURST 255 + +static int ntp_leak_burst; +static int cmd_leak_burst; + +/* Rates at which responses are randomly allowed (in log2). This is + necessary to prevent an attacker sending requests with spoofed + source address from blocking responses to the client completely. */ + +#define MIN_LEAK_RATE 1 +#define MAX_LEAK_RATE 4 + +static int ntp_leak_rate; +static int cmd_leak_rate; + +/* Flag indicating whether the last response was dropped */ +#define FLAG_NTP_DROPPED 0x1 + /* Flag indicating whether facility is turned on or not */ static int active; @@ -137,7 +174,11 @@ get_record(IPAddr *ip) record->ip_addr = *ip; record->ntp_hits = record->cmd_hits = 0; + record->ntp_drops = record->cmd_drops = 0; record->ntp_rate = record->cmd_rate = INVALID_RATE; + record->ntp_timeout_rate = INVALID_RATE; + record->ntp_burst = record->cmd_burst = 0; + record->flags = 0; record->last_ntp_hit = record->last_cmd_hit = 0; return record; @@ -236,6 +277,8 @@ update_rate(int rate, time_t now, time_t last_hit) void CLG_Initialise(void) { + int threshold, burst, leak_rate; + active = !CNF_GetNoClientLog(); if (!active) return; @@ -250,6 +293,20 @@ CLG_Initialise(void) records = NULL; expand_hashtable(); + + if (CNF_GetNTPRateLimit(&threshold, &burst, &leak_rate)) + ntp_threshold = CLAMP(MIN_THRESHOLD, threshold * -RATE_SCALE, MAX_THRESHOLD); + else + ntp_threshold = INVALID_RATE; + ntp_leak_burst = CLAMP(MIN_LEAK_BURST, burst, MAX_LEAK_BURST); + ntp_leak_rate = CLAMP(MIN_LEAK_RATE, leak_rate, MAX_LEAK_RATE); + + if (CNF_GetCommandRateLimit(&threshold, &burst, &leak_rate)) + cmd_threshold = CLAMP(MIN_THRESHOLD, threshold * -RATE_SCALE, MAX_THRESHOLD); + else + cmd_threshold = INVALID_RATE; + cmd_leak_burst = CLAMP(MIN_LEAK_BURST, burst, MAX_LEAK_BURST); + cmd_leak_rate = CLAMP(MIN_LEAK_RATE, leak_rate, MAX_LEAK_RATE); } /* ================================================== */ @@ -265,46 +322,161 @@ CLG_Finalise(void) /* ================================================== */ -void +static int +get_index(Record *record) +{ + return record - (Record *)ARR_GetElements(records); +} + +/* ================================================== */ + +int CLG_LogNTPAccess(IPAddr *client, time_t now) { Record *record; if (!active) - return; + return -1; record = get_record(client); if (record == NULL) - return; + return -1; record->ntp_hits++; - record->ntp_rate = update_rate(record->ntp_rate, now, record->last_ntp_hit); + + /* Update one of the two rates depending on whether the previous request + of the client had a reply or it timed out */ + if (record->flags & FLAG_NTP_DROPPED) + record->ntp_timeout_rate = update_rate(record->ntp_timeout_rate, + now, record->last_ntp_hit); + else + record->ntp_rate = update_rate(record->ntp_rate, now, record->last_ntp_hit); + record->last_ntp_hit = now; - DEBUG_LOG(LOGF_ClientLog, "NTP hits %"PRIu32" rate %d", - record->ntp_hits, record->ntp_rate); + DEBUG_LOG(LOGF_ClientLog, "NTP hits %"PRIu32" rate %d trate %d burst %d", + record->ntp_hits, record->ntp_rate, record->ntp_timeout_rate, + record->ntp_burst); + + return get_index(record); } /* ================================================== */ -void +int CLG_LogCommandAccess(IPAddr *client, time_t now) { Record *record; if (!active) - return; + return -1; record = get_record(client); if (record == NULL) - return; + return -1; record->cmd_hits++; record->cmd_rate = update_rate(record->cmd_rate, now, record->last_cmd_hit); record->last_cmd_hit = now; - DEBUG_LOG(LOGF_ClientLog, "Cmd hits %"PRIu32" rate %d", - record->cmd_hits, record->cmd_rate); + DEBUG_LOG(LOGF_ClientLog, "Cmd hits %"PRIu32" rate %d burst %d", + record->cmd_hits, record->cmd_rate, record->cmd_burst); + + 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_LimitNTPResponseRate(int index) +{ + Record *record; + int drop; + + record = ARR_GetElement(records, index); + record->flags &= ~FLAG_NTP_DROPPED; + + /* Respond to all requests if the rate doesn't exceed the threshold */ + if (ntp_threshold == INVALID_RATE || + record->ntp_rate == INVALID_RATE || + record->ntp_rate <= ntp_threshold) { + record->ntp_burst = 0; + return 0; + } + + /* Allow the client to send a burst of requests */ + if (record->ntp_burst < ntp_leak_burst) { + record->ntp_burst++; + return 0; + } + + drop = limit_response_random(ntp_leak_rate); + + /* Poorly implemented clients may send new requests at even 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 (record->ntp_timeout_rate != INVALID_RATE && + record->ntp_timeout_rate > record->ntp_rate + RATE_SCALE) + drop = !drop; + + if (!drop) + return 0; + + record->flags |= FLAG_NTP_DROPPED; + record->ntp_drops++; + + return 1; +} + +/* ================================================== */ + +int +CLG_LimitCommandResponseRate(int index) +{ + Record *record; + + record = ARR_GetElement(records, index); + + if (cmd_threshold == INVALID_RATE || + record->cmd_rate == INVALID_RATE || + record->cmd_rate <= cmd_threshold) { + record->cmd_burst = 0; + return 0; + } + + if (record->cmd_burst < cmd_leak_burst) { + record->cmd_burst++; + return 0; + } + + if (!limit_response_random(cmd_leak_rate)) + return 0; + + return 1; } /* ================================================== */ diff --git a/clientlog.h b/clientlog.h index 573bccd..a4a429f 100644 --- a/clientlog.h +++ b/clientlog.h @@ -33,8 +33,10 @@ extern void CLG_Initialise(void); extern void CLG_Finalise(void); -extern void CLG_LogNTPAccess(IPAddr *client, time_t now); -extern void CLG_LogCommandAccess(IPAddr *client, time_t now); +extern int CLG_LogNTPAccess(IPAddr *client, time_t now); +extern int CLG_LogCommandAccess(IPAddr *client, time_t now); +extern int CLG_LimitNTPResponseRate(int index); +extern int CLG_LimitCommandResponseRate(int index); /* And some reporting functions, for use by chronyc. */ /* TBD */ diff --git a/cmdmon.c b/cmdmon.c index 059fdea..b71b657 100644 --- a/cmdmon.c +++ b/cmdmon.c @@ -1158,7 +1158,7 @@ read_from_cmd_socket(void *anything) CMD_Request rx_message; CMD_Reply tx_message; int status, read_length, expected_length, rx_message_length; - int localhost, allowed, sock_fd; + int localhost, allowed, sock_fd, log_index; union sockaddr_all where_from; socklen_t from_length; IPAddr remote_ip; @@ -1290,7 +1290,14 @@ read_from_cmd_socket(void *anything) /* OK, we have a valid message. Now dispatch on message type and process it. */ - CLG_LogCommandAccess(&remote_ip, cooked_now.tv_sec); + log_index = CLG_LogCommandAccess(&remote_ip, cooked_now.tv_sec); + + /* Don't reply to all requests from hosts other than localhost if the rate + is excessive */ + if (!localhost && log_index >= 0 && CLG_LimitCommandResponseRate(log_index)) { + DEBUG_LOG(LOGF_CmdMon, "Command packet discarded to limit response rate"); + return; + } if (rx_command >= N_REQUEST_TYPES) { /* This should be already handled */ diff --git a/conf.c b/conf.c index 3fbdbc0..d5e6373 100644 --- a/conf.c +++ b/conf.c @@ -70,6 +70,8 @@ static void parse_makestep(char *); static void parse_maxchange(char *); static void parse_peer(char *); static void parse_pool(char *); +static void parse_ratelimit(char *line, int *enabled, int *interval, + int *burst, int *leak); static void parse_refclock(char *); static void parse_server(char *); static void parse_smoothtime(char *); @@ -187,6 +189,16 @@ static char *bind_cmd_path; * chronyds being started. */ static char *pidfile; +/* Rate limiting parameters */ +static int ntp_ratelimit_enabled = 0; +static int ntp_ratelimit_interval = 3; +static int ntp_ratelimit_burst = 7; +static int ntp_ratelimit_leak = 3; +static int cmd_ratelimit_enabled = 0; +static int cmd_ratelimit_interval = 1; +static int cmd_ratelimit_burst = 50; +static int cmd_ratelimit_leak = 1; + /* Smoothing constants */ static double smooth_max_freq = 0.0; /* in ppm */ static double smooth_max_wander = 0.0; /* in ppm/s */ @@ -431,6 +443,9 @@ CNF_ParseLine(const char *filename, int number, char *line) parse_cmddeny(p); } else if (!strcasecmp(command, "cmdport")) { parse_int(p, &cmd_port); + } else if (!strcasecmp(command, "cmdratelimit")) { + parse_ratelimit(p, &cmd_ratelimit_enabled, &cmd_ratelimit_interval, + &cmd_ratelimit_burst, &cmd_ratelimit_leak); } else if (!strcasecmp(command, "combinelimit")) { parse_double(p, &combine_limit); } else if (!strcasecmp(command, "corrtimeratio")) { @@ -501,6 +516,9 @@ CNF_ParseLine(const char *filename, int number, char *line) parse_pool(p); } else if (!strcasecmp(command, "port")) { parse_int(p, &ntp_port); + } else if (!strcasecmp(command, "ratelimit")) { + parse_ratelimit(p, &ntp_ratelimit_enabled, &ntp_ratelimit_interval, + &ntp_ratelimit_burst, &ntp_ratelimit_leak); } else if (!strcasecmp(command, "refclock")) { parse_refclock(p); } else if (!strcasecmp(command, "reselectdist")) { @@ -632,6 +650,35 @@ parse_pool(char *line) /* ================================================== */ +static void +parse_ratelimit(char *line, int *enabled, int *interval, int *burst, int *leak) +{ + int n, val; + char *opt; + + *enabled = 1; + + while (*line) { + opt = line; + line = CPS_SplitWord(line); + if (sscanf(line, "%d%n", &val, &n) != 1) { + command_parse_error(); + return; + } + line += n; + if (!strcasecmp(opt, "interval")) + *interval = val; + else if (!strcasecmp(opt, "burst")) + *burst = val; + else if (!strcasecmp(opt, "leak")) + *leak = val; + else + command_parse_error(); + } +} + +/* ================================================== */ + static void parse_refclock(char *line) { @@ -1785,6 +1832,26 @@ CNF_GetLockMemory(void) /* ================================================== */ +int CNF_GetNTPRateLimit(int *interval, int *burst, int *leak) +{ + *interval = ntp_ratelimit_interval; + *burst = ntp_ratelimit_burst; + *leak = ntp_ratelimit_leak; + return ntp_ratelimit_enabled; +} + +/* ================================================== */ + +int CNF_GetCommandRateLimit(int *interval, int *burst, int *leak) +{ + *interval = cmd_ratelimit_interval; + *burst = cmd_ratelimit_burst; + *leak = cmd_ratelimit_leak; + return cmd_ratelimit_enabled; +} + +/* ================================================== */ + void CNF_GetSmooth(double *max_freq, double *max_wander, int *leap_only) { diff --git a/conf.h b/conf.h index 75f2764..fd79c60 100644 --- a/conf.h +++ b/conf.h @@ -98,6 +98,8 @@ extern void CNF_SetupAccessRestrictions(void); extern int CNF_GetSchedPriority(void); extern int CNF_GetLockMemory(void); +extern int CNF_GetNTPRateLimit(int *interval, int *burst, int *leak); +extern int CNF_GetCommandRateLimit(int *interval, int *burst, int *leak); extern void CNF_GetSmooth(double *max_freq, double *max_wander, int *leap_only); extern void CNF_GetTempComp(char **file, double *interval, char **point_file, double *T0, double *k0, double *k1, double *k2); diff --git a/ntp_core.c b/ntp_core.c index 96a50f0..e867899 100644 --- a/ntp_core.c +++ b/ntp_core.c @@ -1649,7 +1649,7 @@ NCR_ProcessUnknown ) { NTP_Mode pkt_mode, my_mode; - int has_auth, valid_auth; + int has_auth, valid_auth, log_index; uint32_t key_id; /* Ignore the packet if it wasn't received by server socket */ @@ -1686,7 +1686,13 @@ NCR_ProcessUnknown return; } - CLG_LogNTPAccess(&remote_addr->ip_addr, now->tv_sec); + log_index = CLG_LogNTPAccess(&remote_addr->ip_addr, now->tv_sec); + + /* Don't reply to all requests if the rate is excessive */ + if (log_index >= 0 && CLG_LimitNTPResponseRate(log_index)) { + DEBUG_LOG(LOGF_NtpCore, "NTP packet discarded to limit response rate"); + return; + } /* Check if the packet includes MAC that authenticates properly */ valid_auth = check_packet_auth(message, length, &has_auth, &key_id);