04dcf56e48821f38dd37cc82cb5f74c2da41a1c1
[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);
81 static void delete_iph(iphist_t *ip);
82
83 static
84 void
85 block_ip(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 *reason) {
145         char ips[128];
146         int n1;
147         int n2;
148         int n3;
149         int n4;
150
151         if (sscanf(str, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) {
152                 snprintf(ips, sizeof(ips), "%d.%d.%d.%d", n1, n2, n3, n4);
153                 if (insert_iph(ips)) {
154                         syslog(LOG_ERR,
155                                "Detected ssh password login attempt "
156                                "for %s, locking out %s\n",
157                                reason, ips);
158                         block_ip(ips);
159                 }
160         }
161 }
162
163 static
164 void
165 checkline(char *buf)
166 {
167         char *str;
168
169         /*
170          * ssh login attempt with password (only hit if ssh allows
171          * password entry).  Root or admin.
172          */
173         if ((str = strstr(buf, "Failed password for root from")) != NULL ||
174             (str = strstr(buf, "Failed password for admin from")) != NULL) {
175                 while (*str && (*str < '0' || *str > '9'))
176                         ++str;
177                 checkip(str, "root or admin");
178                 return;
179         }
180
181         /*
182          * ssh login attempt with password (only hit if ssh allows password
183          * entry).  Non-existant user.
184          */
185         if ((str = strstr(buf, "Failed password for invalid user")) != NULL) {
186                 str += 32;
187                 while (*str == ' ')
188                         ++str;
189                 while (*str && *str != ' ')
190                         ++str;
191                 if (strncmp(str, " from", 5) == 0) {
192                         checkip(str + 5, "an invalid user"); 
193                 }
194                 return;
195         }
196
197         /*
198          * Premature disconnect in pre-authorization phase, typically an
199          * attack but require 5 attempts in an hour before cleaning it out.
200          */
201         if ((str = strstr(buf, "Received disconnect from ")) != NULL &&
202             strstr(buf, "[preauth]") != NULL) {
203                 checkip(str + 25, "an inalid user");
204                 return;
205         }
206 }
207
208 /*
209  * Insert IP record
210  */
211 static
212 int
213 insert_iph(const char *ips)
214 {
215         iphist_t *ip = malloc(sizeof(*ip));
216         iphist_t *scan;
217         time_t t = time(NULL);
218         int found;
219
220         ip->hv = iphash(ips);
221         ip->ips = strdup(ips);
222         ip->t = t;
223
224         ip->hnext = hist_hash[ip->hv & HMASK];
225         hist_hash[ip->hv & HMASK] = ip;
226         ip->next = NULL;
227         *hist_tail = ip;
228         hist_tail = &ip->next;
229         ++hist_count;
230
231         /*
232          * hysteresis
233          */
234         if (hist_count > MAXHIST + 16) {
235                 while (hist_count > MAXHIST)
236                         delete_iph(hist_base);
237         }
238
239         /*
240          * Check limit
241          */
242         found = 0;
243         for (scan = hist_hash[ip->hv & HMASK]; scan; scan = scan->hnext) {
244                 if (scan->hv == ip->hv && strcmp(scan->ips, ip->ips) == 0) {
245                         int dt = (int)(t - ip->t);
246                         if (dt < 60 * 60) {
247                                 ++found;
248                                 if (found > SSHLIMIT)
249                                         break;
250                         }
251                 }
252         }
253         return (found > SSHLIMIT);
254 }
255
256 /*
257  * Delete an ip record.  Note that we always delete from the head of the
258  * list, but we will still wind up scanning hash chains.
259  */
260 static
261 void
262 delete_iph(iphist_t *ip)
263 {
264         iphist_t **scanp;
265         iphist_t *scan;
266
267         scanp = &hist_base;
268         while ((scan = *scanp) != ip) {
269                 scanp = &scan->next;
270         }
271         *scanp = ip->next;
272         if (hist_tail == &ip->next)
273                 hist_tail = scanp;
274
275         scanp = &hist_hash[ip->hv & HMASK];
276         while ((scan = *scanp) != ip) {
277                 scanp = &scan->hnext;
278         }
279         *scanp = ip->hnext;
280
281         --hist_count;
282         free(ip);
283 }
284
285 static
286 void
287 init_iphist(void) {
288         hist_base = NULL;
289         hist_tail = &hist_base;
290         for (int i = 0; i < HSIZE; i++) {
291                 hist_hash[i] = NULL;
292         }
293         hist_count = 0;
294 }