2 * Bouncer -- A Daemon for Turning Away Mischievous Guests
4 * (c) 2016 Martin Mares <mj@ucw.cz>
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
14 #include <ucw/clists.h>
15 #include <ucw/stkstring.h>
16 #include <ucw/string.h>
18 #include <arpa/inet.h>
20 #include <sys/socket.h>
25 #include <libipset/data.h>
26 #include <libipset/session.h>
27 #include <libipset/types.h>
31 static struct ipset_session *is_sess;
38 static const char * const is_names[] = {
43 static const char *trim_eol(const char *msg)
45 int len = strlen(msg);
46 if (!len || msg[len-1] != '\n')
50 char *x = xstrdup(msg);
56 static void is_die(const char *when)
58 const char *warn = ipset_session_warning(is_sess);
60 msg(L_WARN, "%s: %s", when, trim_eol(warn));
62 const char *err = ipset_session_error(is_sess);
63 die("%s: %s", when, err ? trim_eol(err) : "Unknown error");
66 static void is_err(const char *when)
68 const char *warn = ipset_session_warning(is_sess);
70 msg(L_WARN, "%s: %s", when, trim_eol(warn));
72 const char *err = ipset_session_error(is_sess);
73 msg(L_ERROR, "%s: %s", when, err ? trim_eol(err) : "Unknown error");
75 ipset_session_report_reset(is_sess);
78 static void is_init(void)
82 is_sess = ipset_session_init(printf);
84 die("Unable to initialize ipset session");
87 static void is_setup(int set)
89 if (ipset_parse_setname(is_sess, IPSET_SETNAME, is_names[set]) < 0)
90 is_die("ipset_parse_setname");
93 static void is_flush(int set)
97 if (ipset_cmd(is_sess, IPSET_CMD_FLUSH, 0) < 0)
98 return is_err("IPSET_CMD_FLUSH");
101 static void is_add(int set, const char *elt)
105 if (ipset_envopt_parse(is_sess, IPSET_ENV_EXIST, NULL) < 0)
106 is_die("IPSET_ENV_EXIST");
108 const struct ipset_type *is_type = ipset_type_get(is_sess, IPSET_CMD_ADD);
110 is_die("ipset_type_get");
112 if (is_type->dimension != 1)
113 die("Invalid ipset dimension %d", is_type->dimension);
115 if (ipset_parse_elem(is_sess, 0, elt) < 0)
116 return is_err("ipset_parse_elem");
118 if (ipset_cmd(is_sess, IPSET_CMD_ADD, 0) < 0)
119 return is_err("IPSET_CMD_ADD");
122 /*** Handling of login failures ***/
129 #define AFMT(_a) stk_printf("%08x:%08x:%08x:%08x", _a.a[0], _a.a[1], _a.a[2], _a.a[3])
131 struct culprit_node {
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>
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;
160 static void handle_failed_login(struct addr addr)
163 time_t now = time(NULL);
165 struct culprit_node *c = culprit_lookup((byte *) &addr, &is_new);
171 clist_add_tail(&culprit_lru, &c->n);
173 // FIXME: Warn on overflow, but not too frequently
174 DBG("%s: first fail", AFMT(addr));
180 clist_add_tail(&culprit_lru, &c->n);
181 DBG("%s: next fail, cnt=%u", AFMT(addr), c->fail_count);
184 if (!c->banned && c->fail_count >= max_failures)
186 DBG("%s: banned", AFMT(addr));
189 clist_add_tail(&culprit_bans, &c->n);
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))
196 DBG("%s: unbanned", AFMT(d->addr));
201 while ((d = clist_head(&culprit_lru)) && (now - d->first_fail >= max_idle_time || num_culprits > max_culprits))
203 DBG("%s: removing from LRU", AFMT(d->addr));
209 int set = strchr(rhost, ':') ? IS_IPV6 : IS_IPV4;
211 msg(L_INFO, "Banning %s", rhost);
216 static void fail_init(void)
219 clist_init(&culprit_lru);
220 clist_init(&culprit_bans);
223 /*** Parsing of log messages ***/
225 static bool check_next(char **pp, char *want)
237 static void parse_failed_login(char *rhost)
243 if (inet_pton(AF_INET, rhost, &a4))
245 addr.a[0] = addr.a[1] = 0;
247 addr.a[3] = a4.s_addr;
248 handle_failed_login(addr);
250 else if (inet_pton(AF_INET6, rhost, &a6))
252 memcpy(&addr, &a6, 16);
253 handle_failed_login(addr);
256 msg(L_WARN, "Unable to parse address %s", rhost);
259 static void process_msg(char *line)
261 msg(L_DEBUG, "Received <%s>", line);
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
267 // We shall start with 32 non-spaces
268 for (int i=0; i<32; i++)
274 DBG("Parse 1: <%s>", p);
276 // Space, something, colon, space
279 while (*p && *p != ' ' && *p != ':')
281 if (!check_next(&p, ": "))
283 DBG("Parse 2: <%s>", p);
285 // pam_unix(something), colon, space (FIXME: make configurable)
286 if (!check_next(&p, "pam_unix("))
295 if (!check_next(&p, ": "))
297 DBG("Parse 3: <%s>", p);
299 // "authentication failure;"
300 if (!check_next(&p, "authentication failure; "))
302 DBG("Parse 4: <%s>", p);
315 while (*p && *p != ' ' && *p != '=')
322 while (*p && *p != ' ')
329 DBG("Parse KV: %s=<%s>", key, val);
330 if (!strcmp(key, "rhost"))
334 // Act on the message
335 parse_failed_login(rhost);
338 /*** Socket for receiving messages from rsyslog ***/
340 static const char sk_name[] = "/var/run/bouncer.sock";
343 static void sk_init(void)
347 if ((sk_fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0)
348 die("Cannot create PF_UNIX socket: %m");
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);
355 // FIXME: Permissions
358 static void sk_loop(void)
363 int len = recv(sk_fd, line, sizeof(line), MSG_TRUNC);
367 if (len >= (int) sizeof(line))
369 msg(L_WARN, "Truncated message received (length=%d)", len);
370 len = sizeof(line) - 1;
374 if (len > 0 && line[len-1] == '\n')
376 if (len > 0 && line[len-1] == '\r')
390 msg(L_INFO, "Clearing previous state");