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