Path: blob/main/contrib/blocklist/bin/blocklistd.c
105189 views
/* $NetBSD: blocklistd.c,v 1.15 2026/02/07 14:32:04 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.15 2026/02/07 14:32:04 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)193goto out;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\" "199"uid=%lu gid=%lu",200bi->bi_type, bi->bi_fd, rbuf,201bi->bi_msg, (unsigned long)bi->bi_uid,202(unsigned long)bi->bi_gid);203}204205if (conf_find(bi->bi_fd, bi->bi_uid, &rss, &c) == NULL) {206(*lfun)(LOG_DEBUG, "no rule matched");207goto out;208}209210211if (state_get(state, &c, &dbi) == -1)212goto out;213214if (debug) {215char b1[128], b2[128];216(*lfun)(LOG_DEBUG, "%s: initial db state for %s: count=%d/%d "217"last=%s now=%s", __func__, rbuf, dbi.count, c.c_nfail,218fmttime(b1, sizeof(b1), dbi.last),219fmttime(b2, sizeof(b2), ts.tv_sec));220}221222switch (bi->bi_type) {223case BL_ABUSE:224/*225* If the application has signaled abusive behavior,226* set the number of fails to be one less than the227* configured limit. Fallthrough to the normal BL_ADD228* processing, which will increment the failure count229* to the threshold, and block the abusive address.230*/231if (c.c_nfail != -1)232dbi.count = c.c_nfail - 1;233/*FALLTHROUGH*/234case BL_ADD:235dbi.count++;236dbi.last = ts.tv_sec;237if (c.c_nfail != -1 && dbi.count >= c.c_nfail) {238/*239* No point in re-adding the rule.240* It might exist already due to latency in processing241* and removing the rule is the wrong thing to do as242* it allows a window to attack again.243*/244if (dbi.id[0] == '\0') {245int res = run_change("add", &c,246dbi.id, sizeof(dbi.id));247if (res == -1)248goto out;249}250sockaddr_snprintf(rbuf, sizeof(rbuf), "%a",251(void *)&rss);252(*lfun)(LOG_INFO,253"blocked %s/%d:%d for %d seconds",254rbuf, c.c_lmask, c.c_port, c.c_duration);255}256break;257case BL_DELETE:258if (dbi.last == 0)259goto out;260dbi.count = 0;261dbi.last = 0;262break;263case BL_BADUSER:264/* ignore for now */265break;266default:267(*lfun)(LOG_ERR, "unknown message %d", bi->bi_type);268}269state_put(state, &c, &dbi);270271out:272close(bi->bi_fd);273274if (debug) {275char b1[128], b2[128];276(*lfun)(LOG_DEBUG, "%s: final db state for %s: count=%d/%d "277"last=%s now=%s", __func__, rbuf, dbi.count, c.c_nfail,278fmttime(b1, sizeof(b1), dbi.last),279fmttime(b2, sizeof(b2), ts.tv_sec));280}281}282283static void284update_interfaces(void)285{286struct ifaddrs *oifas, *nifas;287288if (getifaddrs(&nifas) == -1)289return;290291oifas = ifas;292ifas = nifas;293294if (oifas)295freeifaddrs(oifas);296}297298static void299update(void)300{301struct timespec ts;302struct conf c;303struct dbinfo dbi;304unsigned int f, n;305char buf[128];306void *ss = &c.c_ss;307308if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {309(*lfun)(LOG_ERR, "clock_gettime failed (%m)");310return;311}312313again:314for (n = 0, f = 1; state_iterate(state, &c, &dbi, f) == 1;315f = 0, n++)316{317time_t when = c.c_duration + dbi.last;318if (debug > 1) {319char b1[64], b2[64];320sockaddr_snprintf(buf, sizeof(buf), "%a:%p", ss);321(*lfun)(LOG_DEBUG, "%s:[%u] %s count=%d duration=%d "322"last=%s " "now=%s", __func__, n, buf, dbi.count,323c.c_duration, fmttime(b1, sizeof(b1), dbi.last),324fmttime(b2, sizeof(b2), ts.tv_sec));325}326if (c.c_duration == -1 || when >= ts.tv_sec)327continue;328if (dbi.id[0]) {329run_change("rem", &c, dbi.id, 0);330sockaddr_snprintf(buf, sizeof(buf), "%a", ss);331(*lfun)(LOG_INFO, "released %s/%d:%d after %d seconds",332buf, c.c_lmask, c.c_port, c.c_duration);333}334if (state_del(state, &c) == 0)335goto again;336}337}338339static void340addfd(struct pollfd **pfdp, bl_t **blp, size_t *nfd, size_t *maxfd,341const char *path)342{343bl_t bl = bl_create(true, path, vflag ? vdlog : vsyslog_r);344if (bl == NULL || !bl_isconnected(bl))345exit(EXIT_FAILURE);346if (*nfd >= *maxfd) {347*maxfd += 10;348*blp = reallocarray(*blp, *maxfd, sizeof(**blp));349if (*blp == NULL)350err(EXIT_FAILURE, "malloc");351*pfdp = reallocarray(*pfdp, *maxfd, sizeof(**pfdp));352if (*pfdp == NULL)353err(EXIT_FAILURE, "malloc");354}355356(*pfdp)[*nfd].fd = bl_getfd(bl);357(*pfdp)[*nfd].events = POLLIN;358(*blp)[*nfd] = bl;359*nfd += 1;360}361362static void363uniqueadd(struct conf ***listp, size_t *nlist, size_t *mlist, struct conf *c)364{365struct conf **list = *listp;366367if (c->c_name[0] == '\0')368return;369for (size_t i = 0; i < *nlist; i++) {370if (strcmp(list[i]->c_name, c->c_name) == 0)371return;372}373if (*nlist == *mlist) {374*mlist += 10;375void *p = reallocarray(*listp, *mlist, sizeof(*list));376if (p == NULL)377err(EXIT_FAILURE, "Can't allocate for rule list");378list = *listp = p;379}380list[(*nlist)++] = c;381}382383static void384rules_flush(void)385{386struct conf **list;387size_t nlist, mlist;388389list = NULL;390mlist = nlist = 0;391for (size_t i = 0; i < rconf.cs_n; i++)392uniqueadd(&list, &nlist, &mlist, &rconf.cs_c[i]);393for (size_t i = 0; i < lconf.cs_n; i++)394uniqueadd(&list, &nlist, &mlist, &lconf.cs_c[i]);395396for (size_t i = 0; i < nlist; i++)397run_flush(list[i]);398free(list);399}400401static void402rules_restore(void)403{404DB *db;405struct conf c;406struct dbinfo dbi;407unsigned int f;408409db = state_open(dbfile, O_RDONLY, 0);410if (db == NULL) {411(*lfun)(LOG_ERR, "Can't open `%s' to restore state (%m)",412dbfile);413return;414}415for (f = 1; state_iterate(db, &c, &dbi, f) == 1; f = 0) {416if (dbi.id[0] == '\0')417continue;418(void)run_change("add", &c, dbi.id, sizeof(dbi.id));419state_put(state, &c, &dbi);420}421state_close(db);422state_sync(state);423}424425int426main(int argc, char *argv[])427{428int c, tout, flags, flush, restore, ret;429const char *spath, **blsock;430size_t nblsock, maxblsock;431432setprogname(argv[0]);433434spath = NULL;435blsock = NULL;436maxblsock = nblsock = 0;437flush = 0;438restore = 0;439tout = 0;440flags = O_RDWR|O_EXCL|O_CLOEXEC;441while ((c = getopt(argc, argv, "C:c:D:dfP:rR:s:t:v")) != -1) {442switch (c) {443case 'C':444controlprog = optarg;445break;446case 'c':447configfile = optarg;448break;449case 'D':450dbfile = optarg;451break;452case 'd':453debug++;454break;455case 'f':456flush++;457break;458case 'P':459spath = optarg;460break;461case 'R':462rulename = optarg;463break;464case 'r':465restore++;466break;467case 's':468if (nblsock >= maxblsock) {469maxblsock += 10;470void *p = reallocarray(blsock, maxblsock,471sizeof(*blsock));472if (p == NULL)473err(EXIT_FAILURE, "Can't allocate "474"memory for %zu sockets",475maxblsock);476blsock = p;477}478blsock[nblsock++] = optarg;479break;480case 't':481tout = atoi(optarg) * 1000;482break;483case 'v':484vflag++;485break;486default:487usage(c);488}489}490491argc -= optind;492if (argc)493usage('?');494495signal(SIGHUP, sighup);496signal(SIGINT, sigdone);497signal(SIGQUIT, sigdone);498signal(SIGTERM, sigdone);499signal(SIGUSR1, sigusr1);500signal(SIGUSR2, sigusr2);501502openlog(getprogname(), LOG_PID, LOG_DAEMON);503504if (debug) {505lfun = dlog;506if (tout == 0)507tout = 5000;508} else {509if (tout == 0)510tout = 15000;511}512513update_interfaces();514conf_parse(configfile);515if (flush) {516rules_flush();517if (!restore)518flags |= O_TRUNC;519}520521struct pollfd *pfd = NULL;522bl_t *bl = NULL;523size_t nfd = 0;524size_t maxfd = 0;525526for (size_t i = 0; i < nblsock; i++)527addfd(&pfd, &bl, &nfd, &maxfd, blsock[i]);528free(blsock);529530if (spath) {531FILE *fp = fopen(spath, "r");532char *line;533if (fp == NULL)534err(EXIT_FAILURE, "Can't open `%s'", spath);535for (; (line = fparseln(fp, NULL, NULL, NULL, 0)) != NULL;536free(line))537addfd(&pfd, &bl, &nfd, &maxfd, line);538fclose(fp);539}540if (nfd == 0)541addfd(&pfd, &bl, &nfd, &maxfd, _PATH_BLSOCK);542543state = state_open(dbfile, flags, 0600);544if (state == NULL)545state = state_open(dbfile, flags | O_CREAT, 0600);546else {547if (restore) {548if (!flush)549rules_flush();550rules_restore();551}552}553if (state == NULL)554exit(EXIT_FAILURE);555556if (!debug) {557if (daemon(0, 0) == -1)558err(EXIT_FAILURE, "daemon failed");559if (pidfile(NULL) == -1)560err(EXIT_FAILURE, "Can't create pidfile");561}562563for (size_t t = 0; !done; t++) {564if (readconf) {565readconf = 0;566conf_parse(configfile);567}568ret = poll(pfd, (nfds_t)nfd, tout);569if (debug)570(*lfun)(LOG_DEBUG, "received %d from poll()", ret);571switch (ret) {572case -1:573if (errno == EINTR)574continue;575(*lfun)(LOG_ERR, "poll (%m)");576exit(EXIT_FAILURE);577case 0:578state_sync(state);579break;580default:581for (size_t i = 0; i < nfd; i++)582if (pfd[i].revents & POLLIN)583process(bl[i]);584}585if (t % 100 == 0)586state_sync(state);587if (t % 10000 == 0)588update_interfaces();589update();590}591state_close(state);592exit(EXIT_SUCCESS);593}594595596