Path: blob/main/contrib/blocklist/bin/blocklistd.c
48062 views
/* $NetBSD: blocklistd.c,v 1.12 2025/10/25 18:43:51 christos Exp $ */12/*-3* Copyright (c) 2015 The NetBSD Foundation, Inc.4* All rights reserved.5*6* This code is derived from software contributed to The NetBSD Foundation7* by Christos Zoulas.8*9* Redistribution and use in source and binary forms, with or without10* modification, are permitted provided that the following conditions11* are met:12* 1. Redistributions of source code must retain the above copyright13* notice, this list of conditions and the following disclaimer.14* 2. Redistributions in binary form must reproduce the above copyright15* notice, this list of conditions and the following disclaimer in the16* documentation and/or other materials provided with the distribution.17*18* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS19* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED20* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR21* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS22* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR23* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF24* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS25* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN26* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)27* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE28* POSSIBILITY OF SUCH DAMAGE.29*/30#ifdef HAVE_CONFIG_H31#include "config.h"32#endif3334#ifdef HAVE_SYS_CDEFS_H35#include <sys/cdefs.h>36#endif37__RCSID("$NetBSD: blocklistd.c,v 1.12 2025/10/25 18:43:51 christos Exp $");3839#include <sys/types.h>40#include <sys/socket.h>41#include <sys/queue.h>4243#ifdef HAVE_LIBUTIL_H44#include <libutil.h>45#endif46#ifdef HAVE_UTIL_H47#include <util.h>48#endif49#include <string.h>50#include <signal.h>51#include <netdb.h>52#include <stdio.h>53#include <stdbool.h>54#include <string.h>55#include <inttypes.h>56#include <syslog.h>57#include <ctype.h>58#include <limits.h>59#include <errno.h>60#include <poll.h>61#include <fcntl.h>62#include <err.h>63#include <stdlib.h>64#include <unistd.h>65#include <time.h>66#include <ifaddrs.h>67#include <netinet/in.h>6869#include "bl.h"70#include "internal.h"71#include "conf.h"72#include "run.h"73#include "state.h"74#include "support.h"7576static const char *configfile = _PATH_BLCONF;77static DB *state;78static const char *dbfile = _PATH_BLSTATE;79static sig_atomic_t readconf;80static sig_atomic_t done;81static int vflag;8283static void84sigusr1(int n __unused)85{86debug++;87}8889static void90sigusr2(int n __unused)91{92debug--;93}9495static void96sighup(int n __unused)97{98readconf++;99}100101static void102sigdone(int n __unused)103{104done++;105}106107static __dead void108usage(int c)109{110if (c != '?')111warnx("Unknown option `%c'", (char)c);112fprintf(stderr, "Usage: %s [-vdfr] [-c <config>] [-R <rulename>] "113"[-P <sockpathsfile>] [-C <controlprog>] [-D <dbfile>] "114"[-s <sockpath>] [-t <timeout>]\n", getprogname());115exit(EXIT_FAILURE);116}117118static int119getremoteaddress(bl_info_t *bi, struct sockaddr_storage *rss, socklen_t *rsl)120{121*rsl = sizeof(*rss);122memset(rss, 0, *rsl);123124if (getpeername(bi->bi_fd, (void *)rss, rsl) != -1)125return 0;126127if (errno != ENOTCONN) {128(*lfun)(LOG_ERR, "getpeername failed (%m)");129return -1;130}131132if (bi->bi_slen == 0) {133(*lfun)(LOG_ERR, "unconnected socket with no peer in message");134return -1;135}136137switch (bi->bi_ss.ss_family) {138case AF_INET:139*rsl = sizeof(struct sockaddr_in);140break;141case AF_INET6:142*rsl = sizeof(struct sockaddr_in6);143break;144default:145(*lfun)(LOG_ERR, "bad client passed socket family %u",146(unsigned)bi->bi_ss.ss_family);147return -1;148}149150if (*rsl != bi->bi_slen) {151(*lfun)(LOG_ERR, "bad client passed socket length %u != %u",152(unsigned)*rsl, (unsigned)bi->bi_slen);153return -1;154}155156memcpy(rss, &bi->bi_ss, *rsl);157158#ifdef HAVE_STRUCT_SOCKADDR_SA_LEN159if (*rsl != rss->ss_len) {160(*lfun)(LOG_ERR,161"bad client passed socket internal length %u != %u",162(unsigned)*rsl, (unsigned)rss->ss_len);163return -1;164}165#endif166return 0;167}168169static void170process(bl_t bl)171{172struct sockaddr_storage rss;173socklen_t rsl;174char rbuf[BUFSIZ];175bl_info_t *bi;176struct conf c;177struct dbinfo dbi;178struct timespec ts;179180memset(&dbi, 0, sizeof(dbi));181memset(&c, 0, sizeof(c));182if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {183(*lfun)(LOG_ERR, "clock_gettime failed (%m)");184return;185}186187if ((bi = bl_recv(bl)) == NULL) {188(*lfun)(LOG_ERR, "no message (%m)");189return;190}191192if (getremoteaddress(bi, &rss, &rsl) == -1)193return;194195if (debug || bi->bi_msg[0]) {196sockaddr_snprintf(rbuf, sizeof(rbuf), "%a:%p", (void *)&rss);197(*lfun)(bi->bi_msg[0] ? LOG_INFO : LOG_DEBUG,198"processing type=%d fd=%d remote=%s msg=\"%s\" uid=%lu gid=%lu",199bi->bi_type, bi->bi_fd, rbuf,200bi->bi_msg, (unsigned long)bi->bi_uid,201(unsigned long)bi->bi_gid);202}203204if (conf_find(bi->bi_fd, bi->bi_uid, &rss, &c) == NULL) {205(*lfun)(LOG_DEBUG, "no rule matched");206return;207}208209210if (state_get(state, &c, &dbi) == -1)211return;212213if (debug) {214char b1[128], b2[128];215(*lfun)(LOG_DEBUG, "%s: initial db state for %s: count=%d/%d "216"last=%s now=%s", __func__, rbuf, dbi.count, c.c_nfail,217fmttime(b1, sizeof(b1), dbi.last),218fmttime(b2, sizeof(b2), ts.tv_sec));219}220221switch (bi->bi_type) {222case BL_ABUSE:223/*224* If the application has signaled abusive behavior,225* set the number of fails to be one less than the226* configured limit. Fallthrough to the normal BL_ADD227* processing, which will increment the failure count228* to the threshold, and block the abusive address.229*/230if (c.c_nfail != -1)231dbi.count = c.c_nfail - 1;232/*FALLTHROUGH*/233case BL_ADD:234dbi.count++;235dbi.last = ts.tv_sec;236if (c.c_nfail != -1 && dbi.count >= c.c_nfail) {237/*238* No point in re-adding the rule.239* It might exist already due to latency in processing240* and removing the rule is the wrong thing to do as241* it allows a window to attack again.242*/243if (dbi.id[0] == '\0') {244int res = run_change("add", &c,245dbi.id, sizeof(dbi.id));246if (res == -1)247goto out;248}249sockaddr_snprintf(rbuf, sizeof(rbuf), "%a",250(void *)&rss);251(*lfun)(LOG_INFO,252"blocked %s/%d:%d for %d seconds",253rbuf, c.c_lmask, c.c_port, c.c_duration);254}255break;256case BL_DELETE:257if (dbi.last == 0)258goto out;259dbi.count = 0;260dbi.last = 0;261break;262case BL_BADUSER:263/* ignore for now */264break;265default:266(*lfun)(LOG_ERR, "unknown message %d", bi->bi_type);267}268state_put(state, &c, &dbi);269270out:271if (debug) {272char b1[128], b2[128];273(*lfun)(LOG_DEBUG, "%s: final db state for %s: count=%d/%d "274"last=%s now=%s", __func__, rbuf, dbi.count, c.c_nfail,275fmttime(b1, sizeof(b1), dbi.last),276fmttime(b2, sizeof(b2), ts.tv_sec));277}278}279280static void281update_interfaces(void)282{283struct ifaddrs *oifas, *nifas;284285if (getifaddrs(&nifas) == -1)286return;287288oifas = ifas;289ifas = nifas;290291if (oifas)292freeifaddrs(oifas);293}294295static void296update(void)297{298struct timespec ts;299struct conf c;300struct dbinfo dbi;301unsigned int f, n;302char buf[128];303void *ss = &c.c_ss;304305if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {306(*lfun)(LOG_ERR, "clock_gettime failed (%m)");307return;308}309310again:311for (n = 0, f = 1; state_iterate(state, &c, &dbi, f) == 1;312f = 0, n++)313{314time_t when = c.c_duration + dbi.last;315if (debug > 1) {316char b1[64], b2[64];317sockaddr_snprintf(buf, sizeof(buf), "%a:%p", ss);318(*lfun)(LOG_DEBUG, "%s:[%u] %s count=%d duration=%d "319"last=%s " "now=%s", __func__, n, buf, dbi.count,320c.c_duration, fmttime(b1, sizeof(b1), dbi.last),321fmttime(b2, sizeof(b2), ts.tv_sec));322}323if (c.c_duration == -1 || when >= ts.tv_sec)324continue;325if (dbi.id[0]) {326run_change("rem", &c, dbi.id, 0);327sockaddr_snprintf(buf, sizeof(buf), "%a", ss);328(*lfun)(LOG_INFO, "released %s/%d:%d after %d seconds",329buf, c.c_lmask, c.c_port, c.c_duration);330}331if (state_del(state, &c) == 0)332goto again;333}334}335336static void337addfd(struct pollfd **pfdp, bl_t **blp, size_t *nfd, size_t *maxfd,338const char *path)339{340bl_t bl = bl_create(true, path, vflag ? vdlog : vsyslog_r);341if (bl == NULL || !bl_isconnected(bl))342exit(EXIT_FAILURE);343if (*nfd >= *maxfd) {344*maxfd += 10;345*blp = realloc(*blp, sizeof(**blp) * *maxfd);346if (*blp == NULL)347err(EXIT_FAILURE, "malloc");348*pfdp = realloc(*pfdp, sizeof(**pfdp) * *maxfd);349if (*pfdp == NULL)350err(EXIT_FAILURE, "malloc");351}352353(*pfdp)[*nfd].fd = bl_getfd(bl);354(*pfdp)[*nfd].events = POLLIN;355(*blp)[*nfd] = bl;356*nfd += 1;357}358359static void360uniqueadd(struct conf ***listp, size_t *nlist, size_t *mlist, struct conf *c)361{362struct conf **list = *listp;363364if (c->c_name[0] == '\0')365return;366for (size_t i = 0; i < *nlist; i++) {367if (strcmp(list[i]->c_name, c->c_name) == 0)368return;369}370if (*nlist == *mlist) {371*mlist += 10;372void *p = realloc(*listp, *mlist * sizeof(*list));373if (p == NULL)374err(EXIT_FAILURE, "Can't allocate for rule list");375list = *listp = p;376}377list[(*nlist)++] = c;378}379380static void381rules_flush(void)382{383struct conf **list;384size_t nlist, mlist;385386list = NULL;387mlist = nlist = 0;388for (size_t i = 0; i < rconf.cs_n; i++)389uniqueadd(&list, &nlist, &mlist, &rconf.cs_c[i]);390for (size_t i = 0; i < lconf.cs_n; i++)391uniqueadd(&list, &nlist, &mlist, &lconf.cs_c[i]);392393for (size_t i = 0; i < nlist; i++)394run_flush(list[i]);395free(list);396}397398static void399rules_restore(void)400{401DB *db;402struct conf c;403struct dbinfo dbi;404unsigned int f;405406db = state_open(dbfile, O_RDONLY, 0);407if (db == NULL) {408(*lfun)(LOG_ERR, "Can't open `%s' to restore state (%m)",409dbfile);410return;411}412for (f = 1; state_iterate(db, &c, &dbi, f) == 1; f = 0) {413if (dbi.id[0] == '\0')414continue;415(void)run_change("add", &c, dbi.id, sizeof(dbi.id));416state_put(state, &c, &dbi);417}418state_close(db);419state_sync(state);420}421422int423main(int argc, char *argv[])424{425int c, tout, flags, flush, restore, ret;426const char *spath, **blsock;427size_t nblsock, maxblsock;428429setprogname(argv[0]);430431spath = NULL;432blsock = NULL;433maxblsock = nblsock = 0;434flush = 0;435restore = 0;436tout = 0;437flags = O_RDWR|O_EXCL|O_CLOEXEC;438while ((c = getopt(argc, argv, "C:c:D:dfP:rR:s:t:v")) != -1) {439switch (c) {440case 'C':441controlprog = optarg;442break;443case 'c':444configfile = optarg;445break;446case 'D':447dbfile = optarg;448break;449case 'd':450debug++;451break;452case 'f':453flush++;454break;455case 'P':456spath = optarg;457break;458case 'R':459rulename = optarg;460break;461case 'r':462restore++;463break;464case 's':465if (nblsock >= maxblsock) {466maxblsock += 10;467void *p = realloc(blsock,468sizeof(*blsock) * maxblsock);469if (p == NULL)470err(EXIT_FAILURE,471"Can't allocate memory for %zu sockets",472maxblsock);473blsock = p;474}475blsock[nblsock++] = optarg;476break;477case 't':478tout = atoi(optarg) * 1000;479break;480case 'v':481vflag++;482break;483default:484usage(c);485}486}487488argc -= optind;489if (argc)490usage('?');491492signal(SIGHUP, sighup);493signal(SIGINT, sigdone);494signal(SIGQUIT, sigdone);495signal(SIGTERM, sigdone);496signal(SIGUSR1, sigusr1);497signal(SIGUSR2, sigusr2);498499openlog(getprogname(), LOG_PID, LOG_DAEMON);500501if (debug) {502lfun = dlog;503if (tout == 0)504tout = 5000;505} else {506if (tout == 0)507tout = 15000;508}509510update_interfaces();511conf_parse(configfile);512if (flush) {513rules_flush();514if (!restore)515flags |= O_TRUNC;516}517518struct pollfd *pfd = NULL;519bl_t *bl = NULL;520size_t nfd = 0;521size_t maxfd = 0;522523for (size_t i = 0; i < nblsock; i++)524addfd(&pfd, &bl, &nfd, &maxfd, blsock[i]);525free(blsock);526527if (spath) {528FILE *fp = fopen(spath, "r");529char *line;530if (fp == NULL)531err(EXIT_FAILURE, "Can't open `%s'", spath);532for (; (line = fparseln(fp, NULL, NULL, NULL, 0)) != NULL;533free(line))534addfd(&pfd, &bl, &nfd, &maxfd, line);535fclose(fp);536}537if (nfd == 0)538addfd(&pfd, &bl, &nfd, &maxfd, _PATH_BLSOCK);539540state = state_open(dbfile, flags, 0600);541if (state == NULL)542state = state_open(dbfile, flags | O_CREAT, 0600);543if (state == NULL)544return EXIT_FAILURE;545546if (restore) {547if (!flush)548rules_flush();549rules_restore();550}551552if (!debug) {553if (daemon(0, 0) == -1)554err(EXIT_FAILURE, "daemon failed");555if (pidfile(NULL) == -1)556err(EXIT_FAILURE, "Can't create pidfile");557}558559for (size_t t = 0; !done; t++) {560if (readconf) {561readconf = 0;562conf_parse(configfile);563}564ret = poll(pfd, (nfds_t)nfd, tout);565if (debug && ret != 0)566(*lfun)(LOG_DEBUG, "received %d from poll()", ret);567switch (ret) {568case -1:569if (errno == EINTR)570continue;571(*lfun)(LOG_ERR, "poll (%m)");572return EXIT_FAILURE;573case 0:574state_sync(state);575break;576default:577for (size_t i = 0; i < nfd; i++)578if (pfd[i].revents & POLLIN)579process(bl[i]);580}581if (t % 100 == 0)582state_sync(state);583if (t % 10000 == 0)584update_interfaces();585update();586}587state_close(state);588return 0;589}590591592