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