Files
tallow/tallow.c
Auke Kok d51577bd4f Add one more preauth disconnect pattern.
This pattern has been recurring a lot recently and does not
get dropped as expected. It is another typical preauth failure.
2018-02-05 11:02:59 -08:00

493 lines
11 KiB
C

/*
* tallow.c - IP block sshd login abuse
*
* (C) Copyright 2015 Intel Corporation
* Authors:
* Auke Kok <auke-jan.h.kok@intel.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 3
* of the License.
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdbool.h>
#include <signal.h>
#include <unistd.h>
#include <limits.h>
#include <sys/time.h>
#include <pcre.h>
#include <systemd/sd-journal.h>
#ifdef DEBUG
#define dbg(args...) fprintf(stderr, ##args)
#else
#define dbg(args...) do {} while (0)
#endif
struct tallow_struct {
char *ip;
float score;
struct timeval time;
struct tallow_struct *next;
bool blocked;
};
static struct tallow_struct *head;
struct whitelist_struct {
char *ip;
size_t len;
struct whitelist_struct *next;
};
static struct whitelist_struct *whitelist;
#define FILTER_STRING "SYSLOG_IDENTIFIER=sshd"
struct pattern_struct {
int instant_block;
float weight;
char *pattern;
pcre *re;
};
#define PATTERN_COUNT 10
static struct pattern_struct patterns[PATTERN_COUNT] = {
{ 0, 0.4, "MESSAGE=Failed .* for .* from ([0-9a-z:.]+) port \\d+ ssh2", NULL},
{ 0, 0.4, "MESSAGE=error: PAM: Authentication failure for .* from ([0-9a-z:.]+)", NULL},
{15, 0.3, "MESSAGE=Invalid user .* from ([0-9a-z:.]+) port \\d+", NULL},
{15, 0.3, "MESSAGE=Did not receive identification string from ([0-9a-z:.]+) port \\d+", NULL},
{30, 0.3, "MESSAGE=Failed .* for root from ([0-9a-z:.]+) port \\d+ ssh2", NULL},
{15, 0.6, "MESSAGE=Bad protocol version identification .* from ([0-9a-z:.]+)", NULL},
{15, 0.6, "MESSAGE=Connection closed by authenticating user .* ([0-9a-z:.]+) port \\d+", NULL},
{15, 0.6, "MESSAGE=Received disconnect from ([0-9a-z:.]+) port .*\\[preauth\\]", NULL},
{15, 0.6, "MESSAGE=Connection closed by ([0-9a-z:.]+) port .*\\[preauth\\]", NULL},
{60, 0.6, "MESSAGE=Unable to negotiate with ([0-9a-z:.]+) port \\d+: no matching key exchange method found.", NULL}
};
#define MAX_OFFSETS 30
static char ipt_path[PATH_MAX];
static int expires = 3600;
static int has_ipv6 = 0;
static bool nocreate = false;
static sd_journal *j;
static int ext(char *fmt, ...)
{
va_list args;
char cmd[1024];
int ret = 0;
va_start(args, fmt);
vsnprintf(cmd, sizeof(cmd), fmt, args);
va_end(args);
ret = system(cmd);
if (ret)
fprintf(stderr, "Error executing \"%s\": returned %d\n", cmd, ret);
return (ret);
}
static void ext_ignore(char *fmt, ...)
{
va_list args;
char cmd[1024];
va_start(args, fmt);
vsnprintf(cmd, sizeof(cmd), fmt, args);
va_end(args);
__attribute__((unused)) int ret = system(cmd);
}
static void setup(void)
{
static bool done = false;
if (done)
return;
done = true;
if (nocreate)
return;
/* init ipset and iptables */
/* delete iptables ref to set before the ipset! */
ext_ignore("%s/iptables -t filter -D INPUT -m set --match-set tallow src -j DROP 2> /dev/null", ipt_path);
ext_ignore("%s/ipset destroy tallow 2> /dev/null", ipt_path);
if (ext("%s/ipset create tallow hash:ip family inet timeout %d", ipt_path, expires)) {
fprintf(stderr, "Unable to create ipv4 ipset.\n");
exit(EXIT_FAILURE);
}
if (ext("%s/iptables -t filter -A INPUT -m set --match-set tallow src -j DROP", ipt_path)) {
fprintf(stderr, "Unable to create iptables rule.\n");
exit(EXIT_FAILURE);
}
if (has_ipv6) {
ext_ignore("%s/ip6tables -t filter -D INPUT -m set --match-set tallow6 src -j DROP 2> /dev/null", ipt_path);
ext_ignore("%s/ipset destroy tallow6 2> /dev/null", ipt_path);
if (ext("%s/ipset create tallow6 hash:ip family inet6 timeout %d", ipt_path, expires)) {
fprintf(stderr, "Unable to create ipv6 ipset.\n");
exit(EXIT_FAILURE);
}
if (ext("%s/ip6tables -t filter -A INPUT -m set --match-set tallow6 src -j DROP", ipt_path)) {
fprintf(stderr, "Unable to create ipt6ables rule.\n");
exit(EXIT_FAILURE);
}
}
}
static void block(struct tallow_struct *s, int instant_block)
{
setup();
if (strchr(s->ip, ':')) {
if (has_ipv6) {
if (instant_block > 0) {
(void) ext("%s/ipset -! add tallow6 %s timeout %d",
ipt_path, s->ip, instant_block);
} else {
(void) ext("%s/ipset -! add tallow6 %s", ipt_path, s->ip);
s->blocked = true;
}
}
} else {
if (instant_block > 0) {
(void) ext("%s/ipset -! add tallow %s timeout %d",
ipt_path, s->ip, instant_block);
} else {
(void) ext("%s/ipset -! add tallow %s", ipt_path, s->ip);
s->blocked = true;
}
}
if (s->blocked) {
fprintf(stderr, "Blocked %s\n", s->ip);
} else {
dbg("Throttled %s\n", s->ip);
}
}
static void whitelist_add(char *ip)
{
struct whitelist_struct *w = whitelist;
struct whitelist_struct *n;
while (w && w->next)
w = w->next;
n = calloc(1, sizeof(struct whitelist_struct));
if (!n) {
fprintf(stderr, "Out of memory.\n");
exit(EXIT_FAILURE);
}
n->ip = strdup(ip);
size_t l = strlen(ip);
if ((ip[l-1] == '.') || (ip[l-1] == ':'))
n->len = l;
else
n->len = -1;
if (!whitelist)
whitelist = n;
else
w->next = n;
}
static void find(const char *ip, float weight, int instant_block)
{
struct tallow_struct *s = head;
struct tallow_struct *n;
struct whitelist_struct *w = whitelist;
if (!ip)
return;
/*
* not validating the IP address format here, just
* making sure we're not passing special characters
* to system().
*/
if (strspn(ip, "0123456789abcdef:.") != strlen(ip))
return;
/* whitelist */
while (w) {
if (w->len > 0) {
if (!strncmp(w->ip, ip, w->len))
return;
} else {
if (!strcmp(w->ip, ip))
return;
}
w = w->next;
}
/* walk and update entry */
while (s) {
if (!strcmp(s->ip, ip)) {
s->score += weight;
dbg("%s: %1.3f\n", s->ip, s->score);
(void) gettimeofday(&s->time, NULL);
if (s->blocked) {
return;
}
if (s->score >= 1.0) {
block(s, 0);
} else if (instant_block > 0) {
block(s, instant_block);
}
return;
}
if (s->next)
s = s->next;
else
break;
}
/* append */
n = calloc(1, sizeof(struct tallow_struct));
if (!n) {
fprintf(stderr, "Out of memory.\n");
exit(EXIT_FAILURE);
}
if (!head)
head = n;
else
s->next = n;
n->ip = strdup(ip);
n->score = weight;
n->next = NULL;
n->blocked = false;
(void) gettimeofday(&n->time, NULL);
dbg("%s: %1.3f\n", n->ip, n->score);
if (weight >= 1.0) {
block(n, 0);
} else if (instant_block > 0) {
block(n, instant_block);
}
return;
}
static void sig(int u __attribute__ ((unused)))
{
fprintf(stderr, "Exiting on request.\n");
sd_journal_close(j);
struct tallow_struct *s = head;
while (s) {
struct tallow_struct *n = NULL;
free(s->ip);
n = s;
s = s->next;
free(n);
}
exit(EXIT_SUCCESS);
}
static void sigusr1(int u __attribute__ ((unused)))
{
fprintf(stderr, "Dumping score list on request:\n");
struct tallow_struct *s = head;
while (s) {
fprintf(stderr, "%ld %s %1.3f\n", s->time.tv_sec, s->ip, s->score);
s = s->next;
}
}
static void prune(void)
{
struct tallow_struct *s = head;
struct tallow_struct *p;
struct timeval tv;
(void) gettimeofday(&tv, NULL);
p = NULL;
while (s) {
/*
* Expire all records, but if they are blocked, make sure to
* expire them *before* the ipset rule expires, otherwise
* you might get an IP to bypass checks.
*/
time_t age = tv.tv_sec - s->time.tv_sec;
if ((age > expires) ||
((s->blocked) && (age > expires / 2))) {
dbg("Expired record for %s\n", s->ip);
if (p) {
p->next = s->next;
free(s->ip);
free(s);
s = p->next;
continue;
} else {
head = s->next;
free(s->ip);
free(s);
s = head;
p = NULL;
continue;
}
}
p = s;
s = s->next;
}
}
int main(void)
{
int r;
FILE *f;
struct sigaction s;
int timeout = 60;
strcpy(ipt_path, "/usr/sbin");
memset(&s, 0, sizeof(struct sigaction));
s.sa_handler = sig;
sigaction(SIGHUP, &s, NULL);
sigaction(SIGTERM, &s, NULL);
sigaction(SIGINT, &s, NULL);
memset(&s, 0, sizeof(struct sigaction));
s.sa_handler = sigusr1;
sigaction(SIGUSR1, &s, NULL);
if (access("/proc/sys/net/ipv6", R_OK | X_OK) == 0)
has_ipv6 = 1;
f = fopen("/etc/tallow.conf", "r");
if (f) {
char buf[256];
char *key;
char *val;
while (fgets(buf, 80, f) != NULL) {
char *c;
c = strchr(buf, '\n');
if (c) *c = 0; /* remove trailing \n */
if (buf[0] == '#')
continue; /* comment line */
key = strtok(buf, "=");
if (!key)
continue;
val = strtok(NULL, "=");
if (!val)
continue;
// todo: filter leading/trailing whitespace
if (!strcmp(key, "ipt_path"))
strncpy(ipt_path, val, PATH_MAX - 1);
if (!strcmp(key, "expires"))
expires = atoi(val);
if (!strcmp(key, "whitelist"))
whitelist_add(val);
if (!strcmp(key, "ipv6"))
has_ipv6 = atoi(val);
if (!strcmp(key, "nocreate"))
nocreate = (atoi(val) == 1);
}
fclose(f);
}
if (!has_ipv6)
fprintf(stdout, "ipv6 support disabled.\n");
if (!whitelist)
whitelist_add("127.0.0.1");
r = sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY);
if (r < 0) {
fprintf(stderr, "Failed to open journal: %s\n", strerror(-r));
exit(EXIT_FAILURE);
}
/* ffwd journal */
sd_journal_add_match(j, FILTER_STRING, 0);
r = sd_journal_seek_tail(j);
sd_journal_wait(j, (uint64_t) 0);
dbg("sd_journal_seek_tail() returned %d\n", r);
while (sd_journal_next(j) != 0)
r++;
dbg("Forwarded through %d items in the journal to reach the end\n", r);
fprintf(stderr, "Started\n");
for (int i = 0; i < PATTERN_COUNT; i++) {
int err;
const char *pcre_err;
patterns[i].re = pcre_compile(patterns[i].pattern, 0, &pcre_err, &err, NULL);
if (!patterns[i].re) {
fprintf(stderr, "PCRE compilation failed. Pattern %d, offset %d: %s\n",
i, err, pcre_err);
exit(EXIT_FAILURE);
}
}
for (;;) {
const void *d;
size_t l;
r = sd_journal_wait(j, (uint64_t) timeout * 1000000);
if (r == SD_JOURNAL_INVALIDATE) {
fprintf(stderr, "Journal was rotated, resetting\n");
sd_journal_seek_tail(j);
}
while (sd_journal_next(j) != 0) {
char *m;
if (sd_journal_get_data(j, "MESSAGE", &d, &l) < 0) {
fprintf(stderr, "Failed to read message field: %s\n", strerror(-r));
continue;
}
m = strndup(d, l+1);
m[l] = '\0';
for (int i = 0; i < PATTERN_COUNT; i++) {
int off[MAX_OFFSETS];
int ret = pcre_exec(patterns[i].re, NULL, m, l, 0, 0, off, MAX_OFFSETS);
if (ret == 2) {
const char *s;
ret = pcre_get_substring(m, off, 2, 1, &s);
if (ret > 0) {
dbg("%s == %s\n", s, patterns[i].pattern);
find(s, patterns[i].weight, patterns[i].instant_block);
pcre_free_substring(s);
}
}
}
free(m);
}
prune();
}
sd_journal_close(j);
exit(EXIT_SUCCESS);
}