Skip to content

Commit 5a6ddf9

Browse files
committed
upstream: add infrastructure for ratelimited logging; feedback/ok
dtucker OpenBSD-Commit-ID: 18a83e5ac09d59aaf1e834fd6b796db89dd842e7
1 parent 85f0c1e commit 5a6ddf9

File tree

2 files changed

+193
-3
lines changed

2 files changed

+193
-3
lines changed

log.c

+168-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* $OpenBSD: log.c,v 1.62 2024/06/27 22:36:44 djm Exp $ */
1+
/* $OpenBSD: log.c,v 1.64 2024/12/07 10:05:36 djm Exp $ */
22
/*
33
* Author: Tatu Ylonen <[email protected]>
44
* Copyright (c) 1995 Tatu Ylonen <[email protected]>, Espoo, Finland
@@ -38,14 +38,16 @@
3838

3939
#include <sys/types.h>
4040

41+
#include <errno.h>
4142
#include <fcntl.h>
43+
#include <limits.h>
4244
#include <stdarg.h>
4345
#include <stdio.h>
4446
#include <stdlib.h>
4547
#include <string.h>
4648
#include <syslog.h>
49+
#include <time.h>
4750
#include <unistd.h>
48-
#include <errno.h>
4951
#if defined(HAVE_STRNVIS) && defined(HAVE_VIS_H) && !defined(BROKEN_STRNVIS)
5052
# include <vis.h>
5153
#endif
@@ -489,3 +491,167 @@ sshlogdirect(LogLevel level, int forced, const char *fmt, ...)
489491
do_log(level, forced, NULL, fmt, args);
490492
va_end(args);
491493
}
494+
495+
496+
/*
497+
* A simple system for ratelimiting aperiodic events such as logs, without
498+
* needing to be hooked into a mainloop/timer. A running total of events is
499+
* maintained and when it exceeds a threshold further events are dropped
500+
* until the rate falls back below that threshold.
501+
*
502+
* To prevent flipping in and out of rate-limiting, there is a hysteresis
503+
* timer that delays leaving the rate-limited state.
504+
*
505+
* While in the rate-limited state, events can be periodically allowed though
506+
* and the number of dropped events since the last log obtained.
507+
*
508+
* XXX a moving average rate of events might be a better approach here rather
509+
* than linear decay, which can suppress events for a while after large
510+
* bursts.
511+
*/
512+
513+
/* #define RATELIMIT_DEBUG 1 */
514+
515+
#ifdef RATELIMIT_DEBUG
516+
# define RLDBG(x) do { \
517+
printf("%s:%d %s: ", __FILE__, __LINE__, __func__); \
518+
printf x; \
519+
printf("\n"); \
520+
fflush(stdout); \
521+
} while (0)
522+
#else
523+
# define RLDBG(x)
524+
#endif
525+
526+
/* set up a ratelimit */
527+
void
528+
log_ratelimit_init(struct log_ratelimit_ctx *rl, u_int threshold,
529+
u_int max_accum, u_int hysteresis, u_int log_every)
530+
{
531+
memset(rl, 0, sizeof(*rl));
532+
rl->threshold = threshold;
533+
rl->max_accum = max_accum;
534+
rl->hysteresis = hysteresis;
535+
rl->log_every = log_every;
536+
RLDBG(("called: rl=%p thresh=%u max=%u hys=%u log_every=%u",
537+
rl, rl->threshold, rl->max_accum, rl->hysteresis, rl->log_every));
538+
}
539+
540+
/*
541+
* check whether a log event should be dropped because of rate-limiting.
542+
* returns non-zero if the event should be dropped. If events_since_last
543+
* is supplied then, for periodic logs, it will be set to the number of
544+
* dropped events since the last message.
545+
*/
546+
int
547+
log_ratelimit(struct log_ratelimit_ctx *rl, time_t now, int *active,
548+
u_int *events_dropped)
549+
{
550+
time_t olast_event;
551+
552+
RLDBG(("called: rl=%p thresh=%u max=%u hys=%u log_every=%u "
553+
"accum=%u since=%ld since_last=%u", rl, rl->threshold,
554+
rl->max_accum, rl->hysteresis,
555+
rl->log_every, rl->accumulated_events,
556+
rl->last_event == 0 ? -1 : (long)(now - rl->last_event),
557+
rl->ratelimited_events));
558+
559+
if (now < 0)
560+
return 0;
561+
if (events_dropped != NULL)
562+
*events_dropped = 0;
563+
if (active != NULL)
564+
*active = rl->ratelimit_active;
565+
566+
/* First, decay accumulated events */
567+
if (rl->last_event <= 0)
568+
rl->last_event = now;
569+
if (now > rl->last_event) {
570+
uint64_t n = now - rl->last_event;
571+
572+
if (n > UINT_MAX)
573+
n = UINT_MAX;
574+
if (rl->accumulated_events < (u_int)n)
575+
rl->accumulated_events = 0;
576+
else
577+
rl->accumulated_events -= (u_int)n;
578+
RLDBG(("decay: accum=%u", rl->accumulated_events));
579+
}
580+
rl->accumulated_events++; /* add this event */
581+
if (rl->accumulated_events > rl->max_accum)
582+
rl->accumulated_events = rl->max_accum;
583+
olast_event = rl->last_event;
584+
rl->last_event = now;
585+
RLDBG(("check threshold: accum=%u vs thresh=%u",
586+
rl->accumulated_events, rl->threshold));
587+
588+
/* Are we under threshold? */
589+
if (rl->accumulated_events < rl->threshold) {
590+
if (!rl->ratelimit_active)
591+
return 0;
592+
RLDBG(("under threshold: hys=%u since_hys=%ld since_last=%ld",
593+
rl->hysteresis, rl->hysteresis_start == 0 ? -1 :
594+
(long)(now - rl->hysteresis_start),
595+
olast_event == 0 ? -1 : (long)(now - olast_event)));
596+
if (rl->hysteresis_start == 0) {
597+
/* active, but under threshold; hysteresis */
598+
if (olast_event + rl->hysteresis < now) {
599+
/* hysteresis expired before this event */
600+
RLDBG(("hysteresis preexpired"));
601+
goto inactive;
602+
}
603+
RLDBG(("start hysteresis"));
604+
rl->hysteresis_start = now;
605+
} else if (rl->hysteresis_start + rl->hysteresis < now) {
606+
/* Hysteresis period expired, transition to inactive */
607+
RLDBG(("complete hysteresis"));
608+
inactive:
609+
if (events_dropped != NULL)
610+
*events_dropped = rl->ratelimited_events;
611+
if (active != NULL)
612+
*active = 0;
613+
rl->ratelimit_active = 0;
614+
rl->ratelimit_start = 0;
615+
rl->last_log = 0;
616+
rl->hysteresis_start = 0;
617+
rl->ratelimited_events = 0;
618+
return 0;
619+
}
620+
/* ratelimiting active, but in hysteresis period */
621+
} else if (!rl->ratelimit_active) {
622+
/* Transition to rate-limiting */
623+
RLDBG(("start ratelimit"));
624+
rl->ratelimit_active = 1;
625+
rl->ratelimit_start = now;
626+
rl->last_log = now;
627+
rl->hysteresis_start = 0;
628+
rl->ratelimited_events = 1;
629+
if (active != NULL)
630+
*active = 1;
631+
return 1;
632+
} else if (rl->hysteresis_start != 0) {
633+
/* active and over threshold; reset hysteresis timer */
634+
RLDBG(("clear hysteresis"));
635+
rl->hysteresis_start = 0;
636+
}
637+
638+
/* over threshold or in hysteresis period; log periodically */
639+
if (active != NULL)
640+
*active = 1;
641+
RLDBG(("log_every=%u since_log=%ld", rl->log_every,
642+
(long)(now - rl->last_log)));
643+
if (rl->log_every > 0 && now >= rl->last_log + rl->log_every) {
644+
RLDBG(("periodic: since_last=%u", rl->ratelimited_events));
645+
rl->last_log = now;
646+
if (events_dropped != NULL) {
647+
*events_dropped = rl->ratelimited_events;
648+
rl->ratelimited_events = 0;
649+
}
650+
return 0;
651+
}
652+
653+
/* drop event */
654+
rl->ratelimited_events++;
655+
RLDBG(("drop: ratelimited_events=%u", rl->ratelimited_events));
656+
return 1;
657+
}

