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