]> mj.ucw.cz Git - bouncer.git/blob - bouncer.c
cedf14c65549a77b64d4badf10f2623dc318054c
[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 timeout 10000 maxelem 100 forceadd
7  *      FIXME: ipset create bouncer6 hash:ip family inet6 timeout 10000 maxelem 100 forceadd
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/stkstring.h>
16 #include <ucw/string.h>
17
18 #include <arpa/inet.h>
19 #include <string.h>
20 #include <sys/socket.h>
21 #include <sys/un.h>
22 #include <time.h>
23 #include <unistd.h>
24
25 #include <libipset/data.h>
26 #include <libipset/session.h>
27 #include <libipset/types.h>
28
29 /*** IP sets ***/
30
31 static struct ipset_session *is_sess;
32
33 enum is_index {
34   IS_IPV4,
35   IS_IPV6,
36 };
37
38 static const char * const is_names[] = {
39   "bouncer4",
40   "bouncer6",
41 };
42
43 static const char *trim_eol(const char *msg)
44 {
45   int len = strlen(msg);
46   if (!len || msg[len-1] != '\n')
47     return msg;
48   else
49     {
50       char *x = xstrdup(msg);
51       x[len-1] = 0;
52       return x;
53     }
54 }
55
56 static void is_die(const char *when)
57 {
58   const char *warn = ipset_session_warning(is_sess);
59   if (warn)
60     msg(L_WARN, "%s: %s", when, trim_eol(warn));
61
62   const char *err = ipset_session_error(is_sess);
63   die("%s: %s", when, err ? trim_eol(err) : "Unknown error");
64 }
65
66 static void is_err(const char *when)
67 {
68   const char *warn = ipset_session_warning(is_sess);
69   if (warn)
70     msg(L_WARN, "%s: %s", when, trim_eol(warn));
71
72   const char *err = ipset_session_error(is_sess);
73   msg(L_ERROR, "%s: %s", when, err ? trim_eol(err) : "Unknown error");
74
75   ipset_session_report_reset(is_sess);
76 }
77
78 static void is_init(void)
79 {
80   ipset_load_types();
81
82   is_sess = ipset_session_init(printf);
83   if (!is_sess)
84     die("Unable to initialize ipset session");
85 }
86
87 static void is_setup(int set)
88 {
89   if (ipset_parse_setname(is_sess, IPSET_SETNAME, is_names[set]) < 0)
90     is_die("ipset_parse_setname");
91 }
92
93 static void is_flush(int set)
94 {
95   is_setup(set);
96
97   if (ipset_cmd(is_sess, IPSET_CMD_FLUSH, 0) < 0)
98     return is_err("IPSET_CMD_FLUSH");
99 }
100
101 static void is_add(int set, const char *elt)
102 {
103   is_setup(set);
104
105   if (ipset_envopt_parse(is_sess, IPSET_ENV_EXIST, NULL) < 0)
106     is_die("IPSET_ENV_EXIST");
107
108   const struct ipset_type *is_type = ipset_type_get(is_sess, IPSET_CMD_ADD);
109   if (!is_type)
110     is_die("ipset_type_get");
111
112   if (is_type->dimension != 1)
113     die("Invalid ipset dimension %d", is_type->dimension);
114
115   if (ipset_parse_elem(is_sess, 0, elt) < 0)
116     return is_err("ipset_parse_elem");
117
118   if (ipset_cmd(is_sess, IPSET_CMD_ADD, 0) < 0)
119     return is_err("IPSET_CMD_ADD");
120 }
121
122 /*** Handling of login failures ***/
123
124 struct addr {
125   u32 a[4];
126 };
127
128 // FIXME: call ntohl
129 #define AFMT(_a) stk_printf("%08x:%08x:%08x:%08x", _a.a[0], _a.a[1], _a.a[2], _a.a[3])
130
131 struct culprit_node {
132   cnode n;
133   union {
134     struct addr addr;
135     byte addr_bytes[16];
136   };
137   time_t first_fail;
138   uns fail_count;
139   bool banned;
140 };
141
142 #define HASH_NODE struct culprit_node
143 #define HASH_PREFIX(x) culprit_##x
144 #define HASH_KEY_MEMORY addr_bytes
145 #define HASH_KEY_SIZE 16
146 #define HASH_WANT_LOOKUP
147 #define HASH_WANT_REMOVE
148 #define HASH_USE_AUTO_ELTPOOL 1000
149 #define HASH_ZERO_FILL
150 #define HASH_LOOKUP_DETECT_NEW
151 #include <ucw/hashtable.h>
152
153 static uns num_culprits;
154 static uns max_culprits = 3;    // FIXME
155 static uns max_failures = 3;    // FIXME
156 static uns max_idle_time = 600; // FIXME
157 static uns max_banned_time = 600;       // FIXME
158 static clist culprit_lru, culprit_bans;
159
160 static void handle_failed_login(struct addr addr)
161 {
162   int is_new;
163   time_t now = time(NULL);
164
165   struct culprit_node *c = culprit_lookup((byte *) &addr, &is_new);
166   if (is_new)
167     {
168       c->first_fail = now;
169       c->fail_count = 1;
170       c->banned = 0;
171       clist_add_tail(&culprit_lru, &c->n);
172       num_culprits++;
173       // FIXME: Warn on overflow, but not too frequently
174       DBG("%s: first fail", AFMT(addr));
175     }
176   else if (!c->banned)
177     {
178       c->fail_count++;
179       clist_remove(&c->n);
180       clist_add_tail(&culprit_lru, &c->n);
181       DBG("%s: next fail, cnt=%u", AFMT(addr), c->fail_count);
182     }
183
184   if (!c->banned && c->fail_count >= max_failures)
185     {
186       DBG("%s: banned", AFMT(addr));
187       c->banned = 1;
188       clist_remove(&c->n);
189       clist_add_tail(&culprit_bans, &c->n);
190     }
191
192   // FIXME: This must be called from main loop, not from here
193   struct culprit_node *d;
194   while ((d = clist_head(&culprit_bans)) && (now - d->first_fail >= max_banned_time || num_culprits > max_culprits))
195     {
196       DBG("%s: unbanned", AFMT(d->addr));
197       clist_remove(&d->n);
198       culprit_remove(d);
199       num_culprits--;
200     }
201   while ((d = clist_head(&culprit_lru)) && (now - d->first_fail >= max_idle_time || num_culprits > max_culprits))
202     {
203       DBG("%s: removing from LRU", AFMT(d->addr));
204       clist_remove(&d->n);
205       culprit_remove(d);
206     }
207
208 #if 0  // FIXME
209   int set = strchr(rhost, ':') ? IS_IPV6 : IS_IPV4;
210
211   msg(L_INFO, "Banning %s", rhost);
212   is_add(set, rhost);
213 #endif
214 }
215
216 static void fail_init(void)
217 {
218   culprit_init();
219   clist_init(&culprit_lru);
220   clist_init(&culprit_bans);
221 }
222
223 /*** Parsing of log messages ***/
224
225 static bool check_next(char **pp, char *want)
226 {
227   char *p = *pp;
228   while (*want)
229     {
230       if (*p++ != *want++)
231         return 0;
232     }
233   *pp = p;
234   return 1;
235 }
236
237 static void parse_failed_login(char *rhost)
238 {
239   struct in_addr a4;
240   struct in6_addr a6;
241   struct addr addr;
242
243   if (inet_pton(AF_INET, rhost, &a4))
244     {
245       addr.a[0] = addr.a[1] = 0;
246       addr.a[2] = 0xffff;
247       addr.a[3] = a4.s_addr;
248       handle_failed_login(addr);
249     }
250   else if (inet_pton(AF_INET6, rhost, &a6))
251     {
252       memcpy(&addr, &a6, 16);
253       handle_failed_login(addr);
254     }
255   else
256     msg(L_WARN, "Unable to parse address %s", rhost);
257 }
258
259 static void process_msg(char *line)
260 {
261   msg(L_DEBUG, "Received <%s>", line);
262
263   char *p = line;
264   int c;
265   // 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
266
267   // We shall start with 32 non-spaces
268   for (int i=0; i<32; i++)
269     {
270       c = *p++;
271       if (!c || c == ' ')
272         return;
273     }
274   DBG("Parse 1: <%s>", p);
275
276   // Space, something, colon, space
277   if (*p++ != ' ')
278     return;
279   while (*p && *p != ' ' && *p != ':')
280     p++;
281   if (!check_next(&p, ": "))
282     return;
283   DBG("Parse 2: <%s>", p);
284
285   // pam_unix(something), colon, space (FIXME: make configurable)
286   if (!check_next(&p, "pam_unix("))
287     return;
288   do
289     {
290       c = *p++;
291       if (!c || c == ' ')
292         return;
293     }
294   while (c != ')');
295   if (!check_next(&p, ": "))
296     return;
297   DBG("Parse 3: <%s>", p);
298
299   // "authentication failure;"
300   if (!check_next(&p, "authentication failure; "))
301     return;
302   DBG("Parse 4: <%s>", p);
303
304   // Decode attributes
305   bool done = 0;
306   char *rhost = NULL;
307   while (!done)
308     {
309       while (*p == ' ')
310         p++;
311       if (!*p)
312         break;
313
314       char *key = p;
315       while (*p && *p != ' ' && *p != '=')
316         p++;
317       if (*p != '=')
318         continue;
319       *p++ = 0;
320
321       char *val = p;
322       while (*p && *p != ' ')
323         p++;
324       if (*p)
325         *p++ = 0;
326       else
327         done = 1;
328
329       DBG("Parse KV: %s=<%s>", key, val);
330       if (!strcmp(key, "rhost"))
331         rhost = val;
332     }
333
334   // Act on the message
335   parse_failed_login(rhost);
336 }
337
338 /*** Socket for receiving messages from rsyslog ***/
339
340 static const char sk_name[] = "/var/run/bouncer.sock";
341 static int sk_fd;
342
343 static void sk_init(void)
344 {
345   unlink(sk_name);
346
347   if ((sk_fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0)
348     die("Cannot create PF_UNIX socket: %m");
349
350   struct sockaddr_un sa = { .sun_family = AF_UNIX };
351   strcpy(sa.sun_path, sk_name);
352   if (bind(sk_fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
353     die("Cannot bind socket %s: %m", sk_name);
354
355   // FIXME: Permissions
356 }
357
358 static void sk_loop(void)
359 {
360   for (;;)
361     {
362       char line[1024];
363       int len = recv(sk_fd, line, sizeof(line), MSG_TRUNC);
364       if (len < 0)
365         die("recv: %m");
366
367       if (len >= (int) sizeof(line))
368         {
369           msg(L_WARN, "Truncated message received (length=%d)", len);
370           len = sizeof(line) - 1;
371         }
372       line[len] = 0;
373
374       if (len > 0 && line[len-1] == '\n')
375         line[--len] = 0;
376       if (len > 0 && line[len-1] == '\r')
377         line[--len] = 0;
378
379       process_msg(line);
380     }
381 }
382
383 /*** Main ***/
384
385 int main(void)
386 {
387   is_init();
388   fail_init();
389
390   msg(L_INFO, "Clearing previous state");
391   is_flush(IS_IPV4);
392   is_flush(IS_IPV6);
393
394   sk_init();
395   sk_loop();
396
397   return 0;
398 }