jcs's openbsd hax
openbsd

add infrastructure for ratelimited logging; feedback/ok dtucker

djm cc7fda5d 28676b80

+193 -3
+168 -2
usr.bin/ssh/log.c
··· 1 - /* $OpenBSD: log.c,v 1.63 2024/10/14 01:57:50 djm Exp $ */ 1 + /* $OpenBSD: log.c,v 1.64 2024/12/07 10:05:36 djm Exp $ */ 2 2 /* 3 3 * Author: Tatu Ylonen <ylo@cs.hut.fi> 4 4 * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland ··· 36 36 37 37 #include <sys/types.h> 38 38 39 + #include <errno.h> 39 40 #include <fcntl.h> 41 + #include <limits.h> 40 42 #include <stdarg.h> 41 43 #include <stdio.h> 42 44 #include <stdlib.h> 43 45 #include <string.h> 44 46 #include <syslog.h> 47 + #include <time.h> 45 48 #include <unistd.h> 46 - #include <errno.h> 47 49 #include <vis.h> 48 50 49 51 #include "log.h" ··· 447 449 do_log(level, forced, NULL, fmt, args); 448 450 va_end(args); 449 451 } 452 + 453 + 454 + /* 455 + * A simple system for ratelimiting aperiodic events such as logs, without 456 + * needing to be hooked into a mainloop/timer. A running total of events is 457 + * maintained and when it exceeds a threshold further events are dropped 458 + * until the rate falls back below that threshold. 459 + * 460 + * To prevent flipping in and out of rate-limiting, there is a hysteresis 461 + * timer that delays leaving the rate-limited state. 462 + * 463 + * While in the rate-limited state, events can be periodically allowed though 464 + * and the number of dropped events since the last log obtained. 465 + * 466 + * XXX a moving average rate of events might be a better approach here rather 467 + * than linear decay, which can suppress events for a while after large 468 + * bursts. 469 + */ 470 + 471 + /* #define RATELIMIT_DEBUG 1 */ 472 + 473 + #ifdef RATELIMIT_DEBUG 474 + # define RLDBG(x) do { \ 475 + printf("%s:%d %s: ", __FILE__, __LINE__, __func__); \ 476 + printf x; \ 477 + printf("\n"); \ 478 + fflush(stdout); \ 479 + } while (0) 480 + #else 481 + # define RLDBG(x) 482 + #endif 483 + 484 + /* set up a ratelimit */ 485 + void 486 + log_ratelimit_init(struct log_ratelimit_ctx *rl, u_int threshold, 487 + u_int max_accum, u_int hysteresis, u_int log_every) 488 + { 489 + memset(rl, 0, sizeof(*rl)); 490 + rl->threshold = threshold; 491 + rl->max_accum = max_accum; 492 + rl->hysteresis = hysteresis; 493 + rl->log_every = log_every; 494 + RLDBG(("called: rl=%p thresh=%u max=%u hys=%u log_every=%u", 495 + rl, rl->threshold, rl->max_accum, rl->hysteresis, rl->log_every)); 496 + } 497 + 498 + /* 499 + * check whether a log event should be dropped because of rate-limiting. 500 + * returns non-zero if the event should be dropped. If events_since_last 501 + * is supplied then, for periodic logs, it will be set to the number of 502 + * dropped events since the last message. 503 + */ 504 + int 505 + log_ratelimit(struct log_ratelimit_ctx *rl, time_t now, int *active, 506 + u_int *events_dropped) 507 + { 508 + time_t olast_event; 509 + 510 + RLDBG(("called: rl=%p thresh=%u max=%u hys=%u log_every=%u " 511 + "accum=%u since=%ld since_last=%u", rl, rl->threshold, 512 + rl->max_accum, rl->hysteresis, 513 + rl->log_every, rl->accumulated_events, 514 + rl->last_event == 0 ? -1 : (long)(now - rl->last_event), 515 + rl->ratelimited_events)); 516 + 517 + if (now < 0) 518 + return 0; 519 + if (events_dropped != NULL) 520 + *events_dropped = 0; 521 + if (active != NULL) 522 + *active = rl->ratelimit_active; 523 + 524 + /* First, decay accumulated events */ 525 + if (rl->last_event <= 0) 526 + rl->last_event = now; 527 + if (now > rl->last_event) { 528 + uint64_t n = now - rl->last_event; 529 + 530 + if (n > UINT_MAX) 531 + n = UINT_MAX; 532 + if (rl->accumulated_events < (u_int)n) 533 + rl->accumulated_events = 0; 534 + else 535 + rl->accumulated_events -= (u_int)n; 536 + RLDBG(("decay: accum=%u", rl->accumulated_events)); 537 + } 538 + rl->accumulated_events++; /* add this event */ 539 + if (rl->accumulated_events > rl->max_accum) 540 + rl->accumulated_events = rl->max_accum; 541 + olast_event = rl->last_event; 542 + rl->last_event = now; 543 + RLDBG(("check threshold: accum=%u vs thresh=%u", 544 + rl->accumulated_events, rl->threshold)); 545 + 546 + /* Are we under threshold? */ 547 + if (rl->accumulated_events < rl->threshold) { 548 + if (!rl->ratelimit_active) 549 + return 0; 550 + RLDBG(("under threshold: hys=%u since_hys=%ld since_last=%ld", 551 + rl->hysteresis, rl->hysteresis_start == 0 ? -1 : 552 + (long)(now - rl->hysteresis_start), 553 + olast_event == 0 ? -1 : (long)(now - olast_event))); 554 + if (rl->hysteresis_start == 0) { 555 + /* active, but under threshold; hysteresis */ 556 + if (olast_event + rl->hysteresis < now) { 557 + /* hysteresis expired before this event */ 558 + RLDBG(("hysteresis preexpired")); 559 + goto inactive; 560 + } 561 + RLDBG(("start hysteresis")); 562 + rl->hysteresis_start = now; 563 + } else if (rl->hysteresis_start + rl->hysteresis < now) { 564 + /* Hysteresis period expired, transition to inactive */ 565 + RLDBG(("complete hysteresis")); 566 + inactive: 567 + if (events_dropped != NULL) 568 + *events_dropped = rl->ratelimited_events; 569 + if (active != NULL) 570 + *active = 0; 571 + rl->ratelimit_active = 0; 572 + rl->ratelimit_start = 0; 573 + rl->last_log = 0; 574 + rl->hysteresis_start = 0; 575 + rl->ratelimited_events = 0; 576 + return 0; 577 + } 578 + /* ratelimiting active, but in hysteresis period */ 579 + } else if (!rl->ratelimit_active) { 580 + /* Transition to rate-limiting */ 581 + RLDBG(("start ratelimit")); 582 + rl->ratelimit_active = 1; 583 + rl->ratelimit_start = now; 584 + rl->last_log = now; 585 + rl->hysteresis_start = 0; 586 + rl->ratelimited_events = 1; 587 + if (active != NULL) 588 + *active = 1; 589 + return 1; 590 + } else if (rl->hysteresis_start != 0) { 591 + /* active and over threshold; reset hysteresis timer */ 592 + RLDBG(("clear hysteresis")); 593 + rl->hysteresis_start = 0; 594 + } 595 + 596 + /* over threshold or in hysteresis period; log periodically */ 597 + if (active != NULL) 598 + *active = 1; 599 + RLDBG(("log_every=%u since_log=%ld", rl->log_every, 600 + (long)(now - rl->last_log))); 601 + if (rl->log_every > 0 && now >= rl->last_log + rl->log_every) { 602 + RLDBG(("periodic: since_last=%u", rl->ratelimited_events)); 603 + rl->last_log = now; 604 + if (events_dropped != NULL) { 605 + *events_dropped = rl->ratelimited_events; 606 + rl->ratelimited_events = 0; 607 + } 608 + return 0; 609 + } 610 + 611 + /* drop event */ 612 + rl->ratelimited_events++; 613 + RLDBG(("drop: ratelimited_events=%u", rl->ratelimited_events)); 614 + return 1; 615 + }
+25 -1
usr.bin/ssh/log.h
··· 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 $ */ 2 2 3 3 /* 4 4 * Author: Tatu Ylonen <ylo@cs.hut.fi> ··· 77 77 __attribute__((format(printf, 7, 8))); 78 78 void sshlogdirect(LogLevel, int, const char *, ...) 79 79 __attribute__((format(printf, 3, 4))); 80 + 81 + struct log_ratelimit_ctx { 82 + /* configuration */ 83 + u_int threshold; /* events per second */ 84 + u_int max_accum; /* max events to accumulate */ 85 + u_int hysteresis; /* seconds */ 86 + u_int log_every; /* seconds */ 87 + 88 + /* state */ 89 + time_t last_event; 90 + u_int accumulated_events; /* used for threshold comparisons */ 91 + 92 + /* state while actively rate-limiting */ 93 + int ratelimit_active; 94 + time_t ratelimit_start; 95 + time_t last_log; 96 + time_t hysteresis_start; 97 + u_int ratelimited_events; 98 + }; 99 + 100 + void log_ratelimit_init(struct log_ratelimit_ctx *rl, u_int threshold, 101 + u_int max_accum, u_int hysteresis, u_int log_every); 102 + int log_ratelimit(struct log_ratelimit_ctx *rl, time_t now, int *active, 103 + u_int *events_dropped); 80 104 81 105 #define do_log2(level, ...) sshlog(__FILE__, __func__, __LINE__, 0, level, NULL, __VA_ARGS__) 82 106 #define debug3(...) sshlog(__FILE__, __func__, __LINE__, 0, SYSLOG_LEVEL_DEBUG3, NULL, __VA_ARGS__)