log.h

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* $OpenBSD: log.h,v 1.34 2024/06/27 22:36:44 djm Exp $ */
1+
/* $OpenBSD: log.h,v 1.35 2024/12/07 10:05:37 djm Exp $ */
22

33
/*
44
* Author: Tatu Ylonen <[email protected]>
@@ -81,6 +81,30 @@ void sshfatal(const char *, const char *, int, int,
8181
void sshlogdirect(LogLevel, int, const char *, ...)
8282
__attribute__((format(printf, 3, 4)));
8383

84+
struct log_ratelimit_ctx {
85+
/* configuration */
86+
u_int threshold; /* events per second */
87+
u_int max_accum; /* max events to accumulate */
88+
u_int hysteresis; /* seconds */
89+
u_int log_every; /* seconds */
90+
91+
/* state */
92+
time_t last_event;
93+
u_int accumulated_events; /* used for threshold comparisons */
94+
95+
/* state while actively rate-limiting */
96+
int ratelimit_active;
97+
time_t ratelimit_start;
98+
time_t last_log;
99+
time_t hysteresis_start;
100+
u_int ratelimited_events;
101+
};
102+
103+
void log_ratelimit_init(struct log_ratelimit_ctx *rl, u_int threshold,
104+
u_int max_accum, u_int hysteresis, u_int log_every);
105+
int log_ratelimit(struct log_ratelimit_ctx *rl, time_t now, int *active,
106+
u_int *events_dropped);
107+
84108
#define do_log2(level, ...) sshlog(__FILE__, __func__, __LINE__, 0, level, NULL, __VA_ARGS__)
85109
#define debug3(...) sshlog(__FILE__, __func__, __LINE__, 0, SYSLOG_LEVEL_DEBUG3, NULL, __VA_ARGS__)
86110
#define debug2(...) sshlog(__FILE__, __func__, __LINE__, 0, SYSLOG_LEVEL_DEBUG2, NULL, __VA_ARGS__)

0 commit comments

Comments
 (0)