sshlockout - use a PF table instead of IPFW
[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
70 static iphist_t *hist_base;
71 static iphist_t **hist_tail = &hist_base;
72 static iphist_t *hist_hash[HSIZE];
73 static int hist_count = 0;
74
75 static char *pftable = NULL;
76
77 static void init_iphist(void);
78 static void checkline(char *buf);
79 static int insert_iph(const char *ips);
80 static void delete_iph(iphist_t *ip);
81
82 /*
83  * Stupid simple string hash
84  */
85 static __inline
86 int
87 iphash(const char *str)
88 {
89         int hv = 0xA1B3569D;
90         while (*str) {
91                 hv = (hv << 5) ^ *str ^ (hv >> 23);
92                 ++str;
93         }
94         return hv;
95 }
96
97 int
98 main(int ac, char **av)
99 {
100         char buf[1024];
101
102         init_iphist();
103
104         if (ac == 2 && av[1] != NULL) {
105                 pftable = av[1];
106         }
107         else {
108                 syslog(LOG_ERR, "sshlockout: invalid argument");
109                 return(1);
110         }
111
112         openlog("sshlockout", LOG_PID|LOG_CONS, LOG_AUTH);
113         syslog(LOG_ERR, "sshlockout starting up");
114         freopen("/dev/null", "w", stdout);
115         freopen("/dev/null", "w", stderr);
116
117         while (fgets(buf, sizeof(buf), stdin) != NULL) {
118                 if (strstr(buf, "sshd") == NULL)
119                         continue;
120                 checkline(buf);
121         }
122         syslog(LOG_ERR, "sshlockout exiting");
123         return(0);
124 }
125
126 static
127 void
128 checkline(char *buf)
129 {
130         char ips[128];
131         char *str;
132         int n1;
133         int n2;
134         int n3;
135         int n4;
136
137         /*
138          * ssh login attempt with password (only hit if ssh allows
139          * password entry).  Root or admin.
140          */
141         if ((str = strstr(buf, "Failed password for root from")) != NULL ||
142             (str = strstr(buf, "Failed password for admin from")) != NULL) {
143                 while (*str && (*str < '0' || *str > '9'))
144                         ++str;
145                 if (sscanf(str, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) {
146                         snprintf(ips, sizeof(ips), "%d.%d.%d.%d",
147                                  n1, n2, n3, n4);
148                         if (insert_iph(ips)) {
149                                 syslog(LOG_ERR,
150                                        "Detected ssh password login attempt "
151                                        "for root or admin, locking out %s\n",
152                                        ips);
153                                 snprintf(buf, sizeof(buf),
154                                          "pfctl -t%s -Tadd %s",
155                                          pftable, ips);
156                                 system(buf);
157                         }
158                 }
159                 return;
160         }
161
162         /*
163          * ssh login attempt with password (only hit if ssh allows password
164          * entry).  Non-existant user.
165          */
166         if ((str = strstr(buf, "Failed password for invalid user")) != NULL) {
167                 str += 32;
168                 while (*str == ' ')
169                         ++str;
170                 while (*str && *str != ' ')
171                         ++str;
172                 if (strncmp(str, " from", 5) == 0 &&
173                     sscanf(str + 5, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) {
174                         snprintf(ips, sizeof(ips), "%d.%d.%d.%d",
175                                  n1, n2, n3, n4);
176                         if (insert_iph(ips)) {
177                                 syslog(LOG_ERR,
178                                        "Detected ssh password login attempt "
179                                        "for an invalid user, locking out %s\n",
180                                        ips);
181                                 snprintf(buf, sizeof(buf),
182                                          "pfctl -t%s -Tadd %s",
183                                          pftable, ips);
184                                 system(buf);
185                         }
186                 }
187                 return;
188         }
189
190         /*
191          * Premature disconnect in pre-authorization phase, typically an
192          * attack but require 5 attempts in an hour before cleaning it out.
193          */
194         if ((str = strstr(buf, "Received disconnect from ")) != NULL &&
195             strstr(buf, "[preauth]") != NULL) {
196                 if (sscanf(str + 25, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) {
197                         snprintf(ips, sizeof(ips), "%d.%d.%d.%d",
198                                  n1, n2, n3, n4);
199                         if (insert_iph(ips)) {
200                                 syslog(LOG_ERR,
201                                        "Detected ssh password login attempt "
202                                        "for an invalid user, locking out %s\n",
203                                        ips);
204                                 snprintf(buf, sizeof(buf),
205                                          "pfctl -t%s -Tadd %s",
206                                          pftable, ips);
207                                 system(buf);
208                         }
209                 }
210                 return;
211         }
212 }
213
214 /*
215  * Insert IP record
216  */
217 static
218 int
219 insert_iph(const char *ips)
220 {
221         iphist_t *ip = malloc(sizeof(*ip));
222         iphist_t *scan;
223         time_t t = time(NULL);
224         int found;
225
226         ip->hv = iphash(ips);
227         ip->ips = strdup(ips);
228         ip->t = t;
229
230         ip->hnext = hist_hash[ip->hv & HMASK];
231         hist_hash[ip->hv & HMASK] = ip;
232         ip->next = NULL;
233         *hist_tail = ip;
234         hist_tail = &ip->next;
235         ++hist_count;
236
237         /*
238          * hysteresis
239          */
240         if (hist_count > MAXHIST + 16) {
241                 while (hist_count > MAXHIST)
242                         delete_iph(hist_base);
243         }
244
245         /*
246          * Check limit
247          */
248         found = 0;
249         for (scan = hist_hash[ip->hv & HMASK]; scan; scan = scan->hnext) {
250                 if (scan->hv == ip->hv && strcmp(scan->ips, ip->ips) == 0) {
251                         int dt = (int)(t - ip->t);
252                         if (dt < 60 * 60) {
253                                 ++found;
254                                 if (found > SSHLIMIT)
255                                         break;
256                         }
257                 }
258         }
259         return (found > SSHLIMIT);
260 }
261
262 /*
263  * Delete an ip record.  Note that we always delete from the head of the
264  * list, but we will still wind up scanning hash chains.
265  */
266 static
267 void
268 delete_iph(iphist_t *ip)
269 {
270         iphist_t **scanp;
271         iphist_t *scan;
272
273         scanp = &hist_base;
274         while ((scan = *scanp) != ip) {
275                 scanp = &scan->next;
276         }
277         *scanp = ip->next;
278         if (hist_tail == &ip->next)
279                 hist_tail = scanp;
280
281         scanp = &hist_hash[ip->hv & HMASK];
282         while ((scan = *scanp) != ip) {
283                 scanp = &scan->hnext;
284         }
285         *scanp = ip->hnext;
286
287         --hist_count;
288         free(ip);
289 }
290
291 static
292 void
293 init_iphist(void) {
294         hist_base = NULL;
295         hist_tail = &hist_base;
296         for (int i = 0; i < HSIZE; i++) {
297                 hist_hash[i] = NULL;
298         }
299         hist_count = 0;
300 }