]> mj.ucw.cz Git - bouncer.git/blob - bouncer.c
make release: Reorganization of directory structure
[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 suspect_time = 86400;
92 static uns banned_time = 86400;
93 static uns max_probation = ~0U;
94 static double banned_again_coeff = 2;
95 static uns max_banned_time = 86400;
96 static uns probation_time = 86400;
97 static uns max_culprits = ~0U;
98 static char *config_log_stream;
99 static char *ipv4_set;
100 static char *ipv6_set;
101
102 static struct cf_section bouncer_cf = {
103   CF_ITEMS {
104     CF_STRING("ListenOn", &listen_on),
105     CF_UNS("MaxFailures", &max_failures),
106     CF_UNS("SuspectTime", &suspect_time),
107     CF_UNS("BannedTime", &banned_time),
108     CF_UNS("MaxProbation", &max_probation),
109     CF_DOUBLE("BannedAgainCoeff", &banned_again_coeff),
110     CF_UNS("MaxBannedTime", &max_banned_time),
111     CF_UNS("ProbationTime", &probation_time),
112     CF_UNS("MaxCulprits", &max_culprits),
113     CF_STRING("LogStream", &config_log_stream),
114     CF_STRING("IPv4Set", &ipv4_set),
115     CF_STRING("IPv6Set", &ipv6_set),
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 enum culprit_state {
224   STATE_INVALID,
225   STATE_SUSPECT,
226   STATE_BANNED,
227   STATE_PROBATION,
228 };
229
230 static const char * const culprit_states[] = {
231   "invalid",
232   "suspect",
233   "banned",
234   "probation",
235 };
236
237 struct culprit {
238   union {
239     struct addr addr;
240     byte addr_bytes[16];
241   };
242   enum culprit_state state;
243   uns fail_count;
244   uns sentence_count;
245   uns ban_time;
246   struct main_timer timer;
247 };
248
249 static uns num_culprits;
250 static void culprit_timer(struct main_timer *tm);
251
252 static void culprit_hash_init_data(struct culprit *c)
253 {
254   c->state = STATE_SUSPECT;
255   c->fail_count = 0;
256   bzero(&c->timer, sizeof(c->timer));
257   c->timer.handler = culprit_timer;
258   c->timer.data = c;
259   num_culprits++;
260 }
261
262 #define HASH_NODE struct culprit
263 #define HASH_PREFIX(x) culprit_hash_##x
264 #define HASH_KEY_MEMORY addr_bytes
265 #define HASH_KEY_SIZE 16
266 #define HASH_GIVE_INIT_DATA
267 #define HASH_WANT_LOOKUP
268 #define HASH_WANT_REMOVE
269 #define HASH_USE_AUTO_ELTPOOL 1000
270 #define HASH_ZERO_FILL
271 #include <ucw/hashtable.h>
272
273 static void culprit_delete(struct culprit *c)
274 {
275   timer_del(&c->timer);
276   culprit_hash_remove(c);
277   num_culprits--;
278 }
279
280 static void culprit_set_state(struct culprit *c, enum culprit_state state, uns timeout)
281 {
282   c->state = state;
283   timer_add_rel(&c->timer, (timestamp_t)timeout * 1000);
284   msg(L_DEBUG, "Suspect %s: state=%s failures=%u timeout=%u", AFMT(c->addr), culprit_states[state], c->fail_count, timeout);
285 }
286
287 static void handle_failed_login(struct addr addr, int cnt)
288 {
289   timestamp_t now = main_get_now();
290
291   struct culprit *c = culprit_hash_lookup((byte *) &addr);
292
293   if (num_culprits > max_culprits)
294     {
295       // This can happen only when this lookup created a new culprit.
296       static timestamp_t last_overflow_warning;
297       if (last_overflow_warning + 60000 < now)
298         {
299           last_overflow_warning = now;
300           msg(L_WARN, "Too many culprits, dropping some. Try increasing MaxCulprits.");
301           culprit_delete(c);
302           return;
303         }
304     }
305
306   switch (c->state)
307     {
308     case STATE_SUSPECT:
309       c->fail_count += cnt;
310       if (c->fail_count > max_failures)
311         {
312           c->ban_time = banned_time;
313           msg(L_INFO, "Banned %s: failures=%u ban_time=%u", AFMT(addr), c->fail_count, c->ban_time);
314           c->sentence_count = 1;
315           culprit_set_state(c, STATE_BANNED, c->ban_time);
316           is_modify(1, c->addr);
317         }
318       else
319         culprit_set_state(c, STATE_SUSPECT, suspect_time);
320       break;
321     case STATE_BANNED:
322       break;
323     case STATE_PROBATION:
324       c->fail_count += cnt;
325       if (c->fail_count > max_probation)
326         {
327           c->sentence_count++;
328           c->ban_time = MIN((uns)(c->ban_time * banned_again_coeff), max_banned_time);
329           msg(L_INFO, "Re-banned %s: failures=%u sentences=%u ban_time=%u", AFMT(c->addr), c->fail_count, c->sentence_count, c->ban_time);
330           culprit_set_state(c, STATE_BANNED, c->ban_time);
331           is_modify(1, c->addr);
332         }
333       else
334         culprit_set_state(c, STATE_PROBATION, probation_time);
335       break;
336     default:
337       ASSERT(0);
338     }
339 }
340
341 static void culprit_timer(struct main_timer *tm)
342 {
343   struct culprit *c = tm->data;
344
345   switch (c->state)
346     {
347     case STATE_SUSPECT:
348     case STATE_PROBATION:
349       msg(L_DEBUG, "Suspect %s: acquitted", AFMT(c->addr));
350       culprit_delete(c);
351       break;
352     case STATE_BANNED:
353       msg(L_INFO, "Unbanned %s", AFMT(c->addr));
354       c->fail_count = 0;
355       culprit_set_state(c, STATE_PROBATION, probation_time);
356       is_modify(0, c->addr);
357       break;
358     default:
359       ASSERT(0);
360     }
361 }
362
363 /*** Parsing of log messages ***/
364
365 static bool check_next(char **pp, char *want)
366 {
367   char *p = *pp;
368   while (*want)
369     {
370       if (*p++ != *want++)
371         return 0;
372     }
373   *pp = p;
374   return 1;
375 }
376
377 static void parse_failure(char *p, int cnt)
378 {
379   DBG("Parse 4: <%s> cnt=%d", p, cnt);
380
381   // Decode attributes
382   bool done = 0;
383   char *rhost = NULL;
384   while (!done)
385     {
386       while (*p == ' ')
387         p++;
388       if (!*p)
389         break;
390
391       char *key = p;
392       while (*p && *p != ' ' && *p != '=')
393         p++;
394       if (*p != '=')
395         continue;
396       *p++ = 0;
397
398       char *val = p;
399       while (*p && *p != ' ')
400         p++;
401       if (*p)
402         *p++ = 0;
403       else
404         done = 1;
405
406       DBG("Parse KV: %s=<%s>", key, val);
407       if (!strcmp(key, "rhost"))
408         rhost = val;
409     }
410
411   if (!rhost || !rhost[0])
412     return;
413
414   // Act on the message
415   struct addr addr;
416   if (addr_parse(&addr, rhost))
417     handle_failed_login(addr, cnt);
418   else
419     msg(L_WARN, "Unable to parse address %s", rhost);
420 }
421
422 static void process_msg(char *line)
423 {
424   DBG("Parse: <%s>", line);
425
426   char *p = line;
427   int c;
428   // 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
429   // 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
430
431   // We shall start with 32 non-spaces
432   for (int i=0; i<32; i++)
433     {
434       c = *p++;
435       if (!c || c == ' ')
436         return;
437     }
438   DBG("Parse 1: <%s>", p);
439
440   // Space, something, colon, space
441   if (*p++ != ' ')
442     return;
443   while (*p && *p != ' ' && *p != ':')
444     p++;
445   if (!check_next(&p, ": "))
446     return;
447   DBG("Parse 2: <%s>", p);
448
449   // pam_unix(something), colon, space
450   if (check_next(&p, "pam_unix("))
451     {
452       do
453         {
454           c = *p++;
455           if (!c || c == ' ')
456             return;
457         }
458       while (c != ')');
459       if (!check_next(&p, ": "))
460         return;
461       DBG("Parse 3: <%s>", p);
462
463       if (!check_next(&p, "authentication failure; "))
464         return;
465
466       parse_failure(p, 1);
467     }
468
469   // "PAM <n> more authentication failures;"
470   if (check_next(&p, "PAM "))
471     {
472       if (!(*p >= '0' && *p <= '9'))
473         return;
474       int cnt = atoi(p);
475       while (*p >= '0' && *p <= '9')
476         p++;
477
478       if (!check_next(&p, " more authentication failures; "))
479         return;
480
481       parse_failure(p, cnt);
482     }
483 }
484
485 /*** Socket for receiving messages from rsyslog ***/
486
487 struct main_file sk_file;
488
489 static int sk_read(struct main_file *mf)
490 {
491   char line[1024];
492   int len = recv(mf->fd, line, sizeof(line), MSG_TRUNC);
493   if (len < 0)
494     {
495       if (errno == EINTR || errno == EAGAIN)
496         return HOOK_IDLE;
497       die("recv: %m");
498     }
499
500   if (len >= (int) sizeof(line))
501     {
502       msg(L_WARN, "Truncated message received (length=%d)", len);
503       len = sizeof(line) - 1;
504     }
505   line[len] = 0;
506
507   if (len > 0 && line[len-1] == '\n')
508     line[--len] = 0;
509   if (len > 0 && line[len-1] == '\r')
510     line[--len] = 0;
511
512   process_msg(line);
513   return HOOK_RETRY;
514 }
515
516 static void sk_init(void)
517 {
518   unlink(listen_on);
519   mode_t old_umask = umask(0077);
520
521   int fd;
522   if ((fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0)
523     die("Cannot create PF_UNIX socket: %m");
524
525   struct sockaddr_un sa = { .sun_family = AF_UNIX };
526   strcpy(sa.sun_path, listen_on);
527   if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
528     die("Cannot bind socket %s: %m", listen_on);
529
530   sk_file.fd = fd;
531   sk_file.read_handler = sk_read;
532   file_add(&sk_file);
533
534   umask(old_umask);
535 }
536
537 /*** Main ***/
538
539 static struct opt_section options = {
540   OPT_ITEMS {
541     OPT_HELP("Bouncer -- A Daemon for Turning Away Mischievous Guests"),
542     OPT_HELP(""),
543     OPT_HELP("Options:"),
544     OPT_HELP_OPTION,
545     OPT_CONF_OPTIONS,
546     OPT_END
547   }
548 };
549
550 int main(int argc UNUSED, char **argv)
551 {
552   cf_def_file = "/etc/bouncer";
553   cf_declare_section("Bouncer", &bouncer_cf, 0);
554   opt_parse(&options, argv+1);
555
556   if (config_log_stream)
557     log_configured(config_log_stream);
558
559   main_init();
560   is_init();
561   culprit_hash_init();
562   sk_init();
563
564   is_flush(ipv4_set);
565   is_flush(ipv6_set);
566
567   msg(L_INFO, "Starting");
568   main_loop();
569   return 0;
570 }