larn(6): Fix two "use of index before limits check" issues.
[dragonfly.git] / usr.sbin / sshlockout / sshlockout.c
blob7175d6d06116535ce35c26cb43bc7c0d56f9ab1f
1 /*
2 * Copyright (c) 2015 The DragonFly Project. All rights reserved.
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>
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
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.
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.
36 * Use: pipe syslog auth output to this program.
38 * Detects failed ssh login attempts and maps out the originating IP and
39 * issues, in case of a PF firewall, adds to a PF table <lockout> using
40 * 'pfctl -tlockout -Tadd' commands.
42 * /etc/syslog.conf line example:
43 * auth.info;authpriv.info |exec /usr/sbin/sshlockout -pf lockout
45 * Also suggest a cron entry to clean out the PF table at least once a day.
46 * 3 3 * * * pfctl -tlockout -Tflush
48 * Alternatively there is an ipfw(8) mode (-ipfw <rulenum>).
51 #include <sys/types.h>
52 #include <sys/time.h>
53 #include <stdio.h>
54 #include <stdlib.h>
55 #include <unistd.h>
56 #include <string.h>
57 #include <stdarg.h>
58 #include <syslog.h>
59 #include <ctype.h>
60 #include <stdbool.h>
62 typedef struct iphist {
63 struct iphist *next;
64 struct iphist *hnext;
65 char *ips;
66 time_t t;
67 int hv;
68 } iphist_t;
70 struct args {
71 int fw_type;
72 char *arg1;
73 char *arg2;
76 #define FW_IS_PF 1
77 #define FW_IS_IPFW 2
79 #define HSIZE 1024
80 #define HMASK (HSIZE - 1)
81 #define MAXHIST 100
82 #define SSHLIMIT 5 /* per hour */
83 #define MAX_TABLE_NAME 20 /* PF table name limit */
85 static iphist_t *hist_base;
86 static iphist_t **hist_tail = &hist_base;
87 static iphist_t *hist_hash[HSIZE];
88 static int hist_count = 0;
90 static struct args args;
92 static void init_iphist(void);
93 static void checkline(char *buf);
94 static int insert_iph(const char *ips, time_t t);
95 static void delete_iph(iphist_t *ip);
97 static
98 void
99 block_ip(const char *ips) {
100 char buf[128];
101 int r = 0;
103 switch (args.fw_type) {
104 case FW_IS_PF:
105 r = snprintf(buf, sizeof(buf),
106 "pfctl -t%s -Tadd %s", args.arg1, ips);
107 break;
108 case FW_IS_IPFW:
109 r = snprintf(buf, sizeof(buf),
110 "ipfw add %s deny tcp from %s to me 22",
111 args.arg1, ips);
112 break;
115 if (r > 0 && (int)strlen(buf) == r) {
116 system(buf);
118 else {
119 syslog(LOG_ERR, "sshlockout: invalid command");
124 * Stupid simple string hash
126 static __inline
128 iphash(const char *str)
130 int hv = 0xA1B3569D;
131 while (*str) {
132 hv = (hv << 5) ^ *str ^ (hv >> 23);
133 ++str;
135 return hv;
139 static bool
140 parse_args(int ac, char **av)
142 if (ac >= 2) {
143 if (strcmp(av[1], "-pf") == 0) {
144 // -pf <tablename>
145 char *tablename = av[2];
146 if (ac == 3 && tablename != NULL) {
147 if (strlen(tablename) > 0 &&
148 strlen(tablename) < MAX_TABLE_NAME) {
149 args.fw_type = FW_IS_PF;
150 args.arg1 = tablename;
151 args.arg2 = NULL;
152 return true;
156 if (strcmp(av[1], "-ipfw") == 0) {
157 // -ipfw <rule>
158 char *rule = av[2];
159 if (ac == 3 && rule != NULL) {
160 if (strlen(rule) > 0 && strlen(rule) <= 5) {
161 for (char *s = rule; *s; ++s) {
162 if (!isdigit(*s))
163 return false;
165 if (atoi(rule) < 1)
166 return false;
167 if (atoi(rule) > 65535)
168 return false;
169 args.fw_type = FW_IS_IPFW;
170 args.arg1 = rule;
171 args.arg2 = NULL;
172 return true;
178 return false;
182 main(int ac, char **av)
184 char buf[1024];
186 args.fw_type = 0;
187 args.arg1 = NULL;
188 args.arg2 = NULL;
190 if (!parse_args(ac, av)) {
191 syslog(LOG_ERR, "sshlockout: invalid argument");
192 return(1);
195 init_iphist();
197 openlog("sshlockout", LOG_PID|LOG_CONS, LOG_AUTH);
198 syslog(LOG_ERR, "sshlockout starting up");
199 freopen("/dev/null", "w", stdout);
200 freopen("/dev/null", "w", stderr);
202 while (fgets(buf, sizeof(buf), stdin) != NULL) {
203 if (strstr(buf, "sshd") == NULL)
204 continue;
205 checkline(buf);
207 syslog(LOG_ERR, "sshlockout exiting");
208 return(0);
211 static
212 void
213 checkip(const char *str, const char *reason1, const char *reason2) {
214 char ips[128];
215 int n1;
216 int n2;
217 int n3;
218 int n4;
219 time_t t = time(NULL);
221 ips[0] = '\0';
223 if (sscanf(str, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) {
224 snprintf(ips, sizeof(ips), "%d.%d.%d.%d", n1, n2, n3, n4);
226 else {
228 * Check for IPv6 address (primitive way)
230 int cnt = 0;
231 while (str[cnt] == ':' || isxdigit(str[cnt])) {
232 ++cnt;
234 if (cnt > 0 && cnt < (int)sizeof(ips)) {
235 memcpy(ips, str, cnt);
236 ips[cnt] = '\0';
241 * We do not block localhost as is makes no sense.
243 if (strcmp(ips, "127.0.0.1") == 0)
244 return;
245 if (strcmp(ips, "::1") == 0)
246 return;
248 if (strlen(ips) > 0) {
251 * Check for DoS attack. When connections from too many
252 * IP addresses come in at the same time, our hash table
253 * would overflow, so we delete the oldest entries AND
254 * block it's IP when they are younger than 10 seconds.
255 * This prevents massive attacks from arbitrary IPs.
257 if (hist_count > MAXHIST + 16) {
258 while (hist_count > MAXHIST) {
259 iphist_t *iph = hist_base;
260 int dt = (int)(t - iph->t);
261 if (dt < 10) {
262 syslog(LOG_ERR,
263 "Detected overflow attack, "
264 "locking out %s\n",
265 iph->ips);
266 block_ip(iph->ips);
268 delete_iph(iph);
272 if (insert_iph(ips, t)) {
273 syslog(LOG_ERR,
274 "Detected ssh %s attempt "
275 "for %s, locking out %s\n",
276 reason1, reason2, ips);
277 block_ip(ips);
282 static
283 void
284 checkline(char *buf)
286 char *str;
289 * ssh login attempt with password (only hit if ssh allows
290 * password entry). Root or admin.
292 if ((str = strstr(buf, "Failed password for root from")) != NULL ||
293 (str = strstr(buf, "Failed password for admin from")) != NULL) {
294 while (*str && (*str < '0' || *str > '9'))
295 ++str;
296 checkip(str, "password login", "root or admin");
297 return;
301 * ssh login attempt with password (only hit if ssh allows password
302 * entry). Non-existant user.
304 if ((str = strstr(buf, "Failed password for invalid user")) != NULL) {
305 str += 32;
306 while (*str == ' ')
307 ++str;
308 while (*str && *str != ' ')
309 ++str;
310 if (strncmp(str, " from", 5) == 0) {
311 checkip(str + 5, "password login", "an invalid user");
313 return;
317 * ssh login attempt for non-existant user.
319 if ((str = strstr(buf, "Invalid user")) != NULL) {
320 str += 12;
321 while (*str == ' ')
322 ++str;
323 while (*str && *str != ' ')
324 ++str;
325 if (strncmp(str, " from", 5) == 0) {
326 checkip(str + 5, "login", "an invalid user");
328 return;
332 * Premature disconnect in pre-authorization phase, typically an
333 * attack but require 5 attempts in an hour before cleaning it out.
335 if ((str = strstr(buf, "Received disconnect from ")) != NULL &&
336 strstr(buf, "[preauth]") != NULL) {
337 checkip(str + 25, "preauth", "an invalid user");
338 return;
342 * Maximum authentication attempts exceeded
344 if ((str = strstr(buf, "maximum authentication "
345 "attempts exceeded for ")) != NULL &&
346 strstr(buf, "[preauth]") != NULL) {
347 str += 45;
348 while (*str == ' ')
349 ++str;
350 while (*str && *str != ' ')
351 ++str;
352 if (strncmp(str, " from", 5) == 0) {
353 checkip(str + 5, "login", "many attempts");
355 return;
360 * Insert IP record
362 static
364 insert_iph(const char *ips, time_t t)
366 iphist_t *ip = malloc(sizeof(*ip));
367 iphist_t *scan;
368 int found;
370 ip->hv = iphash(ips);
371 ip->ips = strdup(ips);
372 ip->t = t;
374 ip->hnext = hist_hash[ip->hv & HMASK];
375 hist_hash[ip->hv & HMASK] = ip;
376 ip->next = NULL;
377 *hist_tail = ip;
378 hist_tail = &ip->next;
379 ++hist_count;
382 * hysteresis
384 if (hist_count > MAXHIST + 16) {
385 while (hist_count > MAXHIST)
386 delete_iph(hist_base);
390 * Check limit
392 found = 0;
393 for (scan = hist_hash[ip->hv & HMASK]; scan; scan = scan->hnext) {
394 if (scan->hv == ip->hv && strcmp(scan->ips, ip->ips) == 0) {
395 int dt = (int)(t - ip->t);
396 if (dt < 60 * 60) {
397 ++found;
398 if (found > SSHLIMIT)
399 break;
403 return (found > SSHLIMIT);
407 * Delete an ip record. Note that we always delete from the head of the
408 * list, but we will still wind up scanning hash chains.
410 static
411 void
412 delete_iph(iphist_t *ip)
414 iphist_t **scanp;
415 iphist_t *scan;
417 scanp = &hist_base;
418 while ((scan = *scanp) != ip) {
419 scanp = &scan->next;
421 *scanp = ip->next;
422 if (hist_tail == &ip->next)
423 hist_tail = scanp;
425 scanp = &hist_hash[ip->hv & HMASK];
426 while ((scan = *scanp) != ip) {
427 scanp = &scan->hnext;
429 *scanp = ip->hnext;
431 --hist_count;
432 free(ip);
435 static
436 void
437 init_iphist(void) {
438 hist_base = NULL;
439 hist_tail = &hist_base;
440 for (int i = 0; i < HSIZE; i++) {
441 hist_hash[i] = NULL;
443 hist_count = 0;