1 /* $Id: ssp_pf2.c,v 3.3 2009/11/27 01:39:40 fknobbe Exp $
2 *
3 * Copyright (c) 2003 Hector Paterno <apaterno@dsnsecurity.com>
4 * Copyright (c) 2004, 2005 Olaf Schreck <chakl@syscall.de>
5 * Copyright (c) 2009 Olli Hauer <ohauer@gmx.de>
6 * All rights reserved.
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 * 1. Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 * SUCH DAMAGE.
28 *
29 *
30 * ssp_pf2.c
31 *
32 * Purpose:
33 *
34 * This SnortSam plugin is meant for dynamic (un)blocking on the PF firewall
35 * for OpenBSD >= 3.5, FreeBSD and NetBSD.
36 * SnortSam will expire the blocks itself since PF does not have automatic
37 * time-out functionality.
38 *
39 * This is a reimplementation of the original ssp_pf plugin in order to
40 * simplify it and make it portable.
41 */
42
43 #ifndef USE_SSP_PF
44 #if defined(OpenBSD) || defined(FreeBSD) || defined(NetBSD)
45
46 #ifndef __SSP_PF2_C__
47 #define __SSP_PF2_C__
48
49 #include "snortsam.h"
50 #include "ssp_pf2.h"
51
52 unsigned int PF2use_anchor = TRUE;
53 unsigned int PF2val_count = 0;
54
55
56 /* Routine for opt parsing ( opt=value opt2=value2 etc. ) */
parse_opts(char * line,opt_pf2 * opt,char * sep,char * int_sep,int nopt)57 int parse_opts(char *line, opt_pf2 *opt, char *sep, char *int_sep, int nopt)
58 {
59 char *last;
60 char *last2;
61 char *pt;
62 char *pt2;
63 int fo=0;
64 int di=0;
65
66 if((line==NULL) || (opt==NULL) || (sep==NULL) || (int_sep==NULL))
67 return -1;
68
69 for(pt=strtok_r(line, sep, &last); pt; pt=strtok_r(NULL, sep, &last))
70 {
71 for(pt2=strtok_r(pt, int_sep, &last2), fo=0; pt2; pt2=strtok_r(NULL, int_sep, &last2))
72 {
73 if(fo==0)
74 {
75 for(; fo<nopt; fo++)
76 {
77 if (strncmp(opt[fo].name, pt2, MAX_OPT_NAME)==0)
78 {
79 fo++;
80 di++;
81 break;
82 }
83 }
84 }
85 else
86 {
87 if(di)
88 if(opt[--fo].vt>0)
89 strncpy(opt[fo].v.value_s, pt2, MAX_OPT_VALUE);
90 else
91 opt[fo].v.value_d=atoi(pt2);
92 fo=0;
93 di=0;
94 }
95 }
96 }
97
98 return 0;
99 }
100
101
102 /*
103 * This routine parses the pf2 statements in the config file.
104 */
105 void
PF2Parse(char * val,char * file,unsigned long line,DATALIST * plugindatalist)106 PF2Parse(char *val, char *file, unsigned long line, DATALIST * plugindatalist)
107 {
108 PF2DATA *pfp = NULL;
109 char msg[STRBUFSIZE + 2];
110 char tbuf[PF_TABLE_NAME_SIZE];
111 int pfdev;
112 opt_pf2 options[3]={
113 {"anchor", "", 1},
114 {"table", "", 1},
115 {"kill", "", 1}
116 };
117
118 #ifdef FWSAMDEBUG
119 printf("Debug: [pf2] Plugin Parsing...\n");
120 #endif
121
122 PF2val_count += 1;
123 if (PF2val_count > 1) {
124 snprintf(msg, sizeof(msg) - 1, "Info: [%s: %lu] line ignored ! More than one pf2 statements configured.", file, line);
125 logmessage(1, msg, "pf2", 0);
126 return;
127 }
128
129 if (val != NULL && *val)
130 {
131 if(parse_opts(val, options, " \t", "=", (sizeof(options)/sizeof(opt_pf2)))<0)
132 {
133 snprintf(msg, sizeof(msg) - 1, "Error: [%s: %lu] invalid PF parameters !. PF2 Plugin disabled.", file, line);
134 logmessage(1, msg, "pf2", 0);
135 plugindatalist->data=NULL;
136 return;
137 }
138 }
139 else
140 {
141 snprintf(msg, sizeof(msg) - 1, "Info: [%s: %lu] no parameters configured, using defaults.", file, line);
142 logmessage(1, msg, "pf2", 0);
143 }
144
145 pfp = safemalloc(sizeof(PF2DATA), "PF2Parse", "pfp");
146 bzero(pfp, sizeof(PF2DATA));
147 plugindatalist->data = pfp;
148
149 /* Check Anchor */
150 if(strlen(options[PF2_OPT_ANCHOR].v.value_s)<1)
151 {
152 snprintf(msg, sizeof(msg) - 1,
153 "Info: [%s: %lu] PF anchor name not defined, using \"snortsam\"", file, line);
154 logmessage(1, msg, "pf2", 0);
155 safecopy(pfp->anchorname, "snortsam"); /* save anchorname */
156 }
157 else
158 {
159 safecopy(pfp->anchorname, options[PF2_OPT_ANCHOR].v.value_s); /* save anchorname */
160 /* if PF2use_anchor == FALSE then tables from the main pf section will be used */
161 if ((strncmp(options[PF2_OPT_ANCHOR].v.value_s, "notused", MAX_OPT_VALUE)==0) ||
162 (strncmp(options[PF2_OPT_ANCHOR].v.value_s, "none", MAX_OPT_VALUE)==0)) {
163 PF2use_anchor = FALSE;
164 /* If anchor is not used, wipe none/notused with zeros */
165 bzero(&(pfp->anchorname), sizeof(pfp->anchorname));
166 }
167 }
168
169 /* Check Table */
170 if(strlen(options[PF2_OPT_TABLE].v.value_s)<1)
171 {
172 snprintf(msg, sizeof(msg) - 1,
173 "Info: [%s: %lu] PF table not defined, using \"blockin,blockout\" for tables", file, line);
174 logmessage(1, msg, "pf2", 0);
175 safecopy(pfp->tablein, "blockin");
176 safecopy(pfp->tableout, "blockout");
177 }
178 else
179 {
180 /* save tablenames */
181 snprintf(tbuf, PF_TABLE_NAME_SIZE, "%sin", options[PF2_OPT_TABLE].v.value_s);
182 safecopy(pfp->tablein, tbuf);
183 snprintf(tbuf, PF_TABLE_NAME_SIZE, "%sout", options[PF2_OPT_TABLE].v.value_s);
184 safecopy(pfp->tableout, tbuf);
185 }
186
187 /* Check kill option, for safety reason default: kill=all */
188 if (strlen(options[PF2_OPT_KILL].v.value_s)>1) {
189 if (strncmp(options[PF2_OPT_KILL].v.value_s, "all", MAX_OPT_VALUE)==0)
190 pfp->kill = PF2_KILL_STATE_ALL;
191 else if (strncmp(options[PF2_OPT_KILL].v.value_s, "dir", MAX_OPT_VALUE)==0)
192 pfp->kill = PF2_KILL_STATE_DIR;
193 else if (strncmp(options[PF2_OPT_KILL].v.value_s, "no", MAX_OPT_VALUE)==0)
194 pfp->kill = PF2_KILL_STATE_NO;
195 else {
196 pfp->kill = PF2_KILL_STATE_ALL;
197 snprintf(msg, sizeof(msg) - 1,
198 "Error: [%s: %lu] invalid PF parameters \"kill=%s\"! Fallback to \"kill=all\"",
199 file, line, options[PF2_OPT_KILL].v.value_s);
200 logmessage(1, msg, "pf2", 0);
201 }
202 }
203 else {
204 pfp->kill = PF2_KILL_STATE_ALL;
205 snprintf(msg, sizeof(msg) - 1,
206 "Info: [%s: %lu] PF kill option not defined, using \"kill=all\"", file, line);
207 logmessage(1, msg, "pf2", 0);
208 }
209
210
211 /* check if we can open PFDEV, else disable the plugin */
212 pfdev = open(PFDEV, O_RDWR);
213 if (pfdev == -1) {
214 snprintf(msg, sizeof(msg) - 1, "Error: cannot open device \"%s\" ! PF2 Plugin disabled.", PFDEV);
215 logmessage(1, msg, "pf2", 0);
216 free(pfp);
217 plugindatalist->data=NULL;
218 return;
219 }
220
221 /*
222 * check if anchor and tables exist.
223 * We could disable the plugin if anchor/tables do not exist, but we will throw an error
224 * showing what is missing at start time and for every block/unblock request.
225 */
226 if(PF2use_anchor)
227 lookup_anchor(pfdev, pfp->anchorname);
228 lookup_table(pfdev, pfp->tablein, pfp->anchorname);
229 lookup_table(pfdev, pfp->tableout, pfp->anchorname);
230
231 if(pfdev)
232 close(pfdev);
233
234 #ifdef FWSAMDEBUG
235 printf("Debug: [pf2] Adding PF: \n");
236 printf("\tanchor=%s\n\ttables=%s,%s\n\tkill=%s\n",
237 pfp->anchorname, pfp->tablein, pfp->tableout ,
238 pfp->kill==PF2_KILL_STATE_ALL ? "all" :
239 pfp->kill==PF2_KILL_STATE_DIR ? "dir" : "no");
240 #endif
241
242 }
243
244
245 /*
246 * BLOCK/UNBLOCK Routine
247 */
248 void
PF2Block(BLOCKINFO * bd,void * data,unsigned long qp)249 PF2Block(BLOCKINFO * bd, void * data,unsigned long qp)
250 {
251 PF2DATA *pfp;
252 struct pf_status status;
253 int pfdev;
254 int tin=0, tout=0;
255 char ipsrc[256]; /* ip as a string */
256 char msg[STRBUFSIZE + 2];
257 #ifdef FWSAMDEBUG
258 pthread_t threadid=pthread_self();
259 #endif
260
261 if(!data)
262 return;
263
264 pfp=(PF2DATA *)data;
265
266 #ifdef FWSAMDEBUG
267 printf("Debug: [pf2][%lx] Plugin Blocking...\n", threadid);
268 #endif
269
270 snprintf(ipsrc, sizeof(ipsrc) - 1, inettoa(bd->blockip));
271 switch(bd->mode & FWSAM_HOW)
272 {
273 case FWSAM_HOW_THIS:
274 tin = tout = 1;
275 break;
276 case FWSAM_HOW_IN:
277 tin = 1;
278 break;
279 case FWSAM_HOW_OUT:
280 tout = 1;
281 break;
282 case FWSAM_HOW_INOUT:
283 tin = tout = 1;
284 break;
285 }
286
287 /* open the pf device */
288 pfdev = open(PFDEV, O_RDWR);
289 if (pfdev == -1) {
290 snprintf(msg, sizeof(msg) - 1, "Error: cannot open device %s", PFDEV);
291 logmessage(1, msg, "pf2", 0);
292 return;
293 }
294
295 if (ioctl(pfdev, DIOCGETSTATUS, &status)) {
296 logmessage(1, "Error: cannot get pf status", "pf2", 0);
297 return;
298 }
299
300 if (!status.running) {
301 /* even pf is not enabled, we can add IP's to pf tables if they exist */
302 logmessage(1, "Info: pf is not enabled", "pf2", 0);
303 }
304
305 /* BLOCK */
306 if (bd->block)
307 {
308 snprintf(msg, sizeof(msg) - 1, "Info: Blocking ip %s", ipsrc);
309 logmessage(3, msg, "pf2", 0);
310
311 if (tin)
312 if ( lookup_table(pfdev, pfp->tablein, pfp->anchorname)==0 )
313 change_table(pfdev, 1, pfp->tablein, pfp->anchorname, ipsrc);
314
315 if (tout)
316 if ( lookup_table(pfdev, pfp->tableout, pfp->anchorname)==0 )
317 change_table(pfdev, 1, pfp->tableout, pfp->anchorname, ipsrc);
318
319 /* kill PF states after IP is placed in table */
320 if (pfp->kill != PF2_KILL_STATE_NO)
321 pf2_kill_states(pfdev, ipsrc, tin, tout);
322 }
323 else /* UNBLOCK */
324 {
325 snprintf(msg, sizeof(msg) - 1, "Info: Unblocking ip %s", ipsrc);
326 logmessage(3, msg, "pf2", 0);
327
328 if (tin)
329 if ( lookup_table(pfdev, pfp->tablein, pfp->anchorname)==0 )
330 change_table(pfdev, 0, pfp->tablein, pfp->anchorname, ipsrc);
331
332 if (tout)
333 if ( lookup_table(pfdev, pfp->tableout, pfp->anchorname)==0 )
334 change_table(pfdev, 0, pfp->tableout, pfp->anchorname, ipsrc);
335 }
336 close(pfdev);
337 return;
338 } /* PF2BLOCK */
339
340 /* borrowed from OpenBSDs pfctl code */
341 int
change_table(int pfdev,int add,const char * table,const char * anchor,const char * ipsrc)342 change_table(int pfdev, int add, const char *table, const char *anchor, const char *ipsrc)
343 {
344 char msg[STRBUFSIZE + 2];
345 struct pfioc_table io;
346 struct pfr_addr addr;
347
348 bzero(&io, sizeof(io));
349 strlcpy(io.pfrio_table.pfrt_name, table, sizeof(io.pfrio_table.pfrt_name));
350
351 if (PF2use_anchor == TRUE)
352 strlcpy(io.pfrio_table.pfrt_anchor, anchor, sizeof(io.pfrio_table.pfrt_anchor));
353 io.pfrio_buffer = &addr;
354 io.pfrio_esize = sizeof(addr);
355 io.pfrio_size = 1;
356
357 bzero(&addr, sizeof(addr));
358 if (ipsrc == NULL || !ipsrc[0])
359 return (-1);
360 if (inet_pton(AF_INET, ipsrc, &addr.pfra_ip4addr) == 1) {
361 addr.pfra_af = AF_INET;
362 addr.pfra_net = 32;
363 } else if (inet_pton(AF_INET6, ipsrc, &addr.pfra_ip6addr) == 1) {
364 addr.pfra_af = AF_INET6;
365 addr.pfra_net = 128;
366 } else {
367 snprintf(msg, sizeof(msg) - 1, "invalid ipsrc");
368 logmessage(3, msg, "pf2", 0);
369 return (-1);
370 }
371
372 if (ioctl(pfdev, add ? DIOCRADDADDRS : DIOCRDELADDRS, &io) && errno != ESRCH) {
373 snprintf(msg, sizeof(msg) - 1, "cannot %s %s %s table %s: %s",
374 add ? "add" : "remove", ipsrc, add ? "to" : "from", table, strerror(errno));
375 logmessage(3, msg, "pf2", 0);
376 return (-1);
377 }
378 #ifdef FWSAMDEBUG
379 printf("Debug: [pf2] %s %s %s anchor=%s table=%s\n",
380 add ? "add" : "remove", ipsrc, add ? "to" : "from", anchor, table);
381 #endif
382 return (0);
383 }
384
385
386 /* Kill ipsrc state(s) from PF statefull table, so we can catch the IP with the
387 * configured tables. If states are not killed existing connections stay open as
388 * long they have a valid entry in the PF state.
389 * We can drop all states or only those matching the direction in/out.
390 */
391 int
pf2_kill_states(int pfdev,const char * ipsrc,int tin,int tout)392 pf2_kill_states(int pfdev, const char *ipsrc, int tin, int tout )
393 {
394 char msg[STRBUFSIZE + 2];
395 struct pf_addr pfa;
396 struct pfioc_state_kill psk;
397 sa_family_t saf; /* stafe AF_INET family */
398 unsigned long killed=0, killed_src=0, killed_dst=0;
399
400 bzero(&pfa, sizeof(pfa));
401 bzero(&psk, sizeof(psk));
402
403 if (ipsrc == NULL || !ipsrc[0])
404 return (-1);
405
406 if (inet_pton(AF_INET, ipsrc, &pfa.v4) == 1)
407 psk.psk_af = saf = AF_INET;
408 else if (inet_pton(AF_INET6, ipsrc, &pfa.v6) == 1)
409 psk.psk_af = saf = AF_INET6;
410 else {
411 snprintf(msg, sizeof(msg) - 1, "invalid ipsrc");
412 logmessage(3, msg, "pf2", 0);
413 return (-1);
414 }
415
416 /* Kill all states from pfa */
417 if (tin || PF2_KILL_STATE_ALL) {
418 memcpy(&psk.psk_src.addr.v.a.addr, &pfa, sizeof(psk.psk_src.addr.v.a.addr));
419 memset(&psk.psk_src.addr.v.a.mask, 0xff, sizeof(psk.psk_src.addr.v.a.mask));
420 if (ioctl(pfdev, DIOCKILLSTATES, &psk)) {
421 snprintf(msg, sizeof(msg) - 1, "Error: DIOCKILLSTATES failed (%s)", strerror(errno));
422 logmessage(1, msg, "pf2", 0);
423 }
424 else {
425 #if OpenBSD >= 200811 /* since OpenBSD4_4 killed states returned in psk_killed */
426 killed_src += psk.psk_killed;
427 #else
428 killed_src += psk.psk_af;
429 #endif
430 #ifdef FWSAMDEBUG
431 printf("Debug: [pf2] killed %lu (tin) states for host %s\n", killed_src, ipsrc);
432 #endif
433 }
434 psk.psk_af = saf; /* restore AF_INET */
435 }
436
437 /* Kill all states to pfa */
438 if (tout || PF2_KILL_STATE_ALL) {
439 bzero(&psk.psk_src, sizeof(psk.psk_src)); /* clear source address field (set before for incomming) */
440 memcpy(&psk.psk_dst.addr.v.a.addr, &pfa, sizeof(psk.psk_dst.addr.v.a.addr));
441 memset(&psk.psk_dst.addr.v.a.mask, 0xff, sizeof(psk.psk_dst.addr.v.a.mask));
442 if (ioctl(pfdev, DIOCKILLSTATES, &psk)) {
443 snprintf(msg, sizeof(msg) - 1, "Error: DIOCKILLSTATES failed (%s)", strerror(errno));
444 logmessage(1, msg, "pf2", 0);
445 }
446 else {
447 #if OpenBSD >= 200811 /* since OpenBSD4_4 killed states returned in psk_killed */
448 killed_dst += psk.psk_killed;
449 #else
450 killed_dst += psk.psk_af;
451 #endif
452 #ifdef FWSAMDEBUG
453 printf("Debug: [pf2] killed %lu (tout) states for host %s\n", killed_dst, ipsrc);
454 #endif
455 }
456 }
457
458 if ((killed_src + killed_dst)>0) {
459 snprintf(msg, sizeof(msg) - 1, "Info: Killed %lu PF state(s) (in: %lu, out: %lu) for host %s",
460 killed_src + killed_dst, killed_src, killed_dst, ipsrc);
461 logmessage(3, msg, "pf2", 0);
462 }
463 return(0);
464 } /* pf2_kill_states */
465
466
467 /* check if anchor exist */
468 int
lookup_anchor(int dev,const char * anchorname)469 lookup_anchor(int dev, const char *anchorname)
470 {
471 struct pfioc_ruleset pr;
472 char msg[STRBUFSIZE + 2];
473
474 bzero(&pr, sizeof(pr));
475 strlcpy(pr.path, anchorname, sizeof(pr.path));
476 if (ioctl(dev, DIOCGETRULESETS, &pr)) {
477 if (errno == EINVAL){
478 snprintf(msg, sizeof(msg) - 1, "Error: anchor \"%s\" not found", anchorname);
479 logmessage(1, msg, "pf2", 0);
480 return (-1);
481 }
482 }
483 #ifdef FWSAMDEBUG
484 printf("Debug: [pf2] lookup_anchor: found anchor %s\n", anchorname);
485 #endif
486 return (0);
487 }
488
489
490 /* check if table exist */
491 int
lookup_table(int dev,const char * tablename,const char * anchorname)492 lookup_table(int dev, const char *tablename, const char *anchorname)
493 {
494 struct pfioc_table io;
495 struct pfr_table table;
496 struct pfr_addr pfa;
497 char msg[STRBUFSIZE + 2];
498
499 if (strlen(tablename) == 0)
500 return(-1);
501
502 bzero(&io, sizeof(io));
503 bzero(&table, sizeof(table));
504 bzero(&pfa, sizeof(pfa));
505
506 strlcpy(table.pfrt_anchor, anchorname, sizeof(table.pfrt_anchor));
507 strlcpy(table.pfrt_name, tablename, sizeof(table.pfrt_name));
508
509 io.pfrio_table = table;
510 io.pfrio_esize = sizeof(pfa);
511
512 #ifdef FWSAMDEBUG
513 printf("Debug: [pf2] lookup_table: anchor=%s table=%s\n", io.pfrio_table.pfrt_anchor, io.pfrio_table.pfrt_name);
514 #endif
515
516 if (ioctl(dev, DIOCRGETADDRS, &io)) {
517 snprintf(msg, sizeof(msg) - 1, "Error: table \"%s\" not found, anchor=%s table=%s",
518 io.pfrio_table.pfrt_name, io.pfrio_table.pfrt_anchor, io.pfrio_table.pfrt_name);
519 logmessage(1, msg, "pf2", 0);
520 return(-1);
521 }
522
523 #ifdef FWSAMDEBUG
524 printf("Debug: [pf2] table \"%s\" contains [%d] entries\n", io.pfrio_table.pfrt_name, io.pfrio_size);
525 #endif
526 return(0);
527 }
528
529 #endif /* __SSP_PF2_C__ */
530
531 #endif /* OpenBSD || FreeBSD || NetBSD */
532 #endif /* !USE_SSP_PF */
533 /* vim: set ts=8 sw=4: */
534