]> mj.ucw.cz Git - bouncer.git/blob - bouncer.c
Implemented probation
[bouncer.git] / bouncer.c
1 /*
2  *      Bouncer -- A Daemon for Turning Away Mischievous Guests
3  *
4  *      (c) 2016 Martin Mares <mj@ucw.cz>
5  */
6
7 #undef LOCAL_DEBUG
8
9 #include <ucw/lib.h>
10 #include <ucw/clists.h>
11 #include <ucw/conf.h>
12 #include <ucw/log.h>
13 #include <ucw/mainloop.h>
14 #include <ucw/opt.h>
15 #include <ucw/string.h>
16
17 #include <alloca.h>
18 #include <arpa/inet.h>
19 #include <errno.h>
20 #include <string.h>
21 #include <sys/types.h>
22 #include <sys/socket.h>
23 #include <sys/stat.h>
24 #include <sys/un.h>
25 #include <time.h>
26 #include <unistd.h>
27
28 #include <libipset/data.h>
29 #include <libipset/session.h>
30 #include <libipset/types.h>
31
32 /*** Internal representation of IPv4/IPv6 addresses ***/
33
34 // In network byte order, IPv4 represented as ::ffff:1.2.3.4
35 struct addr {
36   u32 a[4];
37 };
38
39 #define ADDR_BUFSIZE 64
40 #define AFMT(_a) ({ char *_buf = alloca(ADDR_BUFSIZE); addr_format(_buf, _a); _buf; })
41
42 static bool addr_is_v4(struct addr addr)
43 {
44   return !addr.a[0] && !addr.a[1] && addr.a[2] == htonl(0xffff);
45 }
46
47 static void addr_format(char *buf, struct addr addr)
48 {
49   const char *ok;
50   if (addr_is_v4(addr))
51     {
52       struct in_addr in4;
53       in4.s_addr = addr.a[3];
54       ok = inet_ntop(AF_INET, &in4, buf, ADDR_BUFSIZE);
55     }
56   else
57     {
58       struct in6_addr in6;
59       memcpy(&in6, &addr, sizeof(addr));
60       ok = inet_ntop(AF_INET6, &in6, buf, ADDR_BUFSIZE);
61     }
62   if (!ok)
63     snprintf(buf, ADDR_BUFSIZE, "<error %d>", errno);
64 }
65
66 static bool addr_parse(struct addr *addr, const char *src)
67 {
68   struct in_addr a4;
69   struct in6_addr a6;
70
71   if (inet_pton(AF_INET, src, &a4))
72     {
73       addr->a[0] = addr->a[1] = 0;
74       addr->a[2] = htonl(0xffff);
75       addr->a[3] = a4.s_addr;
76       return 1;
77     }
78   else if (inet_pton(AF_INET6, src, &a6))
79     {
80       memcpy(&addr, &a6, 16);
81       return 1;
82     }
83   else
84     return 0;
85 }
86
87 /*** Configuration ***/
88
89 static char *listen_on = "/var/run/bouncer.sock";
90 static uns max_failures = ~0U;
91 static uns max_suspect_time = 86400;
92 static uns max_banned_time = 86400;
93 static uns max_suspects = ~0U;
94 static uns max_banned = ~0U;
95 static uns probation;
96 static char *ipv4_set;
97 static char *ipv6_set;
98 static char *config_log_stream;
99
100 static struct cf_section bouncer_cf = {
101   CF_ITEMS {
102     CF_STRING("ListenOn", &listen_on),
103     CF_UNS("MaxSuspects", &max_suspects),
104     CF_UNS("MaxBanned", &max_banned),
105     CF_UNS("MaxSuspectTime", &max_suspect_time),
106     CF_UNS("MaxBannedTime", &max_banned_time),
107     CF_UNS("MaxFailures", &max_failures),
108     CF_UNS("Probation", &probation),
109     CF_STRING("IPv4Set", &ipv4_set),
110     CF_STRING("IPv6Set", &ipv6_set),
111     CF_STRING("LogStream", &config_log_stream),
112     CF_END
113   }
114 };
115
116 /*** An interface to IP sets ***/
117
118 static struct ipset_session *is_sess;
119
120 static const char *trim_eol(const char *msg)
121 {
122   int len = strlen(msg);
123   if (!len || msg[len-1] != '\n')
124     return msg;
125   else
126     {
127       char *x = xstrdup(msg);
128       x[len-1] = 0;
129       return x;
130     }
131 }
132
133 static void is_die(const char *when)
134 {
135   const char *warn = ipset_session_warning(is_sess);
136   if (warn)
137     msg(L_WARN, "%s: %s", when, trim_eol(warn));
138
139   const char *err = ipset_session_error(is_sess);
140   die("%s: %s", when, err ? trim_eol(err) : "Unknown error");
141 }
142
143 static void is_err(const char *when)
144 {
145   const char *warn = ipset_session_warning(is_sess);
146   if (warn)
147     msg(L_WARN, "%s: %s", when, trim_eol(warn));
148
149   const char *err = ipset_session_error(is_sess);
150   msg(L_ERROR, "%s: %s", when, err ? trim_eol(err) : "Unknown error");
151
152   ipset_session_report_reset(is_sess);
153 }
154
155 static void is_init(void)
156 {
157   ipset_load_types();
158
159   is_sess = ipset_session_init(printf);
160   if (!is_sess)
161     die("Unable to initialize ipset session");
162 }
163
164 static bool is_setup(char *set)
165 {
166   if (!set)
167     return 0;
168
169   if (ipset_parse_setname(is_sess, IPSET_SETNAME, set) < 0)
170     is_die("ipset_parse_setname");
171   return 1;
172 }
173
174 static void is_flush(char *set)
175 {
176   if (!is_setup(set))
177     return;
178
179   if (ipset_cmd(is_sess, IPSET_CMD_FLUSH, 0) < 0)
180     return is_err("IPSET_CMD_FLUSH");
181 }
182
183 static bool is_modify(bool add, struct addr addr)
184 {
185   int cmd = add ? IPSET_CMD_ADD : IPSET_CMD_DEL;
186   char *set = addr_is_v4(addr) ? ipv4_set : ipv6_set;
187   if (!is_setup(set))
188     return 0;
189
190   if (ipset_envopt_parse(is_sess, IPSET_ENV_EXIST, NULL) < 0)
191     is_die("IPSET_ENV_EXIST");
192
193   const struct ipset_type *is_type = ipset_type_get(is_sess, cmd);
194   if (!is_type)
195     is_die("ipset_type_get");
196
197   if (is_type->dimension != 1)
198     die("Invalid ipset dimension %d", is_type->dimension);
199
200   char buf[ADDR_BUFSIZE];
201   addr_format(buf, addr);
202   if (ipset_parse_elem(is_sess, 0, buf) < 0)
203     {
204       is_err("ipset_parse_elem");
205       return 0;
206     }
207
208   if (ipset_cmd(is_sess, cmd, 0) < 0)
209     {
210       is_err(add ? "IPSET_CMD_ADD" : "IPSET_CMD_DEL");
211       return 0;
212     }
213
214   return 1;
215 }
216
217 /*** Handling of login failures ***/
218
219 struct culprit_node {
220   cnode n;                      // In either suspect_list or banned_list
221   union {
222     struct addr addr;
223     byte addr_bytes[16];
224   };
225   uns fail_count;
226   bool banned;
227   timestamp_t last_fail;        // Not updated when banned
228 };
229
230 #define HASH_NODE struct culprit_node
231 #define HASH_PREFIX(x) culprit_##x
232 #define HASH_KEY_MEMORY addr_bytes
233 #define HASH_KEY_SIZE 16
234 #define HASH_WANT_LOOKUP
235 #define HASH_WANT_REMOVE
236 #define HASH_USE_AUTO_ELTPOOL 1000
237 #define HASH_ZERO_FILL
238 #define HASH_LOOKUP_DETECT_NEW
239 #include <ucw/hashtable.h>
240
241 static clist suspect_list, banned_list;
242 static uns num_suspects, num_banned;
243 static struct main_timer cleanup_timer;
244
245 static void cleanup_list(clist *list, uns *counter, timestamp_t max_time, uns max_count, timestamp_t *next)
246 {
247   timestamp_t now = main_get_now();
248
249   for (;;)
250     {
251       struct culprit_node *c = clist_head(list);
252       if (!c)
253         break;
254
255       timestamp_t expire_in = c->last_fail + max_time;
256       if (*counter > max_count)
257         {
258           static timestamp_t last_overflow_warning;
259           if (last_overflow_warning + 60000 < now)
260             {
261               last_overflow_warning = now;
262               if (c->banned)
263                 msg(L_WARN, "Too many bans, dropping some. Try increasing MaxBanned.");
264               else
265                 msg(L_WARN, "Too many suspects, dropping some. Try increasing MaxSuspects.");
266             }
267           expire_in = 0;
268         }
269
270       if (expire_in > now)
271         {
272           *next = MIN(*next, expire_in);
273           break;
274         }
275
276       clist_remove(&c->n);
277       (*counter)--;
278
279       if (c->banned)
280         {
281           msg(L_INFO, "Unbanning %s", AFMT(c->addr));
282           is_modify(0, c->addr);
283           if (probation)
284             {
285               c->banned = 0;
286               c->last_fail = now;
287               c->fail_count = max_failures - probation;
288               clist_add_tail(&suspect_list, &c->n);
289               num_suspects++;
290               msg(L_DEBUG, "Suspect %s: probation, failures=%u", AFMT(c->addr), c->fail_count);
291             }
292           else
293             culprit_remove(c);
294         }
295       else
296         {
297           msg(L_DEBUG, "Suspect %s: acquitted", AFMT(c->addr));
298           culprit_remove(c);
299         }
300     }
301 }
302
303 static void culprit_cleanup(void)
304 {
305   timestamp_t next_cleanup = main_get_now() + (timestamp_t)3600 * 1000;
306   cleanup_list(&banned_list, &num_banned, (timestamp_t)max_banned_time * 1000, max_banned, &next_cleanup);
307   cleanup_list(&suspect_list, &num_suspects, (timestamp_t)max_suspect_time * 1000, max_suspects, &next_cleanup);
308   timer_add(&cleanup_timer, next_cleanup);
309 }
310
311 static void handle_failed_login(struct addr addr, int cnt)
312 {
313   int is_new;
314   timestamp_t now = main_get_now();
315
316   struct culprit_node *c = culprit_lookup((byte *) &addr, &is_new);
317   if (is_new)
318     {
319       c->last_fail = now;
320       c->fail_count = cnt;
321       c->banned = 0;
322       clist_add_tail(&suspect_list, &c->n);
323       num_suspects++;
324       msg(L_DEBUG, "Suspect %s: new, failures=%u", AFMT(addr), c->fail_count);
325     }
326   else if (!c->banned)
327     {
328       c->last_fail = now;
329       c->fail_count += cnt;
330       clist_remove(&c->n);
331       clist_add_tail(&suspect_list, &c->n);
332       msg(L_DEBUG, "Suspect %s: failures=%u", AFMT(addr), c->fail_count);
333     }
334
335   if (!c->banned && c->fail_count >= max_failures)
336     {
337       msg(L_INFO, "Banning %s: failures=%u", AFMT(addr), c->fail_count);
338       c->banned = 1;
339       clist_remove(&c->n);
340       num_suspects--;
341       clist_add_tail(&banned_list, &c->n);
342       num_banned++;
343       is_modify(1, c->addr);
344     }
345
346   culprit_cleanup();
347 }
348
349 static void culprit_timer(struct main_timer *tm UNUSED)
350 {
351   culprit_cleanup();
352 }
353
354 static void fail_init(void)
355 {
356   culprit_init();
357   clist_init(&suspect_list);
358   clist_init(&banned_list);
359   cleanup_timer.handler = culprit_timer;
360 }
361
362 /*** Parsing of log messages ***/
363
364 static bool check_next(char **pp, char *want)
365 {
366   char *p = *pp;
367   while (*want)
368     {
369       if (*p++ != *want++)
370         return 0;
371     }
372   *pp = p;
373   return 1;
374 }
375
376 static void parse_failure(char *p, int cnt)
377 {
378   DBG("Parse 4: <%s> cnt=%d", p, cnt);
379
380   // Decode attributes
381   bool done = 0;
382   char *rhost = NULL;
383   while (!done)
384     {
385       while (*p == ' ')
386         p++;
387       if (!*p)
388         break;
389
390       char *key = p;
391       while (*p && *p != ' ' && *p != '=')
392         p++;
393       if (*p != '=')
394         continue;
395       *p++ = 0;
396
397       char *val = p;
398       while (*p && *p != ' ')
399         p++;
400       if (*p)
401         *p++ = 0;
402       else
403         done = 1;
404
405       DBG("Parse KV: %s=<%s>", key, val);
406       if (!strcmp(key, "rhost"))
407         rhost = val;
408     }
409
410   // Act on the message
411   struct addr addr;
412   if (addr_parse(&addr, rhost))
413     handle_failed_login(addr, cnt);
414   else
415     msg(L_WARN, "Unable to parse address %s", rhost);
416 }
417
418 static void process_msg(char *line)
419 {
420   DBG("Parse: <%s>", line);
421
422   char *p = line;
423   int c;
424   // 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
425   // 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
426
427   // We shall start with 32 non-spaces
428   for (int i=0; i<32; i++)
429     {
430       c = *p++;
431       if (!c || c == ' ')
432         return;
433     }
434   DBG("Parse 1: <%s>", p);
435
436   // Space, something, colon, space
437   if (*p++ != ' ')
438     return;
439   while (*p && *p != ' ' && *p != ':')
440     p++;
441   if (!check_next(&p, ": "))
442     return;
443   DBG("Parse 2: <%s>", p);
444
445   // pam_unix(something), colon, space
446   if (check_next(&p, "pam_unix("))
447     {
448       do
449         {
450           c = *p++;
451           if (!c || c == ' ')
452             return;
453         }
454       while (c != ')');
455       if (!check_next(&p, ": "))
456         return;
457       DBG("Parse 3: <%s>", p);
458
459       if (!check_next(&p, "authentication failure; "))
460         return;
461
462       parse_failure(p, 1);
463     }
464
465   // "PAM <n> more authentication failures;"
466   if (check_next(&p, "PAM "))
467     {
468       if (!(*p >= '0' && *p <= '9'))
469         return;
470       int cnt = atoi(p);
471       while (*p >= '0' && *p <= '9')
472         p++;
473
474       if (!check_next(&p, " more authentication failures; "))
475         return;
476
477       parse_failure(p, cnt);
478     }
479 }
480
481 /*** Socket for receiving messages from rsyslog ***/
482
483 struct main_file sk_file;
484
485 static int sk_read(struct main_file *mf)
486 {
487   char line[1024];
488   int len = recv(mf->fd, line, sizeof(line), MSG_TRUNC);
489   if (len < 0)
490     {
491       if (errno == EINTR || errno == EAGAIN)
492         return HOOK_IDLE;
493       die("recv: %m");
494     }
495
496   if (len >= (int) sizeof(line))
497     {
498       msg(L_WARN, "Truncated message received (length=%d)", len);
499       len = sizeof(line) - 1;
500     }
501   line[len] = 0;
502
503   if (len > 0 && line[len-1] == '\n')
504     line[--len] = 0;
505   if (len > 0 && line[len-1] == '\r')
506     line[--len] = 0;
507
508   process_msg(line);
509   return HOOK_RETRY;
510 }
511
512 static void sk_init(void)
513 {
514   unlink(listen_on);
515   mode_t old_umask = umask(0077);
516
517   int fd;
518   if ((fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0)
519     die("Cannot create PF_UNIX socket: %m");
520
521   struct sockaddr_un sa = { .sun_family = AF_UNIX };
522   strcpy(sa.sun_path, listen_on);
523   if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
524     die("Cannot bind socket %s: %m", listen_on);
525
526   sk_file.fd = fd;
527   sk_file.read_handler = sk_read;
528   file_add(&sk_file);
529
530   umask(old_umask);
531 }
532
533 /*** Main ***/
534
535 static struct opt_section options = {
536   OPT_ITEMS {
537     OPT_HELP("Bouncer -- A Daemon for Turning Away Mischievous Guests"),
538     OPT_HELP(""),
539     OPT_HELP("Options:"),
540     OPT_HELP_OPTION,
541     OPT_CONF_OPTIONS,
542     OPT_END
543   }
544 };
545
546 int main(int argc UNUSED, char **argv)
547 {
548   cf_def_file = "config";       // FIXME
549   cf_declare_section("Bouncer", &bouncer_cf, 0);
550   opt_parse(&options, argv+1);
551
552   if (config_log_stream)
553     log_configured(config_log_stream);
554
555   main_init();
556   is_init();
557   fail_init();
558   sk_init();
559
560   is_flush(ipv4_set);
561   is_flush(ipv6_set);
562
563   msg(L_INFO, "Starting");
564   main_loop();
565   return 0;
566 }