2 * Bouncer -- A Daemon for Turning Away Mischievous Guests
4 * (c) 2016 Martin Mares <mj@ucw.cz>
6 * FIXME: ipset create bouncer4 hash:ip family inet
7 * FIXME: ipset create bouncer6 hash:ip family inet6
8 * FIXME: sshd_config: UseDNS no
9 * FIXME: PAM module names should be made configurable
15 #include <ucw/clists.h>
18 #include <ucw/mainloop.h>
20 #include <ucw/string.h>
23 #include <arpa/inet.h>
26 #include <sys/types.h>
27 #include <sys/socket.h>
33 #include <libipset/data.h>
34 #include <libipset/session.h>
35 #include <libipset/types.h>
37 /*** Internal representation of IPv4/IPv6 addresses ***/
39 // In network byte order, IPv4 represented as ::ffff:1.2.3.4
44 #define ADDR_BUFSIZE 64
45 #define AFMT(_a) ({ char *_buf = alloca(ADDR_BUFSIZE); addr_format(_buf, _a); _buf; })
47 static bool addr_is_v4(struct addr addr)
49 return !addr.a[0] && !addr.a[1] && addr.a[2] == htonl(0xffff);
52 static void addr_format(char *buf, struct addr addr)
58 in4.s_addr = addr.a[3];
59 ok = inet_ntop(AF_INET, &in4, buf, ADDR_BUFSIZE);
64 memcpy(&in6, &addr, sizeof(addr));
65 ok = inet_ntop(AF_INET6, &in6, buf, ADDR_BUFSIZE);
68 snprintf(buf, ADDR_BUFSIZE, "<error %d>", errno);
71 static bool addr_parse(struct addr *addr, const char *src)
76 if (inet_pton(AF_INET, src, &a4))
78 addr->a[0] = addr->a[1] = 0;
79 addr->a[2] = htonl(0xffff);
80 addr->a[3] = a4.s_addr;
83 else if (inet_pton(AF_INET6, src, &a6))
85 memcpy(&addr, &a6, 16);
92 /*** Configuration ***/
94 static char *listen_on = "/var/run/bouncer.sock";
95 static uns max_failures = ~0U;
96 static uns max_suspect_time = 86400;
97 static uns max_banned_time = 86400;
98 static uns max_suspects = ~0U;
99 static uns max_banned = ~0U;
100 static char *ipv4_set;
101 static char *ipv6_set;
102 static char *config_log_stream;
104 static struct cf_section bouncer_cf = {
106 CF_STRING("ListenOn", &listen_on),
107 CF_UNS("MaxSuspects", &max_suspects),
108 CF_UNS("MaxBanned", &max_banned),
109 CF_UNS("MaxSuspectTime", &max_suspect_time),
110 CF_UNS("MaxBannedTime", &max_banned_time),
111 CF_UNS("MaxFailures", &max_failures),
112 CF_STRING("IPv4Set", &ipv4_set),
113 CF_STRING("IPv6Set", &ipv6_set),
114 CF_STRING("LogStream", &config_log_stream),
119 /*** An interface to IP sets ***/
121 static struct ipset_session *is_sess;
123 static const char *trim_eol(const char *msg)
125 int len = strlen(msg);
126 if (!len || msg[len-1] != '\n')
130 char *x = xstrdup(msg);
136 static void is_die(const char *when)
138 const char *warn = ipset_session_warning(is_sess);
140 msg(L_WARN, "%s: %s", when, trim_eol(warn));
142 const char *err = ipset_session_error(is_sess);
143 die("%s: %s", when, err ? trim_eol(err) : "Unknown error");
146 static void is_err(const char *when)
148 const char *warn = ipset_session_warning(is_sess);
150 msg(L_WARN, "%s: %s", when, trim_eol(warn));
152 const char *err = ipset_session_error(is_sess);
153 msg(L_ERROR, "%s: %s", when, err ? trim_eol(err) : "Unknown error");
155 ipset_session_report_reset(is_sess);
158 static void is_init(void)
162 is_sess = ipset_session_init(printf);
164 die("Unable to initialize ipset session");
167 static bool is_setup(char *set)
172 if (ipset_parse_setname(is_sess, IPSET_SETNAME, set) < 0)
173 is_die("ipset_parse_setname");
177 static void is_flush(char *set)
182 if (ipset_cmd(is_sess, IPSET_CMD_FLUSH, 0) < 0)
183 return is_err("IPSET_CMD_FLUSH");
186 static bool is_modify(bool add, struct addr addr)
188 int cmd = add ? IPSET_CMD_ADD : IPSET_CMD_DEL;
189 char *set = addr_is_v4(addr) ? ipv4_set : ipv6_set;
193 if (ipset_envopt_parse(is_sess, IPSET_ENV_EXIST, NULL) < 0)
194 is_die("IPSET_ENV_EXIST");
196 const struct ipset_type *is_type = ipset_type_get(is_sess, cmd);
198 is_die("ipset_type_get");
200 if (is_type->dimension != 1)
201 die("Invalid ipset dimension %d", is_type->dimension);
203 char buf[ADDR_BUFSIZE];
204 addr_format(buf, addr);
205 if (ipset_parse_elem(is_sess, 0, buf) < 0)
207 is_err("ipset_parse_elem");
211 if (ipset_cmd(is_sess, cmd, 0) < 0)
213 is_err(add ? "IPSET_CMD_ADD" : "IPSET_CMD_DEL");
220 /*** Handling of login failures ***/
222 struct culprit_node {
223 cnode n; // In either suspect_list or banned_list
230 timestamp_t last_fail; // Not updated when banned
233 #define HASH_NODE struct culprit_node
234 #define HASH_PREFIX(x) culprit_##x
235 #define HASH_KEY_MEMORY addr_bytes
236 #define HASH_KEY_SIZE 16
237 #define HASH_WANT_LOOKUP
238 #define HASH_WANT_REMOVE
239 #define HASH_USE_AUTO_ELTPOOL 1000
240 #define HASH_ZERO_FILL
241 #define HASH_LOOKUP_DETECT_NEW
242 #include <ucw/hashtable.h>
244 static clist suspect_list, banned_list;
245 static uns num_suspects, num_banned;
246 static struct main_timer cleanup_timer;
248 static void cleanup_list(clist *list, uns *counter, timestamp_t max_time, uns max_count, timestamp_t *next)
250 timestamp_t now = main_get_now();
254 struct culprit_node *c = clist_head(list);
258 timestamp_t expire_in = c->last_fail + max_time;
259 if (*counter > max_count)
261 static timestamp_t last_overflow_warning;
262 if (last_overflow_warning + 60000 < now)
264 last_overflow_warning = now;
266 msg(L_WARN, "Too many bans, dropping some. Try increasing MaxBanned.");
268 msg(L_WARN, "Too many suspects, dropping some. Try increasing MaxSuspects.");
275 *next = MIN(*next, expire_in);
281 msg(L_INFO, "Unbanning %s", AFMT(c->addr));
282 is_modify(0, c->addr);
285 msg(L_DEBUG, "Suspect %s: acquitted", AFMT(c->addr));
293 static void culprit_cleanup(void)
295 timestamp_t next_cleanup = main_get_now() + (timestamp_t)3600 * 1000;
296 cleanup_list(&suspect_list, &num_suspects, (timestamp_t)max_suspect_time * 1000, max_suspects, &next_cleanup);
297 cleanup_list(&banned_list, &num_banned, (timestamp_t)max_banned_time * 1000, max_banned, &next_cleanup);
298 timer_add(&cleanup_timer, next_cleanup);
301 static void handle_failed_login(struct addr addr, int cnt)
304 timestamp_t now = main_get_now();
306 struct culprit_node *c = culprit_lookup((byte *) &addr, &is_new);
312 clist_add_tail(&suspect_list, &c->n);
314 msg(L_DEBUG, "Suspect %s: new, failures=%u", AFMT(addr), c->fail_count);
319 c->fail_count += cnt;
321 clist_add_tail(&suspect_list, &c->n);
322 msg(L_DEBUG, "Suspect %s: failures=%u", AFMT(addr), c->fail_count);
325 if (!c->banned && c->fail_count >= max_failures)
327 msg(L_INFO, "Banning %s: failures=%u", AFMT(addr), c->fail_count);
331 clist_add_tail(&banned_list, &c->n);
333 is_modify(1, c->addr);
339 static void culprit_timer(struct main_timer *tm UNUSED)
344 static void fail_init(void)
347 clist_init(&suspect_list);
348 clist_init(&banned_list);
349 cleanup_timer.handler = culprit_timer;
352 /*** Parsing of log messages ***/
354 static bool check_next(char **pp, char *want)
366 static void parse_failure(char *p, int cnt)
368 DBG("Parse 4: <%s> cnt=%d", p, cnt);
381 while (*p && *p != ' ' && *p != '=')
388 while (*p && *p != ' ')
395 DBG("Parse KV: %s=<%s>", key, val);
396 if (!strcmp(key, "rhost"))
400 // Act on the message
402 if (addr_parse(&addr, rhost))
403 handle_failed_login(addr, cnt);
405 msg(L_WARN, "Unable to parse address %s", rhost);
408 static void process_msg(char *line)
410 DBG("Parse: <%s>", line);
414 // 2016-11-04T17:18:54.825821+01:00 sshd[6733]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=1.2.3.4
415 // 2016-11-05T12:49:52.418880+01:00 sshd[16271]: PAM 2 more authentication failures; logname= uid=0 euid=0 tty=ssh ruser= rhost=116.31.116.26 user=root
417 // We shall start with 32 non-spaces
418 for (int i=0; i<32; i++)
424 DBG("Parse 1: <%s>", p);
426 // Space, something, colon, space
429 while (*p && *p != ' ' && *p != ':')
431 if (!check_next(&p, ": "))
433 DBG("Parse 2: <%s>", p);
435 // pam_unix(something), colon, space
436 if (check_next(&p, "pam_unix("))
445 if (!check_next(&p, ": "))
447 DBG("Parse 3: <%s>", p);
449 if (!check_next(&p, "authentication failure; "))
455 // "PAM <n> more authentication failures;"
456 if (check_next(&p, "PAM "))
458 if (!(*p >= '0' && *p <= '9'))
461 while (*p >= '0' && *p <= '9')
464 if (!check_next(&p, " more authentication failures; "))
467 parse_failure(p, cnt);
471 /*** Socket for receiving messages from rsyslog ***/
473 struct main_file sk_file;
475 static int sk_read(struct main_file *mf)
478 int len = recv(mf->fd, line, sizeof(line), MSG_TRUNC);
481 if (errno == EINTR || errno == EAGAIN)
486 if (len >= (int) sizeof(line))
488 msg(L_WARN, "Truncated message received (length=%d)", len);
489 len = sizeof(line) - 1;
493 if (len > 0 && line[len-1] == '\n')
495 if (len > 0 && line[len-1] == '\r')
502 static void sk_init(void)
505 mode_t old_umask = umask(0077);
508 if ((fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0)
509 die("Cannot create PF_UNIX socket: %m");
511 struct sockaddr_un sa = { .sun_family = AF_UNIX };
512 strcpy(sa.sun_path, listen_on);
513 if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
514 die("Cannot bind socket %s: %m", listen_on);
517 sk_file.read_handler = sk_read;
525 static struct opt_section options = {
527 OPT_HELP("Bouncer -- A Daemon for Turning Away Mischievous Guests"),
529 OPT_HELP("Options:"),
536 int main(int argc UNUSED, char **argv)
538 cf_def_file = "config"; // FIXME
539 cf_declare_section("Bouncer", &bouncer_cf, 0);
540 opt_parse(&options, argv+1);
542 if (config_log_stream)
543 log_configured(config_log_stream);
553 msg(L_INFO, "Starting");