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