]> mj.ucw.cz Git - bouncer.git/blob - bouncer.c
Use libucw mainloop
[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/string.h>
18
19 #include <alloca.h>
20 #include <arpa/inet.h>
21 #include <errno.h>
22 #include <string.h>
23 #include <sys/socket.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 uns max_culprits = 3;    // FIXME
90 static uns max_failures = 3;    // FIXME
91 static uns max_idle_time = 600; // FIXME
92 static uns max_banned_time = 600;       // FIXME
93
94 /*** An interface to IP sets ***/
95
96 static struct ipset_session *is_sess;
97
98 enum is_index {
99   IS_IPV4,
100   IS_IPV6,
101 };
102
103 static const char * const is_names[] = {
104   "bouncer4",
105   "bouncer6",
106 };
107
108 static const char *trim_eol(const char *msg)
109 {
110   int len = strlen(msg);
111   if (!len || msg[len-1] != '\n')
112     return msg;
113   else
114     {
115       char *x = xstrdup(msg);
116       x[len-1] = 0;
117       return x;
118     }
119 }
120
121 static void is_die(const char *when)
122 {
123   const char *warn = ipset_session_warning(is_sess);
124   if (warn)
125     msg(L_WARN, "%s: %s", when, trim_eol(warn));
126
127   const char *err = ipset_session_error(is_sess);
128   die("%s: %s", when, err ? trim_eol(err) : "Unknown error");
129 }
130
131 static void is_err(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   msg(L_ERROR, "%s: %s", when, err ? trim_eol(err) : "Unknown error");
139
140   ipset_session_report_reset(is_sess);
141 }
142
143 static void is_init(void)
144 {
145   ipset_load_types();
146
147   is_sess = ipset_session_init(printf);
148   if (!is_sess)
149     die("Unable to initialize ipset session");
150 }
151
152 static void is_setup(int set)
153 {
154   if (ipset_parse_setname(is_sess, IPSET_SETNAME, is_names[set]) < 0)
155     is_die("ipset_parse_setname");
156 }
157
158 static void is_flush(int set)
159 {
160   is_setup(set);
161
162   if (ipset_cmd(is_sess, IPSET_CMD_FLUSH, 0) < 0)
163     return is_err("IPSET_CMD_FLUSH");
164 }
165
166 static void is_modify(bool add, struct addr addr)
167 {
168   is_setup(addr_is_v4(addr) ? IS_IPV4 : IS_IPV6);
169   int cmd = add ? IPSET_CMD_ADD : IPSET_CMD_DEL;
170
171   if (ipset_envopt_parse(is_sess, IPSET_ENV_EXIST, NULL) < 0)
172     is_die("IPSET_ENV_EXIST");
173
174   const struct ipset_type *is_type = ipset_type_get(is_sess, cmd);
175   if (!is_type)
176     is_die("ipset_type_get");
177
178   if (is_type->dimension != 1)
179     die("Invalid ipset dimension %d", is_type->dimension);
180
181   char buf[ADDR_BUFSIZE];
182   addr_format(buf, addr);
183   if (ipset_parse_elem(is_sess, 0, buf) < 0)
184     return is_err("ipset_parse_elem");
185
186   if (ipset_cmd(is_sess, cmd, 0) < 0)
187     return is_err(add ? "IPSET_CMD_ADD" : "IPSET_CMD_DEL");
188 }
189
190 /*** Handling of login failures ***/
191
192 struct culprit_node {
193   cnode n;
194   union {
195     struct addr addr;
196     byte addr_bytes[16];
197   };
198   timestamp_t last_fail;
199   uns fail_count;
200   bool banned;
201 };
202
203 #define HASH_NODE struct culprit_node
204 #define HASH_PREFIX(x) culprit_##x
205 #define HASH_KEY_MEMORY addr_bytes
206 #define HASH_KEY_SIZE 16
207 #define HASH_WANT_LOOKUP
208 #define HASH_WANT_REMOVE
209 #define HASH_USE_AUTO_ELTPOOL 1000
210 #define HASH_ZERO_FILL
211 #define HASH_LOOKUP_DETECT_NEW
212 #include <ucw/hashtable.h>
213
214 static uns num_culprits;
215 static clist culprit_lru, culprit_bans;
216 static struct main_timer cleanup_timer;
217
218 static void cleanup_list(clist *list, timestamp_t max_time, timestamp_t *next)
219 {
220   timestamp_t now = main_get_now();
221
222   for (;;)
223     {
224       struct culprit_node *c = clist_head(list);
225       if (!c)
226         break;
227
228       timestamp_t expire_in = c->last_fail + max_time - now;
229       if (num_culprits > max_culprits)
230         expire_in = 0;
231
232       if (expire_in > now)
233         {
234           *next = MIN(*next, expire_in);
235           break;
236         }
237
238       if (c->banned)
239         {
240           DBG("%s: unbanned", AFMT(c->addr));
241           is_modify(0, c->addr);
242         }
243       else
244         {
245           DBG("%s: removed from LRU", AFMT(c->addr));
246         }
247
248       clist_remove(&c->n);
249       culprit_remove(c);
250       num_culprits--;
251     }
252 }
253
254 static void culprit_cleanup(void)
255 {
256   timestamp_t next_cleanup = main_get_now() + (timestamp_t)3600 * 1000;
257   cleanup_list(&culprit_bans, (timestamp_t)max_banned_time * 1000, &next_cleanup);
258   cleanup_list(&culprit_lru, (timestamp_t)max_idle_time * 1000, &next_cleanup);
259   timer_add(&cleanup_timer, next_cleanup);
260 }
261
262 static void handle_failed_login(struct addr addr)
263 {
264   int is_new;
265   timestamp_t now = main_get_now();
266
267   struct culprit_node *c = culprit_lookup((byte *) &addr, &is_new);
268   if (is_new)
269     {
270       c->fail_count = 1;
271       c->banned = 0;
272       clist_add_tail(&culprit_lru, &c->n);
273       num_culprits++;
274       // FIXME: Warn on overflow, but not too frequently
275       DBG("%s: first fail", AFMT(addr));
276     }
277   else if (!c->banned)
278     {
279       c->fail_count++;
280       clist_remove(&c->n);
281       clist_add_tail(&culprit_lru, &c->n);
282       DBG("%s: next fail, cnt=%u", AFMT(addr), c->fail_count);
283     }
284   c->last_fail = now;
285
286   if (!c->banned && c->fail_count >= max_failures)
287     {
288       DBG("%s: banned", AFMT(addr));
289       c->banned = 1;
290       clist_remove(&c->n);
291       clist_add_tail(&culprit_bans, &c->n);
292       is_modify(1, c->addr);
293     }
294 }
295
296 static void culprit_timer(struct main_timer *tm UNUSED)
297 {
298   culprit_cleanup();
299 }
300
301 static void fail_init(void)
302 {
303   culprit_init();
304   clist_init(&culprit_lru);
305   clist_init(&culprit_bans);
306   cleanup_timer.handler = culprit_timer;
307 }
308
309 /*** Parsing of log messages ***/
310
311 static bool check_next(char **pp, char *want)
312 {
313   char *p = *pp;
314   while (*want)
315     {
316       if (*p++ != *want++)
317         return 0;
318     }
319   *pp = p;
320   return 1;
321 }
322
323 static void process_msg(char *line)
324 {
325   DBG("Parse: <%s>", line);
326
327   char *p = line;
328   int c;
329   // 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
330
331   // We shall start with 32 non-spaces
332   for (int i=0; i<32; i++)
333     {
334       c = *p++;
335       if (!c || c == ' ')
336         return;
337     }
338   DBG("Parse 1: <%s>", p);
339
340   // Space, something, colon, space
341   if (*p++ != ' ')
342     return;
343   while (*p && *p != ' ' && *p != ':')
344     p++;
345   if (!check_next(&p, ": "))
346     return;
347   DBG("Parse 2: <%s>", p);
348
349   // pam_unix(something), colon, space (FIXME: make configurable)
350   if (!check_next(&p, "pam_unix("))
351     return;
352   do
353     {
354       c = *p++;
355       if (!c || c == ' ')
356         return;
357     }
358   while (c != ')');
359   if (!check_next(&p, ": "))
360     return;
361   DBG("Parse 3: <%s>", p);
362
363   // "authentication failure;"
364   if (!check_next(&p, "authentication failure; "))
365     return;
366   DBG("Parse 4: <%s>", p);
367
368   // Decode attributes
369   bool done = 0;
370   char *rhost = NULL;
371   while (!done)
372     {
373       while (*p == ' ')
374         p++;
375       if (!*p)
376         break;
377
378       char *key = p;
379       while (*p && *p != ' ' && *p != '=')
380         p++;
381       if (*p != '=')
382         continue;
383       *p++ = 0;
384
385       char *val = p;
386       while (*p && *p != ' ')
387         p++;
388       if (*p)
389         *p++ = 0;
390       else
391         done = 1;
392
393       DBG("Parse KV: %s=<%s>", key, val);
394       if (!strcmp(key, "rhost"))
395         rhost = val;
396     }
397
398   // Act on the message
399   struct addr addr;
400   if (addr_parse(&addr, rhost))
401     handle_failed_login(addr);
402   else
403     msg(L_WARN, "Unable to parse address %s", rhost);
404 }
405
406 /*** Socket for receiving messages from rsyslog ***/
407
408 static const char sk_name[] = "/var/run/bouncer.sock";
409 struct main_file sk_file;
410
411 static int sk_read(struct main_file *mf)
412 {
413   char line[1024];
414   int len = recv(mf->fd, line, sizeof(line), MSG_TRUNC);
415   if (len < 0)
416     {
417       if (errno == EINTR || errno == EAGAIN)
418         return HOOK_IDLE;
419       die("recv: %m");
420     }
421
422   if (len >= (int) sizeof(line))
423     {
424       msg(L_WARN, "Truncated message received (length=%d)", len);
425       len = sizeof(line) - 1;
426     }
427   line[len] = 0;
428
429   if (len > 0 && line[len-1] == '\n')
430     line[--len] = 0;
431   if (len > 0 && line[len-1] == '\r')
432     line[--len] = 0;
433
434   process_msg(line);
435   return HOOK_RETRY;
436 }
437
438 static void sk_init(void)
439 {
440   unlink(sk_name);
441
442   int fd;
443   if ((fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0)
444     die("Cannot create PF_UNIX socket: %m");
445
446   struct sockaddr_un sa = { .sun_family = AF_UNIX };
447   strcpy(sa.sun_path, sk_name);
448   if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
449     die("Cannot bind socket %s: %m", sk_name);
450
451   // FIXME: Permissions
452
453   sk_file.fd = fd;
454   sk_file.read_handler = sk_read;
455   file_add(&sk_file);
456 }
457
458 /*** Main ***/
459
460 int main(void)
461 {
462   main_init();
463   is_init();
464   fail_init();
465
466   msg(L_INFO, "Clearing previous state");
467   is_flush(IS_IPV4);
468   is_flush(IS_IPV6);
469
470   sk_init();
471
472   main_loop();
473   return 0;
474 }