ccd8082279d8f82b02f28e4fa81793c67de324b3
[dragonfly.git] / usr.sbin / sshlockout / sshlockout.c
1 /*
2  * Copyright (c) 2015 The DragonFly Project.  All rights reserved.
3  *
4  * This code is derived from software contributed to The DragonFly Project
5  * by Matthew Dillon <dillon@dragonflybsd.org>
6  * by Venkatesh Srinivas <vsrinivas@dragonflybsd.org>
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions
10  * are met:
11  *
12  * 1. Redistributions of source code must retain the above copyright
13  *    notice, this list of conditions and the following disclaimer.
14  * 2. Redistributions in binary form must reproduce the above copyright
15  *    notice, this list of conditions and the following disclaimer in
16  *    the documentation and/or other materials provided with the
17  *    distribution.
18  * 3. Neither the name of The DragonFly Project nor the names of its
19  *    contributors may be used to endorse or promote products derived
20  *    from this software without specific, prior written permission.
21  *
22  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
26  * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27  * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
28  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
30  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
32  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33  * SUCH DAMAGE.
34  */
35 /*
36  * Use: pipe syslog auth output to this program.
37  *
38  * Detects failed ssh login attempts and maps out the originating IP and
39  * issues adds to a PF table <lockout> using 'pfctl -tlockout -Tadd' commands.
40  *
41  * /etc/syslog.conf line example:
42  *      auth.info;authpriv.info         |exec /usr/sbin/sshlockout lockout
43  *
44  * Also suggest a cron entry to clean out the PF table at least once a day.
45  *      3 3 * * *       pfctl -tlockout -Tflush
46  */
47
48 #include <sys/types.h>
49 #include <sys/time.h>
50 #include <stdio.h>
51 #include <stdlib.h>
52 #include <unistd.h>
53 #include <string.h>
54 #include <stdarg.h>
55 #include <syslog.h>
56
57 typedef struct iphist {
58         struct iphist *next;
59         struct iphist *hnext;
60         char    *ips;
61         time_t  t;
62         int     hv;
63 } iphist_t;
64
65 #define HSIZE           1024
66 #define HMASK           (HSIZE - 1)
67 #define MAXHIST         100
68 #define SSHLIMIT        5               /* per hour */
69 #define MAX_TABLE_NAME  20              /* PF table name limit */
70
71 static iphist_t *hist_base;
72 static iphist_t **hist_tail = &hist_base;
73 static iphist_t *hist_hash[HSIZE];
74 static int hist_count = 0;
75
76 static char *pftable = NULL;
77
78 static void init_iphist(void);
79 static void checkline(char *buf);
80 static int insert_iph(const char *ips, time_t t);
81 static void delete_iph(iphist_t *ip);
82
83 static
84 void
85 block_ip(const char *ips) {
86         char buf[128];
87         int r = snprintf(buf, sizeof(buf),
88                          "pfctl -t%s -Tadd %s", pftable, ips);
89         if ((int)strlen(buf) == r) {
90                 system(buf);
91         }
92         else {
93                 syslog(LOG_ERR, "sshlockout: command size overflow");
94         }
95 }
96
97 /*
98  * Stupid simple string hash
99  */
100 static __inline
101 int
102 iphash(const char *str)
103 {
104         int hv = 0xA1B3569D;
105         while (*str) {
106                 hv = (hv << 5) ^ *str ^ (hv >> 23);
107                 ++str;
108         }
109         return hv;
110 }
111
112 int
113 main(int ac, char **av)
114 {
115         char buf[1024];
116
117         init_iphist();
118
119         if (ac == 2 && av[1] != NULL &&
120             strlen(av[1]) > 0 && strlen(av[1]) < MAX_TABLE_NAME) {
121                 pftable = av[1];
122         }
123         else {
124                 syslog(LOG_ERR, "sshlockout: invalid argument");
125                 return(1);
126         }
127
128         openlog("sshlockout", LOG_PID|LOG_CONS, LOG_AUTH);
129         syslog(LOG_ERR, "sshlockout starting up");
130         freopen("/dev/null", "w", stdout);
131         freopen("/dev/null", "w", stderr);
132
133         while (fgets(buf, sizeof(buf), stdin) != NULL) {
134                 if (strstr(buf, "sshd") == NULL)
135                         continue;
136                 checkline(buf);
137         }
138         syslog(LOG_ERR, "sshlockout exiting");
139         return(0);
140 }
141
142 static
143 void
144 checkip(const char *str, const char *reason1, const char *reason2) {
145         char ips[128];
146         int n1;
147         int n2;
148         int n3;
149         int n4;
150         time_t t = time(NULL);
151
152         ips[0] = '\0';
153
154         if (sscanf(str, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) {
155                 snprintf(ips, sizeof(ips), "%d.%d.%d.%d", n1, n2, n3, n4);
156         }
157         // TODO: Check for IPv6 address
158
159         if (strlen(ips) > 0) {
160
161                 /*
162                  * Check for DoS attack. When connections from too many
163                  * IP addresses come in at the same time, our hash table
164                  * would overflow, so we delete the oldest entries AND
165                  * block it's IP when they are younger than 10 seconds.
166                  * This prevents massive attacks from arbitrary IPs.
167                  */
168                 if (hist_count > MAXHIST + 16) {
169                         while (hist_count > MAXHIST) {
170                                 iphist_t *iph = hist_base;
171                                 int dt = (int)(t - iph->t);
172                                 if (dt < 10) {
173                                         syslog(LOG_ERR,
174                                                "Detected overflow attack, "
175                                                "locking out %s\n",
176                                                iph->ips);
177                                         block_ip(iph->ips);
178                                 }
179                                 delete_iph(iph);
180                         }
181                 }
182
183                 if (insert_iph(ips, t)) {
184                         syslog(LOG_ERR,
185                                "Detected ssh %s attempt "
186                                "for %s, locking out %s\n",
187                                reason1, reason2, ips);
188                         block_ip(ips);
189                 }
190         }
191 }
192
193 static
194 void
195 checkline(char *buf)
196 {
197         char *str;
198
199         /*
200          * ssh login attempt with password (only hit if ssh allows
201          * password entry).  Root or admin.
202          */
203         if ((str = strstr(buf, "Failed password for root from")) != NULL ||
204             (str = strstr(buf, "Failed password for admin from")) != NULL) {
205                 while (*str && (*str < '0' || *str > '9'))
206                         ++str;
207                 checkip(str, "password login", "root or admin");
208                 return;
209         }
210
211         /*
212          * ssh login attempt with password (only hit if ssh allows password
213          * entry).  Non-existant user.
214          */
215         if ((str = strstr(buf, "Failed password for invalid user")) != NULL) {
216                 str += 32;
217                 while (*str == ' ')
218                         ++str;
219                 while (*str && *str != ' ')
220                         ++str;
221                 if (strncmp(str, " from", 5) == 0) {
222                         checkip(str + 5, "password login", "an invalid user");
223                 }
224                 return;
225         }
226
227         /*
228          * ssh login attempt for non-existant user.
229          */
230         if ((str = strstr(buf, "Invalid user")) != NULL) {
231                 str += 12;
232                 while (*str == ' ')
233                         ++str;
234                 while (*str && *str != ' ')
235                         ++str;
236                 if (strncmp(str, " from", 5) == 0) {
237                         checkip(str + 5, "login", "an invalid user");
238                 }
239                 return;
240         }
241
242         /*
243          * Premature disconnect in pre-authorization phase, typically an
244          * attack but require 5 attempts in an hour before cleaning it out.
245          */
246         if ((str = strstr(buf, "Received disconnect from ")) != NULL &&
247             strstr(buf, "[preauth]") != NULL) {
248                 checkip(str + 25, "preauth", "an invalid user");
249                 return;
250         }
251 }
252
253 /*
254  * Insert IP record
255  */
256 static
257 int
258 insert_iph(const char *ips, time_t t)
259 {
260         iphist_t *ip = malloc(sizeof(*ip));
261         iphist_t *scan;
262         int found;
263
264         ip->hv = iphash(ips);
265         ip->ips = strdup(ips);
266         ip->t = t;
267
268         ip->hnext = hist_hash[ip->hv & HMASK];
269         hist_hash[ip->hv & HMASK] = ip;
270         ip->next = NULL;
271         *hist_tail = ip;
272         hist_tail = &ip->next;
273         ++hist_count;
274
275         /*
276          * hysteresis
277          */
278         if (hist_count > MAXHIST + 16) {
279                 while (hist_count > MAXHIST)
280                         delete_iph(hist_base);
281         }
282
283         /*
284          * Check limit
285          */
286         found = 0;
287         for (scan = hist_hash[ip->hv & HMASK]; scan; scan = scan->hnext) {
288                 if (scan->hv == ip->hv && strcmp(scan->ips, ip->ips) == 0) {
289                         int dt = (int)(t - ip->t);
290                         if (dt < 60 * 60) {
291                                 ++found;
292                                 if (found > SSHLIMIT)
293                                         break;
294                         }
295                 }
296         }
297         return (found > SSHLIMIT);
298 }
299
300 /*
301  * Delete an ip record.  Note that we always delete from the head of the
302  * list, but we will still wind up scanning hash chains.
303  */
304 static
305 void
306 delete_iph(iphist_t *ip)
307 {
308         iphist_t **scanp;
309         iphist_t *scan;
310
311         scanp = &hist_base;
312         while ((scan = *scanp) != ip) {
313                 scanp = &scan->next;
314         }
315         *scanp = ip->next;
316         if (hist_tail == &ip->next)
317                 hist_tail = scanp;
318
319         scanp = &hist_hash[ip->hv & HMASK];
320         while ((scan = *scanp) != ip) {
321                 scanp = &scan->hnext;
322         }
323         *scanp = ip->hnext;
324
325         --hist_count;
326         free(ip);
327 }
328
329 static
330 void
331 init_iphist(void) {
332         hist_base = NULL;
333         hist_tail = &hist_base;
334         for (int i = 0; i < HSIZE; i++) {
335                 hist_hash[i] = NULL;
336         }
337         hist_count = 0;
338 }