3d87fc7925481fee6899884c811909164739d858
[fwsnort.git] / fwsnort
1 #!/usr/bin/perl -w
2 #
3 ###############################################################################
4 #
5 # File: fwsnort
6 #
7 # URL: http://www.cipherdyne.org/fwsnort/
8 #
9 # Purpose: To translate snort rules into equivalent iptables rules.
10 #          fwsnort is based on the original snort2iptables shell script
11 #          written by William Stearns.
12 #
13 # Author: Michael Rash <mbr@cipherdyne.org>
14 #
15 # Credits: (see the CREDITS file)
16 #
17 # Version: 1.6.2-pre1
18 #
19 # Copyright (C) 2003-2012 Michael Rash (mbr@cipherdyne.org)
20 #
21 # License - GNU Public License version 2 (GPLv2):
22 #
23 #    This program is distributed in the hope that it will be useful,
24 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
25 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26 #    GNU General Public License for more details.
27 #
28 #    You should have received a copy of the GNU General Public License
29 #    along with this program; if not, write to the Free Software
30 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
31 #    USA
32 #
33 # TODO:
34 #   - Add the ability to remove rules from a real snort config in the same
35 #     way we remove them from iptables rulesets in fwsnort (we remove rules
36 #     from an iptables ruleset if the iptables policy will not allow such
37 #     traffic through in the first place).
38 #   - New option: --ipt-mark.
39 #
40 # Reference: Snort is a registered trademark of Sourcefire, Inc
41 #
42 # Snort Rule Options:
43 #
44 #   msg:           Prints a message in alerts and packet logs.
45 #   logto:         Log the packet to a user specified filename instead of the
46 #                  standard output file.
47 #   ttl:           Test the IP header's TTL field value.
48 #   tos:           Test the IP header's TOS field value.
49 #   id:            Test the IP header's fragment ID field for a specific
50 #                  value.
51 #   ipoption:      Watch the IP option fields for specific codes.
52 #   fragbits:      Test the fragmentation bits of the IP header.
53 #   dsize:         Test the packet's payload size against a value.
54 #   flags          Test the TCP flags for certain values.
55 #   seq:           Test the TCP sequence number field for a specific value.
56 #   ack:           Test the TCP acknowledgement field for a specific value.
57 #   itype:         Test the ICMP type field against a specific value.
58 #   icode:         Test the ICMP code field against a specific value.
59 #   icmp_id:       Test the ICMP ECHO ID field against a specific value.
60 #   icmp_seq:      Test the ICMP ECHO sequence number against a specific
61 #                  value.
62 #   content:       Search for a pattern in the packet's payload.
63 #   content-list:  Search for a set of patterns in the packet's payload.
64 #   offset:        Modifier for the content option, sets the offset to begin
65 #                  attempting a pattern match.
66 #   depth:         Modifier for the content option, sets the maximum search
67 #                  depth for a pattern match attempt.
68 #   nocase:        Match the preceding content string with case insensitivity.
69 #   session        Dumps the application layer information for a given
70 #                  session.
71 #   rpc:           Watch RPC services for specific application/procedure
72 #                  calls.
73 #   resp:          Active response (knock down connections, etc).
74 #   react:         Active response (block web sites).
75 #   reference:     External attack reference ids.
76 #   sid:           snort rule id.
77 #   rev:           Rule revision number.
78 #   classtype:     Rule classification identifier.
79 #   priority:      Rule severity identifier.
80 #   uricontent:    Search for a pattern in the URI portion of a packet
81 #
82 #   tag:           Advanced logging actions for rules.
83 #   ip_proto:      IP header's protocol value.
84 #   sameip:        Determines if source ip equals the destination ip.
85 #   stateless:     Valid regardless of stream state.
86 #   regex:         Wildcard pattern matching.
87 #
88 ############################################################################
89 #
90
91 use IO::Socket;
92 use File::Copy;
93 use File::Path;
94 use Sys::Hostname;
95 use Data::Dumper;
96 use Cwd;
97 use Getopt::Long;
98 use strict;
99
100 ### config file
101 my $CONFIG_DEFAULT = '/etc/fwsnort/fwsnort.conf';
102 my $fwsnort_conf = $CONFIG_DEFAULT;
103
104 ### version number
105 my $version = '1.6.2-pre1';
106
107 my %ipt_hdr_opts = (
108     'src'      => '-s',
109     'sport'    => '--sport',
110     'dst'      => '-d',
111     'dport'    => '--dport',
112     'proto'    => '-p',
113 );
114
115 my %snort_opts = (
116     ### snort options that we can directly filter on
117     ### in iptables rulesets (snort options are separate
118     ### from the snort "header" which include protocol,
119     ### source, destination, etc.)
120     'filter' => {
121
122         ### application layer
123         'uricontent' => {  ### use --strict to not translate this
124             'iptopt' => '-m string',
125             'regex'  => '[\s;]uricontent:\s*\"(.*?)\"\s*;'
126         },
127         'content' => {
128             'iptopt' => '-m string',
129             'regex'  => '[\s;]content:\s*\"(.*?)\"\s*;'
130         },
131         'fast_pattern' => {
132             'iptopt' => '',  ### fast_pattern just governs ordering of
133                              ### content matches
134             'regex'  => '[\s;]fast_pattern(?::\s*.*?\s*)?;',
135         },
136         'pcre' => {
137             ### only basic PCRE's that just have strings separated
138             ### by ".*" or ".+" are supported.
139             'iptopt' => '-m string',
140             'regex'  => '[\s;]pcre:\s*\"(.*?)\"\s*;'
141         },
142         'nocase'  => {
143             'iptopt' => '--icase',
144             'regex'  => '[\s;]nocase\s*;',
145         },
146         'offset'  => {
147             'iptopt' => '--from',
148             'regex'  => '[\s;]offset:\s*(\d+)\s*;'
149         },
150         'depth' =>  {
151             'iptopt' => '--to',
152             'regex'  => '[\s;]depth:\s*(\d+)\s*;'
153         },
154
155         ### technically, the "distance" and "within" criteria
156         ### are relative to the end of the previous pattern match,
157         ### so iptables cannot emulate these directly; an approximation
158         ### is made based on the on length of the previous pattern an
159         ### any "depth" or "offset" criteria for the previous pattern.
160         ### To disable signatures with "distance" and "within", just
161         ### use the --strict option.
162         'distance'  => {
163             'iptopt' => '--from',
164             'regex'  => '[\s;]distance:\s*(\d+)\s*;'
165         },
166         'within' =>  {
167             'iptopt' => '--to',
168             'regex'  => '[\s;]within:\s*(\d+)\s*;'
169         },
170         'replace' => {  ### for Snort running in inline mode
171             'iptopt' => '--replace-string',
172             'regex'  => '[\s;]replace:\s*\"(.*?)\"\s*;'
173         },
174         'resp' => {
175             'iptopt' => '-j REJECT',
176             'regex'  => '[\s;]resp:\s*(.*?)\s*;'
177         },
178
179         ### transport layer
180         'flags' => {
181             'iptopt' => '--tcp-flags',
182             'regex'  => '[\s;]flags:\s*(.*?)\s*;'
183         },
184         'flow' => {
185             'iptopt' => '--tcp-flags',
186             'regex'  => '[\s;]flow:\s*(.*?)\s*;'
187         },
188
189         ### network layer
190         'itype' => {
191             'iptopt' => '--icmp-type',  ### --icmp-type type/code
192             'regex'  => '[\s;]itype:\s*(.*?)\s*;'
193         },
194         'icode' => {
195             'iptopt' => 'NONE',
196             'regex'  => '[\s;]icode:\s*(.*?)\s*;'
197         },
198         'ttl' => {
199             'iptopt' => '-m ttl', ### requires CONFIG_IP_NF_MATCH_TTL
200             'regex'  => '[\s;]ttl:\s*(.*?)\s*;'
201         },
202         'tos' => {
203             'iptopt' => '-m tos --tos', ### requires CONFIG_IP_NF_MATCH_TOS
204             'regex'  => '[\s;]tos:\s*(\d+)\s*;'
205         },
206         'ipopts' => {
207             'iptopt' => '-m ipv4options',  ### requires ipv4options extension
208             'regex'  => '[\s;]ipopts:\s*(\w+)\s*;'
209         },
210         'ip_proto' => {
211             'iptopt' => '-p',
212             'regex'  => '[\s;]ip_proto:\s*(.*?)\s*;'
213         },
214         'dsize' => {  ### requires CONFIG_IP_NF_MATCH_LENGTH
215             'iptopt' => '-m length --length',
216             'regex'  => '[\s;]dsize:\s*(.*?)\s*;'
217         },
218     },
219
220     ### snort options that can be put into iptables
221     ### ruleset, but only in log messages with --log-prefix
222     'logprefix' =>  {
223         'sid'       => '[\s;]sid:\s*(\d+)\s*;',
224         'msg'       => '[\s;]msg:\s*\"(.*?)\"\s*;',  ### we create a space
225         'classtype' => '[\s;]classtype:\s*(.*?)\s*;',
226         'reference' => '[\s;]reference:\s*(.*?)\s*;',
227         'priority'  => '[\s;]priority:\s*(\d+)\s*;',
228         'rev'       => '[\s;]rev:\s*(\d+)\s*;',
229     },
230
231     ### snort options that cannot be included directly
232     ### within iptables filter statements (yet :)
233     'unsupported' => {
234         'asn1'         => '[\s;]asn1:\s*.*?\s*;',
235         'fragbits'     => '[\s;]fragbits:\s*.*?\s*;',
236         'content-list' => '[\s;]content\-list:\s*\".*?\"\s*;',
237         'rpc'          => '[\s;]rpc:\s*.*?\s*;',
238         'byte_test'    => '[\s;]byte_test\s*.*?\s*;',
239         'byte_jump'    => '[\s;]byte_jump\s*.*?\s*;',
240         'window'       => '[\s;]window:\s*.*?\s*;',
241         'flowbits'     => '[\s;]flowbits:\s*.*?\s*;',
242 #        'offset'       => '[\s;]offset:\s*\d+\s*;',
243 #        'depth'        => '[\s;]depth:\s*\d+\s*;',
244
245         ### the following fields get logged by iptables but
246         ### we cannot filter them directly except with the
247         ### iptables u32 module.  Functionality has been built
248         ### into psad to generate alerts for most of these Snort
249         ### options.
250         'id'        => '[\s;]id:\s*(\d+)\s*;',
251         'seq'       => '[\s;]seq:\s*(\d+)\s*;',  ### --log-tcp-sequence
252         'ack'       => '[\s;]ack:\s*.*?\s*;',    ### --log-tcp-sequence
253         'icmp_seq'  => '[\s;]icmp_seq:\s*(\d+)\s*;',
254         'icmp_id'   => '[\s;]icmp_id:\s*(\d+)\s*;',
255         'sameip'    => '[\s;]sameip\s*;',
256         'regex'     => '[\s;]regex:\s*(.*?)\s*;',
257         'isdataat'  => '[\s;]isdataat:\s*(.*?)\s*;',
258         'threshold' => '[\s;]threshold:\s*.*?\s*;',               ### FIXME --limit
259         'detection_filter' => '[\s;]detection_filter:\s*.*?\s*;'  ### FIXME --limit
260     },
261
262     ### snort options that fwsnort will ignore
263     'ignore' => {
264         'rawbytes' => '[\s;]rawbytes\s*;',  ### iptables does a raw match anyway
265         'logto'    => '[\s;]logto:\s*\S+\s*;',
266         'session'  => '[\s;]session\s*;',
267         'tag'      => '[\s;]tag:\s*.*?\s*;',
268         'react'    => '[\s;]react:\s*.*?\s*;', ### FIXME -j REJECT
269         'http_uri' => '[\s;]http_uri\s*;',
270         'http_method' => '[\s;]http_method\s*;',
271         'urilen'    => '[\s;]urilen:\s*.*?\s*;',
272     }
273 );
274
275 ### rules update link
276 my $DEFAULT_RULES_URL = 'http://rules.emergingthreats.net/open/snort-2.9.0/emerging-all.rules';
277 my $rules_url = $DEFAULT_RULES_URL;
278
279 ### config vars that may span multiple lines
280 my %multi_line_vars = (
281     'UPDATE_RULES_URL' => '',
282     'WHITELIST' => '',
283     'BLACKLIST' => '',
284 );
285
286 ### array that contains the fwsnort iptables script (will be written
287 ### to $config{'FWSNORT_SCRIPT'})
288 my @ipt_script_lines = ();
289
290 ### array that contains the fwsnort policy in iptables-save format.
291 ### This will also contain the running iptables policy, so the fwsnort
292 ### policy is integrated in.
293 my @fwsnort_save_lines = ();
294 my @ipt_save_lines     = ();
295 my $ipt_save_index     = 0;
296 my @ipt_save_script_lines = ();
297 my $ipt_save_completed_line = '';
298 my $save_str    = 'iptables-save';
299 my $ipt_str     = 'iptables';
300 my $save_bin    = '';
301 my $restore_bin = '';
302 my $ipt_bin     = '';
303
304 ### contains a cache of the iptables policy
305 my %ipt_policy = ();
306 my %ipt_default_policy_setting = ();
307 my %ipt_default_drop = ();
308 my %ipt_save_existing_chains = ();
309
310 ### hashes for save format data
311 my %save_format_whitelist = ();
312 my %save_format_blacklist = ();
313 my %save_format_prereqs   = ();
314 my %save_format_rules     = ();
315 my %save_format_conntrack_jumps = ();
316
317 ### regex to match ip addresses
318 my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;
319
320 my %snort_dump_cache = ();
321 my %ipt_dump_cache = ();
322
323 ### for iptables capabilities testing
324 my $NON_HOST     = '127.0.0.2';
325 my $NON_IP6_HOST = '::2/128';
326 my $non_host     = '';
327
328 my $IPT_SUCCESS = 1;
329 my $IPT_FAILURE = 0;
330 my $IPT_TEST_RULE_NUM = 1;
331
332 my $MATCH_EQUIV  = 1;
333 my $MATCH_SUBSTR = 2;
334
335 ### header lengths; note that IP and TCP lengths are defined
336 ### in the fwsnort.conf file since they may each contain options,
337 ### but until the --payload option is added to the string match
338 ### extension there is no way to account for them except to
339 ### define an average length.
340 my $MAC_HDR_LEN = 14;
341 my $UDP_HDR_LEN = 8;
342 my $ICMP_HDR_LEN = 8;
343
344 ### config and commands hashes (constructed by import_config())
345 my %config = ();
346 my %cmds   = ();
347
348 my @local_addrs   = ();
349 my %include_types = ();
350 my %exclude_types = ();
351 my %include_sids  = ();
352 my %exclude_sids  = ();
353 my %restrict_interfaces = ();
354
355 ### establish some default behavior
356 my $home_net   = '';  ### normally comes from fwsnort.conf
357 my $ext_net    = '';  ### normally comes from fwsnort.conf
358 my $ipt_exec   = 0;
359 my $ipt_drop   = 0;
360 my $ipt_reject = 0;
361 my $help       = 0;
362 my $stdout     = 0;
363 my $lib_dir    = '';
364 my $rules_file = '';
365 my $debug      = 0;
366 my $is_root    = 0;
367 my $dumper     = 0;
368 my $dump_ipt   = 0;
369 my $dump_snort = 0;
370 my $strict     = 0;
371 my $ipt_script = '';
372 my $logfile    = '';
373 my $rules_dir  = '';
374 my $homedir    = '';
375 my $abs_num    = 0;
376 my $run_last   = 0;
377 my $queue_rules_dir = '';
378 my $queue_pre_match_max = 0;
379 my $dump_conf  = 0;
380 my $kernel_ver = '2.6';  ### default
381 my $string_match_alg = 'bm';
382 my $verbose    = 0;
383 my $print_ver  = 0;
384 my $cmdl_homedir   = '';
385 my $update_rules   = 0;  ### used to download latest snort rules
386 my $ipt_print_type = 0;
387 my $ipt_check_capabilities = 0;
388 my $ipt_rule_ctr   = 1;
389 my $ipt_sync       = 0;
390 my $ipt_flush      = 0;
391 my $ipt_del_chains = 0;
392 my $ipt_list       = 0;
393 my $ipt_file       = '';
394 my $no_pcre        = 0;
395 my $no_ipt_log     = 0;
396 my $no_ipt_test    = 0;
397 my $no_ipt_jumps   = 0;
398 my $no_ipt_input   = 0;
399 my $no_ipt_output  = 0;
400 my $no_addr_check  = 0;
401 my $no_ipt_forward = 0;
402 my $ignore_opt     = 0;
403 my $include_sids   = '';
404 my $exclude_sids   = '';
405 my $add_deleted    = 0;
406 my $rules_types    = '';
407 my $exclude_types  = '';
408 my $snort_type     = '';
409 my $ulog_nlgroup   = 1;
410 my $queue_mode     = 0;
411 my $nfqueue_mode   = 0;
412 my $nfqueue_num    = 0;
413 my $ulog_mode      = 0;
414 my $exclude_re     = '';
415 my $include_re     = '';
416 my $include_re_caseless = 0;
417 my $exclude_re_caseless = 0;
418 my $enable_ip6tables  = 0;
419 my $ipt_var_str       = 'IPTABLES';
420 my $no_ipt_conntrack  = 0;
421 my $conntrack_state   = 'ESTABLISHED';
422 my $have_conntrack    = 0;
423 my $have_state        = 0;
424 my $snort_conf_file   = '';
425 my $ipt_restrict_intf = '';
426 my $no_ipt_comments  = 0;
427 my $no_ipt_rule_nums = 0;
428 my $no_exclude_loopback = 0;
429 my $no_ipt_log_ip_opts  = 0;
430 my $no_ipt_log_tcp_opts = 0;
431 my $ipt_log_tcp_seq     = 0;
432 my $include_perl_triggers = 0;
433 my $duplicate_last_build  = 0;
434 my $ipt_max_str_len = 1;
435 my $ipt_max_log_prefix_len = 1;
436 my $ipt_max_comment_len = 1;
437 my $no_fast_pattern_order = 0;
438 my $ipt_have_multiport_match = 0;
439 my $ipt_multiport_max = 2;
440
441 ### to be added to the string match extension
442 my $ipt_has_string_payload_offset_opt = 0;
443
444 ### default to processing these filter chains
445 my %process_chains = (
446     'INPUT'   => 1,
447     'FORWARD' => 1,
448     'OUTPUT'  => 1,
449 );
450 my $TEST_CHAIN = 'FWS_CAP_TEST';
451
452 my %chain_ctr = ();
453
454 ### save a copy of the command line args
455 my @argv_cp = @ARGV;
456
457 ### see if we are running as root
458 &is_root();
459
460 ### handle the command line args
461 &handle_cmd_line();
462
463 &run_last_cmdline() if $run_last;
464
465 ### import config, initialize various things, etc.
466 &fwsnort_init();
467
468 ### if we are running with $chk_ipt_policy, then cache
469 ### the current iptables policy
470 &cache_ipt_policy() if $ipt_sync;
471
472 ### truncate old fwsnort log
473 &truncate_logfile();
474
475 ### check to make sure iptables has various functionality available
476 ### such as the LOG target, --hex-strings, the comment match, etc.
477 &ipt_capabilities() unless $no_ipt_test;
478
479 ### cache the running iptables policy in iptables-save format
480 &cache_ipt_save_policy();
481
482 ### print a header at the top of the iptables ruleset
483 ### script
484 &ipt_hdr();
485
486 ### now that we have the interfaces, add the iptables
487 ### chains to the fwsnort shell script
488 &ipt_add_chains();
489
490 ### add any WHITELIST rules to the main fwsnort chains
491 ### with the RETURN target
492 &ipt_whitelist();
493
494 ### add any BLACKLIST rules to the main fwsnort chains
495 ### with the DROP or REJECT targets
496 &ipt_blacklist();
497
498 ### add jump rules for established tcp connections to
499 ### the fwsnort state tracking chains
500 &ipt_add_conntrack_jumps() unless $no_ipt_conntrack;
501
502 ### display the config on STDOUT
503 &dump_conf() if $dump_conf;
504
505 ### make sure <type>.rules file exists if --type was
506 ### specified on the command line
507 &check_type() if $rules_types;
508
509 &logr("[+] Begin parsing cycle.");
510
511 ### parse snort rules (signatures)
512 if ($include_sids) {
513     print "[+] Parsing Snort rules files...\n";
514 } else {
515     if ($ipt_sync) {
516         print "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=",
517             "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n",
518             sprintf("%-30s%-10s%-10s%-10s%-10s", '    Snort Rules File',
519                 'Success', 'Fail', 'Ipt_apply', 'Total'), "\n\n";
520     } else {
521         print "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=",
522             "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n",
523             sprintf("%-30s%-10s%-10s%-10s", '    Snort Rules File',
524                 'Success', 'Fail', 'Total'), "\n\n";
525     }
526 }
527
528 ### main subroutine to parse snort rules and add them to the
529 ### fwsnort.sh script.
530 &parse_snort_rules();
531
532 ### append all translated rules to the iptables-save formatted array
533 &save_format_append_rules();
534
535 ### jump packets (as appropriate) from the INPUT and
536 ### FORWARD chains to our fwsnort chains
537 &ipt_jump_chain() unless $no_ipt_jumps;
538
539 push @ipt_script_lines, qq|\n\$ECHO "[+] Finished."|, '### EOF ###';
540 push @ipt_save_script_lines, $ipt_script_lines[$#ipt_script_lines];
541
542 print "\n[+] Logfile: $config{'LOG_FILE'}\n";
543
544 if ($ipt_rule_ctr > 1) {
545
546     ### write the iptables script out to disk
547     &write_ipt_script();
548
549     if ($queue_mode or $nfqueue_mode) {
550         print "[+] Snort rule set directory for rules to be queued ",
551             "to userspace:\n        $config{'QUEUE_RULES_DIR'}\n";
552     }
553     print "[+] $ipt_str script (individual commands): " .
554         "$config{'FWSNORT_SCRIPT'}\n";
555
556 } else {
557     die "[-] No Snort rules could be translated, exiting\n";
558 }
559
560 &write_save_file();
561
562 &print_final_message();
563
564 exit 0;
565 #===================== end main ======================
566
567 sub parse_snort_rules() {
568
569     my @rfiles = ();
570
571     my $cwd = cwd();
572
573     if ($rules_file) {
574         @rfiles = split /\,/, $rules_file;
575     } else {
576         for my $dir (split /\,/, $config{'RULES_DIR'}) {
577             opendir D, $dir or die "[*] Could not opendir $dir";
578             for my $file (readdir D) {
579                 push @rfiles, "$dir/$file";
580             }
581             closedir D;
582         }
583     }
584
585     my $sabs_num = 0;
586     my $tot_ipt_apply = 0;
587     my $tot_unsup_ctr = 0;
588     FILE: for my $rfile (sort @rfiles) {
589         $rfile = $cwd . '/' . $rfile unless $rfile =~ m|^/|;
590         my $type = '';
591         my $filename = '';
592         if ($rfile =~ m|.*/(\S+\.rules)$|) {
593             $filename = $1;
594         }
595         if ($rfile =~ m|.*/(\S+)\.rules$|) {
596             $type = $1;
597         } else {
598             next FILE;
599         }
600         $ipt_print_type = 0;
601         if ($rules_types) {
602             next FILE unless defined $include_types{$type};
603         }
604         if ($exclude_types) {
605             next FILE if defined $exclude_types{$type};
606         }
607         if ($rfile eq 'deleted.rules') {
608             next FILE unless $add_deleted;
609         }
610         ($snort_type) = ($rfile =~ m|.*/(\S+)\.rules|);
611         printf("%-30s", "[+] $filename") unless $include_sids;
612
613         &logr("[+] Parsing $rfile");
614         open R, "< $rfile" or die "[*] Could not open: $rfile";
615         my @lines = <R>;
616         close R;
617
618         ### contains Snort rules that will be used by Snort_inline
619         ### if fwsnort is building a QUEUE policy; these rules have
620         ### met the criteria that at least one "content" match is
621         ### required.
622         my @queue_rules = ();
623
624         my $line_num   = 0;
625         my $rule_num   = 0;
626         my $parsed_ctr = 0;
627         my $unsup_ctr  = 0;
628         my $ipt_apply_ctr = 0;
629         my $ipt_rules_ctr = 0;
630
631         RULE: for my $rule (@lines) {
632             chomp $rule;
633             my $rule_hdr;
634             my $rule_options;
635             $line_num++;
636
637             ### pass == ACCEPT, log == ULOG
638             unless ($rule =~ /^\s*alert/ or $rule =~ /^\s*pass/
639                     or $rule =~ /^\s*log/) {
640                 next RULE;
641             }
642
643             ### regex filters
644             if ($exclude_re) {
645                 next RULE unless $rule =~ $exclude_re;
646             }
647
648             if ($include_re) {
649                 next RULE unless $rule =~ $include_re;
650             }
651
652             $rule_num++;  ### keep track of the abs num of rules
653             $sabs_num++;
654
655             if ($rule =~ m|^(.*?)\s+\((.*)\)|) {
656                 $rule_hdr     = $1;
657                 $rule_options = " $2 ";  ### allows out-of-order options
658             } else {
659                 &logr("[-] Unrecognized rule format at line: $line_num. " .
660                     "Skipping.");
661                 next RULE;
662             }
663
664             ### skip all icmp "Undefined Code" rules; psad properly
665             ### handles this, but not fwsnort (see the icmp-info.rules
666             ### file).
667             if ($filename =~ /icmp/ and $rule_options =~ /undefined\s+code/i) {
668                 $unsup_ctr++;
669                 $tot_unsup_ctr++;
670                 next RULE;
671             }
672
673             ### parse header portion of Snort rule
674             my $hdr_hr = &parse_rule_hdr($rule_hdr, $line_num);
675             unless (keys %$hdr_hr) {
676                 &logr("[-] Unrecognized rule header: \"$rule_hdr\" at " .
677                     "line: $line_num, skipping.");
678                 $unsup_ctr++;
679                 $tot_unsup_ctr++;
680                 next RULE;
681             }
682
683             ### parse options portion of Snort rule
684             my ($parse_rv, $opts_hr, $patterns_ar)
685                             = &parse_rule_options($rule_options,
686                                     &get_avg_hdr_len($hdr_hr),
687                                     $line_num);
688
689             unless ($parse_rv) {
690                 $unsup_ctr++;
691                 $tot_unsup_ctr++;
692                 next RULE;
693             }
694             if ($include_sids) {
695                 print "[+] Found sid: $opts_hr->{'sid'} in $filename\n";
696             }
697
698             if ($queue_mode or $nfqueue_mode) {
699
700                 ### In general, it is not easy to modify the signatures that
701                 ### snort_inline would use; one would think that an optimzation
702                 ### would be to remove all "content" keywords since the kernel
703                 ### itself is doing this now, but consider the following:
704
705                 ### Suppose there are two original Snort signatures like so:
706                 ###
707                 ###     msg: "SIG1"; content: "abc"; pcre: "(d|e)";
708                 ###     msg: "SIG2"; content: "xyz"; pcre: "(e|f)";
709                 ###
710                 ### Now, suppose there is a packet with the following data:
711                 ###
712                 ###     packet data: "xyz------------e------"
713                 ###
714                 ### Then the SIG1 matches when it shouldn't because the packet
715                 ### does not contain "abc" (assuming the "abc" string is
716                 ### removed from the signature that is actually deployed with
717                 ### snort_inline).  There does not seem to be a good solution
718                 ### for this problem if pcre criteria are involved because the
719                 ### two pcre's would have to be interpreted to see if there is
720                 ### any data that could satisfy both at the same time.
721
722                 ### However, performing the duplicate string matching is far
723                 ### less expensive than not sending a large portion of network
724                 ### traffic to userspace for analysis by snort_inline in the
725                 ### first place.  This is the real benefit of letting fwsnort
726                 ### build a smarter iptables queueing policy.  This does come
727                 ### with a penalty against detection, since snort_inline is
728                 ### only receiving individual packets that match one of the
729                 ### content keywords in a signature; it does not get the
730                 ### entire stream.  But, this may be worth it for large sites
731                 ### where performance is the primary concern.  Also, there is
732                 ### some potential for removing a subset of the content
733                 ### matches if done in the right way; this is the reason the
734                 ### queue_get_rule() function is stubbed in below.
735                 my $queue_rule = &queue_get_rule($rule_hdr, $rule_options);
736
737                 push @queue_rules, $queue_rule if $queue_rule;
738             }
739
740             ### construct the equivalent iptables rule and add it
741             ### to $config{'FWSNORT_SCRIPT'}
742             my ($ipt_rv, $num_rules) = &ipt_build($hdr_hr,
743                     $opts_hr, $patterns_ar, $rule);
744
745             if ($ipt_rv) {
746                 $ipt_apply_ctr++;
747                 $tot_ipt_apply++;
748                 ### may have the rule in several chains
749                 $ipt_rules_ctr += $num_rules;
750                 if ($include_sids) {
751                     print "    Successful translation.\n";
752                 }
753             } else {
754                 if ($include_sids) {
755                     print "    Unsuccessful translation.\n";
756                 }
757             }
758             $parsed_ctr++;  ### keep track of successfully parsed rules
759             $abs_num++;;
760         }
761
762         if (($queue_mode or $nfqueue_mode) and @queue_rules) {
763             open M, "> $config{'QUEUE_RULES_DIR'}/$filename" or die "[*] Could not ",
764                 "open $config{'QUEUE_RULES_DIR'}/$filename: $!";
765             print M "#\n### This file generated with: fwsnort @argv_cp\n#\n\n";
766             print M "$_\n", for @queue_rules;
767             print M "\n### EOF ###\n";
768             close F;
769         }
770
771         if ($ipt_rules_ctr) {
772             $ipt_rules_ctr *= 2 if $ipt_drop;
773             $ipt_rules_ctr *= 2 if $ipt_reject;
774             push @ipt_script_lines,
775                 qq|\$ECHO "    Rules added: $ipt_rules_ctr"|;
776         }
777
778         unless ($include_sids) {
779             if ($ipt_sync) {
780                 printf("%-10s%-10s%-10s%-10s\n", $parsed_ctr, $unsup_ctr,
781                     $ipt_apply_ctr, $rule_num);
782             } else {
783                 printf("%-10s%-10s%-10s\n", $parsed_ctr, $unsup_ctr,
784                     $rule_num);
785             }
786         }
787     }
788     unless ($include_sids) {
789         if ($ipt_sync) {
790             printf("%30s", ' ');
791             print "=======================================\n";
792             printf("%30s%-10s%-10s%-10s%-10s\n", ' ',
793                 $abs_num, $tot_unsup_ctr, $tot_ipt_apply, $sabs_num);
794         } else {
795             printf("%30s", ' ');
796             print "=============================\n";
797             printf("%30s%-10s%-10s%-10s\n", ' ',
798                 $abs_num, $tot_unsup_ctr, $sabs_num);
799         }
800         print "\n";
801         if ($abs_num) {  ### we parsed at least one rule
802             print "[+] Generated $ipt_str rules for $abs_num out of ",
803                 "$sabs_num signatures: ",
804                 sprintf("%.2f", $abs_num/$sabs_num*100), "%\n";
805         } else {
806             print "[+] No rules parsed.\n";
807         }
808         if ($ipt_sync) {
809             print "[+] Found $tot_ipt_apply applicable snort rules to your " .
810                 "current $ipt_str\n    policy.\n";
811         }
812     }
813     return;
814 }
815
816 sub parse_rule_options() {
817     my ($rule_options, $avg_hdr_len, $line_num) = @_;
818
819     my $sid      = -1;
820     my %opts     = ();
821     my @patterns = ();
822
823     ### get the sid here for logging purposes
824     if ($rule_options =~ /$snort_opts{'logprefix'}{'sid'}/) {
825         $sid = $1;
826     } else {
827         return 0, \%opts, \@patterns;
828     }
829
830     if (%exclude_sids) {
831         return 0, \%opts, \@patterns if defined $exclude_sids{$sid};
832     }
833     if (%include_sids) {
834         if (defined $include_sids{$sid}) {
835             &logr("[+] matched sid:$sid: $rule_options");
836         } else {
837             return 0, \%opts, \@patterns;
838         }
839     }
840
841     unless ($queue_mode or $nfqueue_mode) {
842
843         ### if we're queuing packets to userspace Snort, then we don't have to
844         ### disqualify a signature based on an option that is not supported by
845         ### iptables
846         my $found_unsupported = '';
847         for my $opt (keys %{$snort_opts{'unsupported'}}) {
848             ### see if we match a regex belonging to an unsupported option
849             if ($rule_options =~ /$snort_opts{'unsupported'}{$opt}/) {
850                 $found_unsupported .= "'$opt', ";
851             }
852         }
853         if ($found_unsupported) {
854             $found_unsupported =~ s/,\s+$//;
855             &logr("[-] SID: $sid  Unsupported option(s): $found_unsupported " .
856                 "at line: $line_num, skipping.");
857             if (%include_sids and defined $include_sids{$sid}) {
858                 print "[-] SID: $sid contain the unsupported option(s): ",
859                     "$found_unsupported at line: $line_num\n";
860             }
861             return 0, \%opts, \@patterns;
862         }
863     }
864
865     if ($rule_options =~ /ip_proto\s*:.*ip_proto\s*:/) {
866         &logr("[-] SID: $sid, unsupported multiple ip_proto fields at " .
867             "line: $line_num, skipping.");
868         return 0, \%opts, \@patterns;
869     }
870
871     for my $opt (keys %{$snort_opts{'filter'}}) {
872         ### see if we match the option regex
873         if ($rule_options =~ /$snort_opts{'filter'}{$opt}{'regex'}/) {
874             $opts{$opt} = 1;
875             $opts{$opt} = $1 if defined $1;  ### some keywords may not have an option
876         }
877     }
878
879     my $found_content = 0;
880     while ($rule_options =~ /(\w+):?\s*((?:.*?[^\x5c]?))\s*;/g) {
881         my $opt = $1;
882         my $val = 1;
883         $val = $2 if defined $2;  ### some keywords may not have an argument
884
885         if ($opt eq 'content' or $opt eq 'uricontent') {
886             return 0, \%opts, \@patterns unless $val =~ /"$/;
887             $val =~ s/^\s*"//;
888             $val =~ s/"\s*$//;
889             return 0, \%opts, \@patterns unless $val =~ /\S/;
890
891             ### convert the string into a form that is more compatible
892             ### for iptables
893             my ($rv, $log_str, $ipt_pattern_hr)
894                     = &convert_pattern_for_iptables($val);
895
896             if ($rv) {
897                 $found_content = 1;
898                 push @patterns, $ipt_pattern_hr;
899             } else {
900                 &logr("[-] SID: $sid, $log_str");
901                 return 0, \%opts, \@patterns;
902             }
903
904         } elsif ($opt eq 'pcre') {
905
906             $val =~ s|^\s*"/||;
907             $val =~ s|/\w{0,3}"$||;
908
909             ### see if this pcre only has strings separated with ".*" or ".+"
910             ### and if so translate to multple string matches
911             my ($pcre_rv, $pcre_strings_ar) = &parse_pcre($val);
912             if ($pcre_rv) {
913                 for my $str (@$pcre_strings_ar) {
914                     push @patterns, $str;
915                 }
916             } else {
917                 unless ($queue_mode or $nfqueue_mode) {
918                     &logr("[-] SID: $sid, unsupported complex pcre: $val");
919                     return 0, \%opts, \@patterns;
920                 }
921             }
922
923         } elsif ($opt eq 'fast_pattern') {
924             if ($no_fast_pattern_order) {
925                 ### force it to be the first pattern so no reordering
926                 ### will happen
927                 $patterns[0]->{'fast_pattern'} = 1;
928             } else {
929                 $patterns[$#patterns]->{'fast_pattern'} = 1;
930             }
931         } elsif ($opt eq 'nocase') {
932             unless (defined $snort_opts{'ignore'}{'nocase'}) {
933                 $patterns[$#patterns]->{'nocase'} = 1;
934             }
935         } else {
936             for my $key (qw(offset depth within distance)) {
937                 if ($opt eq $key) {
938                     my ($offsets_rv, $log_str)
939                             = &define_offsets(\@patterns,
940                                 $avg_hdr_len, $key, $val);
941                     unless ($offsets_rv) {
942                         &logr("[-] SID: $sid, $log_str");
943                         return 0, \%opts, \@patterns;
944                     }
945                     last;
946                 }
947             }
948         }
949     }
950
951     if (($queue_mode or $nfqueue_mode) and not $found_content) {
952         my $queue_str = 'QUEUE';
953         $queue_str = 'NFQUEUE' if $nfqueue_mode;
954         &logr("[-] SID: $sid  In --$queue_str mode signature must have " .
955                 "'content' or 'uricontent' keyword " .
956                 "at line: $line_num, skipping.");
957         if (%include_sids and defined $include_sids{$sid}) {
958             print "[-] SID: $sid does not contain 'content' ",
959                   "or 'uricontent'\n";
960         }
961         return 0, \%opts, \@patterns;
962     }
963
964     ### update offset, depth, within, and distance values for relative
965     ### matches
966     my ($offsets_rv, $log_str) = &update_offsets_relative_matches(\@patterns);
967     unless ($offsets_rv) {
968         &logr("[-] SID: $sid, $log_str");
969         return 0, \%opts, \@patterns;
970     }
971
972     for my $opt (keys %{$snort_opts{'logprefix'}}) {
973         if ($rule_options =~ /$snort_opts{'logprefix'}{$opt}/) {
974             $opts{$opt} = $1;
975         }
976     }
977
978     unless ($queue_mode or $nfqueue_mode) {
979         while ($rule_options =~ /(\w+):\s*.*?;/g) {
980             my $option = $1;
981             if (not defined $opts{$option}
982                     and not defined $snort_opts{'ignore'}{$option}) {
983                 &logr("[-] SID: $sid bad option: \"$option\" at line: $line_num " .
984                     "-- $rule_options");
985                 return 0, \%opts, \@patterns;
986             }
987         }
988
989         if (defined $opts{'ipopts'}
990                 and $opts{'ipopts'} ne 'rr'
991                 and $opts{'ipopts'} ne 'ts'
992                 and $opts{'ipopts'} ne 'ssrr'
993                 and $opts{'ipopts'} ne 'lsrr'
994                 and $opts{'ipopts'} ne 'any') {
995             &logr("[-] SID: $sid, unsupported ipopts field at " .
996                 "line: $line_num, skipping.");
997             return 0, \%opts, \@patterns;
998         }
999
1000         if (defined $opts{'itype'}
1001                 and ($opts{'itype'} =~ m|<| or $opts{'itype'} =~ m|>|)) {
1002             &logr("[-] SID: $sid, unsupported range operator in itype field " .
1003                 "line: $line_num, skipping.");
1004             return 0, \%opts, \@patterns;
1005         }
1006         if (defined $opts{'icode'}
1007                 and ($opts{'icode'} =~ m|<| or $opts{'icode'} =~ m|>|)) {
1008             &logr("[-] SID: $sid, unsupported range operator in icode field " .
1009                 "line: $line_num, skipping.");
1010             return 0, \%opts, \@patterns;
1011         }
1012         if (defined $opts{'ip_proto'}
1013                 and ($opts{'ip_proto'} =~ m|<| or $opts{'ip_proto'} =~ m|>|)) {
1014             &logr("[-] SID: $sid, unsupported range operator in ip_proto field " .
1015                 "line: $line_num, skipping.");
1016             return 0, \%opts, \@patterns;
1017         }
1018     }
1019
1020     ### success
1021     return 1, \%opts, \@patterns;
1022 }
1023
1024 sub parse_rule_hdr() {
1025     my ($rule_hdr, $line_num) = @_;
1026     my $bidir = 0;
1027     my $action = 'alert';  ### default
1028     if ($rule_hdr =~ /^\s*pass/) {
1029         $action = 'pass';
1030     } elsif ($rule_hdr =~ /^\s*log/) {
1031         $action = 'log';
1032     }
1033     if ($rule_hdr =~ m|^\s*\w+\s+(\S+)\s+(\S+)\s+(\S+)
1034                         \s+(\S+)\s+(\S+)\s+(\S+)|ix) {
1035         my $proto  = lc($1);
1036         my $src    = $2;
1037         my $sport  = $3;
1038         my $bidir  = $4;
1039         my $dst    = $5;
1040         my $dport  = $6;
1041
1042         unless ($proto =~ /^\w+$/) {
1043             &logr("[-] Unsupported protocol: \"$proto\" at line: " .
1044                 "$line_num, skipping.");
1045             return {};
1046         }
1047
1048         ### in --ip6tables mode make sure we're not looking at IPv4 addresses
1049         if ($enable_ip6tables
1050                 and ($src =~ /\b$ip_re\b/ or $dst =~ /\b$ip_re\b/)) {
1051             &logr("[-] --ip6tables mode enabled but IPv4 " .
1052                 "address in rule variable at line: $line_num.");
1053             return {};
1054         }
1055
1056         ### in --ip6tables mode exclude the icmp protocol - maybe should
1057         ### change to icmp6 in the future
1058         if ($enable_ip6tables and $proto eq 'icmp') {
1059             &logr("[-] --ip6tables mode enabled, so excluding " .
1060                 "icmp (non-icmp6) siganture at line: $line_num.");
1061             return {};
1062         }
1063
1064         my $bidir_flag = 0;
1065         $bidir_flag = 1 if $bidir eq '<>';
1066
1067         my %hsh = (
1068             'action' => $action,
1069             'proto'  => $proto,
1070             'src'    => $src,
1071             'sport'  => $sport,
1072             'bidir'  => $bidir_flag,
1073             'dst'    => $dst,
1074             'dport'  => $dport,
1075         );
1076
1077         ### map to expanded values (e.g. $HOME -> "any" or whatever
1078         ### is defined in fwsnort.conf)
1079         for my $var (qw(src sport dst dport)) {
1080             my $val = $hsh{$var};
1081             my $negate_flag = 0;
1082             $negate_flag = 1 if $val =~ m|!|;
1083             while ($val =~ /\$(\w+)/) {
1084                 $val = $1;
1085                 if (defined $config{$val}) {
1086                     $val = $config{$val};
1087                     if ($enable_ip6tables and $val =~ /\b$ip_re\b/) {
1088                         &logr("[-] --ip6tables mode enabled but IPv4 " .
1089                             "address in rule variable at line: $line_num.");
1090                         return {};
1091                     }
1092                 } else {
1093                     &logr("[-] Undefined variable $val in rule header " .
1094                         "at line: $line_num.");
1095                     return {};
1096                 }
1097             }
1098             if ($negate_flag and $val !~ m|!|) {
1099                 $hsh{$var} = "!$val";
1100             } else {
1101                 $hsh{$var} = $val;
1102             }
1103         }
1104
1105         for my $var (qw(sport dport)) {
1106             next unless $hsh{$var} =~ /,/;
1107             if ($ipt_have_multiport_match) {
1108                 $hsh{$var} =~ s/\[//;
1109                 $hsh{$var} =~ s/\]//;
1110                 my $ctr = 1;
1111                 my @ports = split /\s*,\s*/, $hsh{$var};
1112                 my $ports_str = '';
1113                 for my $port (@ports) {
1114                     if ($port =~ /\d+:$/) {
1115                         $ports_str .= "${port}65535,";
1116                     } else {
1117                         $ports_str .= "${port},";
1118                     }
1119                     $ctr++;
1120                     $ctr++ if $port =~ /\:/;  ### a range counts for two ports
1121                     ### multiport is limited to 15 ports
1122                     last if $ctr >= $ipt_multiport_max;
1123                 }
1124                 $ports_str =~ s/,$//;
1125                 $hsh{$var} = $ports_str;
1126             } else {
1127                 &logr("[-] Warning: taking the first port in the list " .
1128                     "$hsh{$var} until the $ipt_str multiport match is supported " .
1129                     "at line: $line_num.");
1130                 $hsh{$var} =~ s/,.*//;
1131                 $hsh{$var} =~ s/\[//;
1132                 $hsh{$var} =~ s/\]//;
1133             }
1134         }
1135
1136         return \%hsh;
1137     }
1138     return {};
1139 }
1140
1141 sub parse_pcre() {
1142     my $pcre = shift;
1143     my $rv = 0;
1144     my @patterns = ();
1145
1146     if ($pcre =~ m|^\w+$|) {
1147         push @patterns, (&convert_pattern_for_iptables($pcre))[2];
1148         $rv = 1;
1149     } elsif ($pcre =~ m|UNION\x5c\x73\x2bSELECT|) {
1150         ### a bunch of Emerging Threats rules contain "UNION\s+SELECT"
1151         ### as a PCRE.  Sure, the translation below can be evaded, but
1152         ### it is better than nothing.
1153         push @patterns, (&convert_pattern_for_iptables('UNION SELECT'))[2];
1154         $rv = 1;
1155     } else {
1156         my @ar = ();
1157         if ($pcre =~ m|\.\*|) {
1158             @ar = split /\.\*/, $pcre;
1159             $rv = 1;
1160         } elsif ($pcre =~ m|\.\+|) {
1161             @ar = split /\.\+/, $pcre;
1162             $rv = 1;
1163         } elsif ($pcre =~ m|\x5b\x5e\x5c\x6e\x5d\x2b|) {  ### [^\n]+
1164             @ar = split /\x5b\x5e\x5c\x6e\x5d\x2b/, $pcre;
1165             $rv = 1;
1166         }
1167         if ($rv == 1) {
1168             for my $part (@ar) {
1169                 next unless $part;  ### some Snort pcre's begin with .* or .+
1170                                     ### (which seems useless)
1171
1172                 ### Replace "\(" with hex equivalent in PCRE's
1173                 ### like: /.+ASCII\(.+SELECT/
1174                 $part =~ s/\x5c\x28/|5c 28|/;
1175
1176                 ### Replace "\:" with hex equivalent in PCRE's
1177                 ### like: /User-Agent\:[^\n]+spyaxe/
1178                 $part =~ s/\x5c\x3a/|5c 3a|/;
1179
1180                 my $basic = $part;
1181                 $basic =~ s/\|5c 28\|//;
1182                 $basic =~ s/\|5c 3a\|//;
1183
1184                 if ($basic =~ /^[\w\x20]+$/) {
1185                     push @patterns, (&convert_pattern_for_iptables($part))[2];
1186                 } elsif ($basic eq 'User-Agent') {
1187                     push @patterns, (&convert_pattern_for_iptables($part))[2];
1188                 } else {
1189                     $rv = 0;
1190                 }
1191             }
1192         }
1193     }
1194     return $rv, \@patterns;
1195 }
1196
1197 sub queue_get_rule() {
1198     my ($rule_hdr, $rule_opts) = @_;
1199
1200     ### FIXME: the following commented out code would need to be
1201     ### drastically improved to ensure that the remaining signatures
1202     ### are completely unique in userspace.  For now, just return
1203     ### the original Snort rule
1204     ###     Remove all of the following keywords since they are handled
1205     ###     within the kernel directly.
1206 #    for my $key qw/uricontent content offset depth within distance/ {
1207 #        $rule_opts =~ s/([\s;])$key:\s*.*?\s*;\s*/$1/g;
1208 #    }
1209
1210     $rule_opts =~ s/^\s*//;
1211     $rule_opts =~ s/\s*$//;
1212
1213     return "$rule_hdr ($rule_opts)";
1214 }
1215
1216 sub ipt_allow_traffic() {
1217     my ($hdr_hr, $opts_hr, $chain, $orig_snort_rule) = @_;
1218
1219     my $rule_ctr = 0;
1220
1221     if ($dump_snort) {
1222         print "\n[+] Snort rule: $orig_snort_rule"
1223                 unless defined $snort_dump_cache{$orig_snort_rule};
1224         $snort_dump_cache{$orig_snort_rule} = '';
1225     }
1226
1227     ### check to see if the header is allowed through the chain,
1228     ### and if not we don't really care about matching traffic
1229     ### because iptables doesn't allow it anyway
1230     RULE: for my $rule_hr (@{$ipt_policy{$chain}}) {
1231         $rule_ctr++;
1232
1233         if ($dumper and $verbose) {
1234             print "[+] RULE: $rule_ctr:\n",
1235                 Dumper($rule_hr);
1236         }
1237         if ($dump_ipt) {
1238             print "[+] $ipt_str rule: $rule_hr->{'raw'}\n"
1239                 unless defined $ipt_dump_cache{$rule_hr->{'raw'}};
1240             $ipt_dump_cache{$rule_hr->{'raw'}} = '';
1241         }
1242
1243         ### don't match on rules to/from the loopback interface
1244         unless ($no_exclude_loopback) {
1245             if ($rule_hr->{'intf_in'} eq 'lo'
1246                     or $rule_hr->{'intf_out'} eq 'lo') {
1247                 print "[-] Skipping $chain rule $rule_ctr: loopback rule\n"
1248                     if $debug;
1249                 next RULE;
1250             }
1251         }
1252
1253         ### don't match on rules that build state
1254         if ($rule_hr->{'extended'} =~ /state/) {
1255             print "[-] Skipping $chain rule $rule_ctr: state rule\n"
1256                 if $debug;
1257             next RULE;
1258         }
1259
1260         ### match protocol
1261         unless (($hdr_hr->{'proto'} eq $rule_hr->{'proto'}
1262                 or $rule_hr->{'proto'} eq 'all')) {
1263             print "[-] Skipping $chain rule $rule_ctr: $hdr_hr->{'proto'} ",
1264                 "!= $rule_hr->{'proto'}\n" if $debug;
1265             next RULE;
1266         }
1267
1268         ### match src/dst IP/network
1269         unless (&match_addr($hdr_hr->{'src'}, $rule_hr->{'src'})) {
1270             print "[-] Skipping $chain rule $rule_ctr: src $hdr_hr->{'src'} ",
1271                 "not part of $rule_hr->{'src'}\n" if $debug;
1272             next RULE;
1273         }
1274         unless (&match_addr($hdr_hr->{'dst'}, $rule_hr->{'dst'})) {
1275             print "[-] Skipping $chain rule $rule_ctr: dst $hdr_hr->{'dst'} ",
1276                 "not part of $rule_hr->{'dst'}\n" if $debug;
1277             next RULE;
1278         }
1279
1280         ### match src/dst ports
1281         if ($hdr_hr->{'proto'} ne 'icmp') {
1282             unless (&match_port($hdr_hr->{'sport'},
1283                     $rule_hr->{'sport'})) {
1284                 print "[-] Skipping $chain rule $rule_ctr: sport ",
1285                     "$hdr_hr->{'sport'} not part of $rule_hr->{'sport'}\n"
1286                     if $debug;
1287                 next RULE;
1288             }
1289             unless (&match_port($hdr_hr->{'dport'},
1290                     $rule_hr->{'dport'})) {
1291                 print "[-] Skipping $chain rule $rule_ctr: dport ",
1292                     "$hdr_hr->{'dport'} not part of $rule_hr->{'dport'}\n"
1293                     if $debug;
1294                 next RULE;
1295             }
1296         }
1297
1298         if (defined $opts_hr->{'flow'} and $rule_hr->{'state'}) {
1299             if ($opts_hr->{'flow'} eq 'established') {
1300                 unless ($rule_hr->{'state'} =~ /ESTABLISHED/) {
1301                     print "[-] Skipping $chain rule $rule_ctr: state ",
1302                         "$opts_hr->{'flow'} not part of $rule_hr->{'state'}\n"
1303                         if $debug;
1304                     next RULE;
1305                 }
1306             }
1307         }
1308
1309         ### if we make it here, then this rule matches the signature
1310         ### (from a header perspective)
1311         if ($rule_hr->{'target'} eq 'DROP'
1312                 or $rule_hr->{'target'} eq 'REJECT') {
1313
1314             print "[-] Matching $ipt_str rule has DROP or REJECT target; ",
1315                 "$ipt_str policy does not allow this Snort rule.\n"
1316                 if $debug;
1317             if ($dumper) {
1318                 print "\n[-] RULE $chain DROP:\n",
1319                     Dumper($hdr_hr),
1320                     Dumper($opts_hr),
1321                     Dumper($rule_hr),
1322                     "\n";
1323             }
1324             return 0;
1325         } elsif ($rule_hr->{'target'} eq 'ACCEPT') {
1326             if ($dumper) {
1327                 print "\n[+] RULE $chain ACCEPT:\n",
1328                     Dumper($hdr_hr),
1329                     Dumper($opts_hr),
1330                     Dumper($rule_hr),
1331                     "\n";
1332             }
1333             print "[-] Matching $ipt_str rule has ACCEPT target; ",
1334                 "$ipt_str policy allows this Snort rule.\n" if $debug;
1335             return 1;
1336         }  ### we don't support other targets besides DROP, REJECT,
1337            ### or ACCEPT for now.
1338     }
1339
1340     ### if we make it here, then no specific ACCEPT rule matched the header,
1341     ### so return false if the chain policy is set to DROP (or there is
1342     ### a default drop rule). Otherwise there is no rule that would block
1343     ### the traffic.
1344     if (defined $ipt_default_policy_setting{$chain}) {
1345         if ($ipt_default_policy_setting{$chain} eq 'ACCEPT') {
1346             if (defined $ipt_default_drop{$chain}) {
1347                 if (defined $ipt_default_drop{$chain}{'all'}) {
1348                     print "[-] Default DROP rule applies to this Snort rule.\n"
1349                         if $debug;
1350                     return 0;
1351                 } elsif (defined $ipt_default_drop{$chain}
1352                         {$hdr_hr->{'proto'}}) {
1353                     print "[-] Default DROP rule applies to this Snort rule.\n"
1354                         if $debug;
1355                     return 0;
1356                 }
1357             }
1358             if ($dumper) {
1359                 print "\nACCEPT $chain, no $ipt_str matching rule\n",
1360                     Dumper($hdr_hr),
1361                     Dumper($opts_hr),
1362                     "\n";
1363             }
1364             return 1;
1365         }
1366     }
1367     if ($dumper) {
1368         print "\nDROP $chain, no $ipt_str matching rule\n",
1369             Dumper($hdr_hr),
1370             Dumper($opts_hr),
1371             "\n";
1372     }
1373
1374     ### maybe a "strict" option should be added here?
1375     return 0;
1376 }
1377
1378 sub match_addr() {
1379     my ($hdr_src, $rule_src) = @_;
1380     return 1 if $rule_src eq '0.0.0.0/0';
1381     return 1 if $hdr_src =~ /any/i;
1382     return 1 if $hdr_src eq $rule_src;
1383
1384     my $ipt_ip   = '';
1385     my $ipt_mask = '32';
1386     my $negate = 0;
1387
1388     my $s_obj   = '';
1389     my $ipt_obj = '';
1390
1391     $negate = 1 if $hdr_src =~ /\!/;
1392
1393     if ($rule_src =~ /\!/) {
1394         if ($negate) {
1395             ### if both hdr_src and rule_src are negated
1396             ### then revert to normal match.
1397             $negate = 0;
1398         } else {
1399             $negate = 1;
1400         }
1401     }
1402
1403     if ($rule_src =~ m|($ip_re)/($ip_re)|) {
1404         $ipt_ip   = $1;
1405         $ipt_mask = $2;
1406     } elsif ($rule_src =~ m|($ip_re)/(\d+)|) {
1407         $ipt_ip   = $1;
1408         $ipt_mask = $2;
1409     } elsif ($rule_src =~ m|($ip_re)|) {
1410         $ipt_ip = $1;
1411     }
1412
1413     $ipt_obj = new NetAddr::IP($ipt_ip, $ipt_mask);
1414
1415     for my $addr (@{&expand_addresses($hdr_src)}) {
1416         my $src_ip   = '';
1417         my $src_mask = '32';
1418         if ($addr =~ m|($ip_re)/($ip_re)|) {
1419             $src_ip   = $1;
1420             $src_mask = $2;
1421         } elsif ($addr =~ m|($ip_re)/(\d+)|) {
1422             $src_ip   = $1;
1423             $src_mask = $2;
1424         } elsif ($addr =~ m|($ip_re)|) {
1425             $src_ip = $1;
1426         }
1427         $s_obj = new NetAddr::IP($src_ip, $src_mask);
1428         if ($negate) {
1429             return 1 unless $ipt_obj->within($s_obj);
1430         } else {
1431             return 1 if $ipt_obj->within($s_obj);
1432         }
1433     }
1434     return 0;
1435 }
1436
1437 sub match_port() {
1438     my ($snort_port, $ipt_port) = @_;
1439     return 1 if $ipt_port eq '0:0';
1440     return 1 if $snort_port =~ /any/i;
1441     return 1 if $ipt_port eq $snort_port;
1442     my $ipt_start = 0;
1443     my $ipt_end   = 65535;
1444     my $h_start   = 0;
1445     my $h_end     = 65535;
1446
1447     if ($ipt_port =~ /:/) {
1448         if ($ipt_port =~ /(\d+):/) {
1449             $ipt_start = $1;
1450         }
1451         if ($ipt_port =~ /:(\d+)/) {
1452             $ipt_end = $1;
1453         }
1454     } elsif ($ipt_port =~ /(\d+)/) {
1455         $ipt_start = $ipt_end = $1;
1456     }
1457
1458     if ($snort_port =~ /:/) {
1459         if ($snort_port =~ /(\d+):/) {
1460             $h_start = $1;
1461         }
1462         if ($snort_port =~ /:(\d+)/) {
1463             $h_end = $1;
1464         }
1465     } elsif ($snort_port =~ /(\d+)/) {
1466         $h_start = $h_end = $1;
1467     }
1468
1469     if ($ipt_port =~ /!/) {
1470         if ($snort_port =~ /!/) {
1471             return 0;
1472         } else {
1473             return 1 if (($h_start < $ipt_start and $h_end < $ipt_start)
1474                     or ($h_start > $ipt_end and $h_end > $ipt_end));
1475         }
1476     } else {
1477         if ($snort_port =~ /!/) {
1478             return 1 if (($ipt_start < $h_start and $ipt_end < $h_start)
1479                     or ($ipt_start > $h_end and $ipt_end > $h_end));
1480         } else {
1481             return 1 if $h_start >= $ipt_start and $h_end <= $ipt_end;
1482         }
1483     }
1484     return 0;
1485 }
1486
1487 sub cache_ipt_policy() {
1488
1489     my $ipt = new IPTables::Parse 'iptables' => $cmds{'iptables'}
1490         or die "[*] Could not acquire IPTables::Parse object: $!";
1491
1492     for my $chain (keys %process_chains) {
1493         next unless $process_chains{$chain};
1494
1495         $ipt_policy{$chain} = $ipt->chain_rules('filter',
1496             $chain, $ipt_file);
1497
1498         $ipt_default_policy_setting{$chain}
1499             = $ipt->chain_policy('filter', $chain, $ipt_file);
1500
1501         my ($def_drop_hr, $ipt_rv)
1502             = $ipt->default_drop('filter', $chain, $ipt_file);
1503
1504         if ($ipt_rv) {
1505             $ipt_default_drop{$chain} = $def_drop_hr;
1506         }
1507     }
1508     return;
1509 }
1510
1511 sub ipt_build() {
1512     my ($snort_hdr_hr, $snort_opts_hr, $patterns_ar, $orig_snort_rule) = @_;
1513
1514     my $found_rule = 0;
1515     my $num_rules  = 0;
1516
1517     my %process_rules = ();
1518
1519     ### define iptables source and destination
1520     if ($snort_hdr_hr->{'dst'} =~ /any/i) {
1521         if ($snort_hdr_hr->{'src'} =~ /any/i) {
1522             push @{$process_rules{'INPUT'}}, '' if $process_chains{'INPUT'};
1523             push @{$process_rules{'FORWARD'}}, ''
1524                 if $process_chains{'FORWARD'};
1525         } else {
1526             my $addr_ar = &expand_addresses($snort_hdr_hr->{'src'});
1527             my $negate = '';
1528             $negate = '! ' if $snort_hdr_hr->{'src'} =~ m|!|;
1529             unless ($addr_ar) {
1530                 &logr("[-] No valid source IPs/networks in Snort " .
1531                     "rule header.");
1532                 return 0, 0;
1533             }
1534             for my $src (@$addr_ar) {
1535                 if (&is_local($src)) {
1536                     push @{$process_rules{'OUTPUT'}},
1537                             "$negate$ipt_hdr_opts{'src'} ${src}"
1538                             if $process_chains{'OUTPUT'};
1539                 } else {
1540                     push @{$process_rules{'INPUT'}},
1541                             "$negate$ipt_hdr_opts{'src'} ${src}"
1542                             if $process_chains{'INPUT'};
1543                 }
1544                 push @{$process_rules{'FORWARD'}},
1545                         "$negate$ipt_hdr_opts{'src'} ${src}"
1546                         if $process_chains{'FORWARD'};
1547             }
1548         }
1549     } else {
1550         my $dst_addr_ar = &expand_addresses($snort_hdr_hr->{'dst'});
1551         unless ($dst_addr_ar) {
1552             &logr("[-] No valid destination IPs/networks in Snort rule " .
1553                 "header.");
1554             return 0, 0;
1555         }
1556         if ($snort_hdr_hr->{'src'} =~ /any/i) {
1557             my $negate = '';
1558             $negate = '! ' if $snort_hdr_hr->{'dst'} =~ m|!|;
1559             for my $dst (@$dst_addr_ar) {
1560                 if (&is_local($dst)) {
1561                     push @{$process_rules{'INPUT'}},
1562                             "$negate$ipt_hdr_opts{'dst'} ${dst}"
1563                             if $process_chains{'INPUT'};
1564                 } else {
1565                     push @{$process_rules{'OUTPUT'}},
1566                             "$negate$ipt_hdr_opts{'dst'} ${dst}"
1567                             if $process_chains{'OUTPUT'};
1568                 }
1569                 push @{$process_rules{'FORWARD'}},
1570                         "$negate$ipt_hdr_opts{'dst'} ${dst}"
1571                         if $process_chains{'FORWARD'};
1572             }
1573         } else {
1574             my $src_addr_ar = &expand_addresses($snort_hdr_hr->{'src'});
1575             my $negate_src = '';
1576             $negate_src = '! ' if $snort_hdr_hr->{'src'} =~ m|!|;
1577             my $negate_dst = '';
1578             $negate_dst = '! ' if $snort_hdr_hr->{'dst'} =~ m|!|;
1579             unless ($src_addr_ar) {
1580                 &logr("[-] No valid source IPs/networks in Snort rule " .
1581                     "header.");
1582                 return 0, 0;
1583             }
1584             for my $src (@$src_addr_ar) {
1585                 for my $dst (@$dst_addr_ar) {
1586                     if (&is_local($dst)) {
1587                         push @{$process_rules{'INPUT'}},
1588                             "$negate_src$ipt_hdr_opts{'src'} ${src}" .
1589                             " $negate_dst$ipt_hdr_opts{'dst'} ${dst}"
1590                             if $process_chains{'INPUT'};
1591                     } else {
1592                         push @{$process_rules{'OUTPUT'}},
1593                             "$negate_src$ipt_hdr_opts{'src'} ${src}" .
1594                             " $negate_dst$ipt_hdr_opts{'dst'} ${dst}"
1595                             if $process_chains{'OUTPUT'};
1596                     }
1597                     push @{$process_rules{'FORWARD'}},
1598                         "$negate_src$ipt_hdr_opts{'src'} ${src}" .
1599                         " $negate_dst$ipt_hdr_opts{'dst'} ${dst}"
1600                         if $process_chains{'FORWARD'};
1601                 }
1602             }
1603         }
1604     }
1605
1606     ### determine which chain (e.g. stateful/stateless)
1607     my $flow_established = '';
1608     unless ($no_ipt_conntrack) {
1609         if (defined $snort_hdr_hr->{'proto'}
1610                 and $snort_hdr_hr->{'proto'} =~ /tcp/i
1611                 and defined $snort_opts_hr->{'flow'}
1612                 and $snort_opts_hr->{'flow'} =~ /established/i) {
1613             $flow_established = 'ESTABLISHED';
1614         }
1615     }
1616
1617     my $add_snort_comment = 1;
1618     my $add_perl_trigger  = 1;
1619     for my $chain (keys %process_chains) {
1620
1621         next unless $process_chains{$chain} and $process_rules{$chain};
1622
1623         for my $src_dst (@{$process_rules{$chain}}) {
1624
1625             my $rule = "\$${ipt_var_str} -A ";
1626             my $save_rule = '-A';
1627             my $fwsnort_chain = '';
1628
1629             ### see if we can jump to the ESTABLISHED inspection chain.
1630             if ($flow_established) {
1631                 $rule .= $config{"FWSNORT_${chain}_ESTAB"};
1632                 $fwsnort_chain = $config{"FWSNORT_${chain}_ESTAB"};
1633             } else {
1634                 $rule .= $config{"FWSNORT_$chain"};
1635                 $fwsnort_chain = $config{"FWSNORT_$chain"};
1636             }
1637
1638             ### append interface restriction if necessary
1639             if ($src_dst =~ m|127\.0\.0\.\d/|) {
1640                 if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
1641                     $rule .= ' ! -i lo';
1642                 } elsif ($chain eq 'OUTPUT') {
1643                     $rule .= ' ! -o lo';
1644                 }
1645             }
1646
1647             ### append source and destination criteria
1648             if ($src_dst) {
1649                 if ($chain eq 'FORWARD') {
1650                     $rule .= " $src_dst";
1651                 } elsif ($chain eq 'INPUT') {
1652                     ### we always treat the INPUT chain as part of the HOME_NET;
1653                     ### the system running iptables may have an interface on the
1654                     ### external network and hence may not be part of the HOME_NET
1655                     ### as defined in the fwsnort.conf file so we don't necessarily
1656                     ### append the IP criteria
1657                     if ($src_dst ne "$ipt_hdr_opts{'dst'} $config{'HOME_NET'}") {
1658                         $rule .= " $src_dst";
1659                     }
1660                 } elsif ($chain eq 'OUTPUT') {
1661                     if ($src_dst ne "$ipt_hdr_opts{'src'} $config{'HOME_NET'}") {
1662                         $rule .= " $src_dst";
1663                     }
1664                 }
1665             }
1666
1667             my $rv = &ipt_build_rule(
1668                 $chain,
1669                 $fwsnort_chain,
1670                 $rule,
1671                 $snort_hdr_hr,
1672                 $snort_opts_hr,
1673                 $patterns_ar,
1674                 $orig_snort_rule,
1675                 $flow_established,
1676                 $add_snort_comment,
1677                 $add_perl_trigger
1678             );
1679             if ($rv) {
1680                 $found_rule        = 1;
1681                 $add_snort_comment = 0;
1682                 $add_perl_trigger  = 0;
1683                 $num_rules++;
1684             }
1685         }
1686     }
1687     return $found_rule, $num_rules;
1688 }
1689
1690 sub is_local() {
1691     my $addr = shift;
1692
1693     return 1 if $no_addr_check;
1694
1695     my $ip   = '';
1696     my $mask = '32';
1697
1698     if ($addr =~ m|($ip_re)/($ip_re)|) {
1699         $ip   = $1;
1700         $mask = $2;
1701     } elsif ($addr =~ m|($ip_re)/(\d+)|) {
1702         $ip   = $1;
1703         $mask = $2;
1704     } elsif ($addr =~ m|($ip_re)|) {
1705         $ip = $1;
1706     }
1707
1708     my $ip_obj = new NetAddr::IP($ip, $mask);
1709
1710     for my $local_ar (@local_addrs) {
1711         my $local_ip   = $local_ar->[0];
1712         my $local_mask = $local_ar->[1];
1713
1714         my $local_obj = new NetAddr::IP($local_ip, $local_mask);
1715
1716         return 1 if $ip_obj->within($local_obj);
1717     }
1718     return 0;
1719 }
1720
1721 sub get_local_addrs() {
1722     open IFC, "$cmds{'ifconfig'} -a |" or die "[*] Could not run ",
1723         "$cmds{'ifconfig'}: $!";
1724     my @lines = <IFC>;
1725     close IFC;
1726
1727     my $intf_name = '';
1728     for my $line (@lines) {
1729         if ($line =~ /^(\w+)\s+Link/) {
1730             $intf_name = $1;
1731             next;
1732         }
1733         next if $intf_name eq 'lo';
1734         next if $intf_name =~ /dummy/i;
1735         if ($line =~ /^\s+inet.*?:($ip_re).*:($ip_re)/i) {
1736             push @local_addrs, [$1, $2];
1737         }
1738     }
1739     return;
1740 }
1741
1742 sub ipt_build_rule() {
1743     my ($chain, $fwsnort_chain, $rule, $hdr_hr, $opts_hr, $patterns_ar,
1744             $orig_snort_rule, $flow_logging_prefix, $add_snort_comment,
1745             $add_perl_trigger) = @_;
1746
1747     ### $chain is used only to see whether or not we need to add the
1748     ### rule to the iptables script based on whether the built-in chain
1749     ### will pass the traffic in the first place.
1750     if ($ipt_sync) {
1751         return 0 unless &ipt_allow_traffic($hdr_hr,
1752                 $opts_hr, $chain, $orig_snort_rule);
1753     }
1754
1755     ### append the protocol to the rule
1756     if (defined $opts_hr->{'ip_proto'}) {
1757         return 0 unless $opts_hr->{'ip_proto'} =~ /^\w+$/;
1758         $rule .= " $snort_opts{'filter'}{'ip_proto'}{'iptopt'} " .
1759             "$opts_hr->{'ip_proto'}";
1760     } else {
1761         return 0 unless $hdr_hr->{'proto'} =~ /^\w+$/;
1762         if ((($hdr_hr->{'sport'} !~ /any/i and $hdr_hr->{'sport'} ne '')
1763                 or ($hdr_hr->{'dport'} !~ /any/i
1764                 and $hdr_hr->{'dport'} ne ''))
1765                 and $hdr_hr->{'proto'} !~ /tcp/i
1766                 and $hdr_hr->{'proto'} !~ /udp/i) {
1767             ### force to tcp because iptables does not like src/dst
1768             ### ports with anything other than tcp or udp
1769             $hdr_hr->{'proto'} = 'tcp';
1770         }
1771
1772         if ($hdr_hr->{'proto'} =~ /ip/) {
1773             $rule .= " $ipt_hdr_opts{'proto'} $hdr_hr->{'proto'}";
1774         } else {
1775             $rule .= " $ipt_hdr_opts{'proto'} $hdr_hr->{'proto'} " .
1776                 "-m $hdr_hr->{'proto'}";
1777         }
1778     }
1779
1780     ### append the source and destination ports
1781     for my $type (qw(sport dport)) {
1782         if (defined $hdr_hr->{$type} and $hdr_hr->{$type} !~ /any/i) {
1783             my $negate = '';
1784             my $port = $hdr_hr->{$type};
1785             $negate = '! ' if $port =~ m|!|;
1786             $port =~ s/\!\s*(\d)/$1/;
1787             if ($port =~ /,/) {
1788                 ### multiport match
1789                 $rule .= " -m multiport ${negate}--${type}s $port";
1790             } else {
1791                 $rule .= " ${negate}$ipt_hdr_opts{$type} $port";
1792             }
1793         }
1794     }
1795
1796     my $rv = &ipt_build_opts($rule, $hdr_hr, $opts_hr, $patterns_ar,
1797         $orig_snort_rule, $flow_logging_prefix, $chain, $fwsnort_chain,
1798         $add_snort_comment, $add_perl_trigger);
1799
1800     return $rv;
1801 }
1802
1803 sub ipt_build_opts() {
1804     my ($rule, $hdr_hr, $opts_hr, $patterns_ar, $orig_snort_rule,
1805             $flow_logging_prefix, $chain, $fwsnort_chain, $add_snort_comment,
1806             $add_perl_trigger) = @_;
1807
1808     ### append tcp flags
1809     if (defined $opts_hr->{'flags'}) {
1810         my $f_str = '';
1811
1812         $f_str .= 'URG,' if $opts_hr->{'flags'} =~ /U/i;
1813         $f_str .= 'ACK,' if $opts_hr->{'flags'} =~ /A/i;
1814         $f_str .= 'PSH,' if $opts_hr->{'flags'} =~ /P/i;
1815         $f_str .= 'RST,' if $opts_hr->{'flags'} =~ /R/i;
1816         $f_str .= 'SYN,' if $opts_hr->{'flags'} =~ /S/i;
1817         $f_str .= 'FIN,' if $opts_hr->{'flags'} =~ /F/i;
1818         $f_str =~ s/\,$//;
1819
1820         if ($opts_hr->{'flags'} =~ /\+/) {
1821             ### --tcp-flags ACK ACK
1822             $rule .= " $snort_opts{'filter'}{'flags'}{'iptopt'} " .
1823                 "$f_str $f_str";
1824         } else {
1825             ### --tcp-flags ALL URG,PSH,SYN,FIN
1826             $rule .= " $snort_opts{'filter'}{'flags'}{'iptopt'} " .
1827                 "ALL $f_str";
1828         }
1829     }
1830
1831     if ($no_ipt_conntrack) {
1832         ### fall back to appending --tcp-flags ACK ACK if flow=established.
1833         ### NOTE: we can't really handle "flow" in the same way snort can,
1834         ### since there is no way to keep track of which side initiated the
1835         ### tcp session (where the SYN packet came from), but older versions
1836         ### of snort (pre 1.9) just used tcp flags "A+" to keep track of
1837         ### this... we need to do the same.
1838         if (defined $opts_hr->{'flow'} && ! defined $opts_hr->{'flags'}) {
1839             if ($opts_hr->{'flow'} =~ /established/i) {
1840                 ### note that this ignores the "stateless" keyword
1841                 ### as it should...
1842                 $rule .= " $snort_opts{'filter'}{'flow'}{'iptopt'} ACK ACK";
1843             }
1844         }
1845     }
1846
1847     ### append icmp type
1848     if (defined $opts_hr->{'itype'} and $hdr_hr->{'proto'} =~ /icmp/i) {
1849         $rule .= " $snort_opts{'filter'}{'itype'}{'iptopt'} " .
1850             "$opts_hr->{'itype'}";
1851         ### append icmp code (becomes "--icmp-type type/code")
1852         if (defined $opts_hr->{'icode'}) {
1853             $rule .= "/$opts_hr->{'icode'}";
1854         }
1855     }
1856
1857     ### append ip options
1858     if (defined $opts_hr->{'ipopts'}) {
1859         $rule .= " $snort_opts{'filter'}{'ipopts'}{'iptopt'} " .
1860             "--$opts_hr->{'ipopts'}"
1861     }
1862
1863     ### append tos (requires CONFIG_IP_NF_MATCH_TOS)
1864     if (defined $opts_hr->{'tos'}) {
1865         $rule .= " $snort_opts{'filter'}{'tos'}{'iptopt'} " .
1866             "$opts_hr->{'tos'}"
1867     }
1868
1869
1870     ### append ttl (requires CONFIG_IP_NF_MATCH_TTL)
1871     if (defined $opts_hr->{'ttl'}) {
1872         if ($opts_hr->{'ttl'} =~ /\<\s*(\d+)/) {
1873             $rule .= " $snort_opts{'filter'}{'ttl'}{'iptopt'} --ttl-lt $1";
1874         } elsif ($opts_hr->{'ttl'} =~ /\>\s*(\d+)/) {
1875             $rule .= " $snort_opts{'filter'}{'ttl'}{'iptopt'} --ttl-gt $1";
1876         } else {
1877             $rule .= " $snort_opts{'filter'}{'ttl'}{'iptopt'} " .
1878                 "--ttl-eq $opts_hr->{'ttl'}";
1879         }
1880     }
1881
1882     my $avg_hdr_len = &get_avg_hdr_len($hdr_hr);
1883
1884     ### append dsize option (requires CONFIG_IP_NF_MATCH_LENGTH)
1885     if (defined $opts_hr->{'dsize'}) {
1886         ### get the average packet header size based on the protocol
1887         ### (the iptables length match applies to the network header
1888         ### and up).
1889         if ($opts_hr->{'dsize'} =~ m|(\d+)\s*<>\s*(\d+)|) {
1890             my $iptables_len1 = $1 + $avg_hdr_len;
1891             my $iptables_len2 = $2 + $avg_hdr_len;
1892             $rule .= " $snort_opts{'filter'}{'dsize'}{'iptopt'} " .
1893                 "$iptables_len1:$iptables_len2";
1894         } elsif ($opts_hr->{'dsize'} =~ m|<\s*(\d+)|) {
1895             my $iptables_len = $1 + $avg_hdr_len;
1896             $rule .= " $snort_opts{'filter'}{'dsize'}{'iptopt'} " .
1897                 "$avg_hdr_len:$iptables_len";
1898         } elsif ($opts_hr->{'dsize'} =~ m|>\s*(\d+)|) {
1899             my $iptables_len = $1 + $avg_hdr_len;
1900             if ($iptables_len < $config{'MAX_FRAME_LEN'} + $avg_hdr_len) {
1901                 $rule .= " $snort_opts{'filter'}{'dsize'}{'iptopt'} " .
1902                     "$iptables_len:" .
1903                     ($config{'MAX_FRAME_LEN'} + $avg_hdr_len);
1904             }
1905         } elsif ($opts_hr->{'dsize'} =~ m|(\d+)|) {
1906             my $iptables_len = $1 + $avg_hdr_len;
1907             $rule .= " $snort_opts{'filter'}{'dsize'}{'iptopt'} " .
1908                 $iptables_len;
1909         }
1910     }
1911
1912     ### append snort content options
1913     my $ipt_content_criteria = 0;
1914     my $perl_trigger_str = '';
1915
1916     if ($#$patterns_ar > -1) {
1917         ($ipt_content_criteria, $perl_trigger_str)
1918             = &build_content_matches($opts_hr, $patterns_ar);
1919
1920         return 0 unless $ipt_content_criteria;
1921         $rule .= $ipt_content_criteria;
1922     }
1923
1924     ### print the rest of the logprefix snort options in a comment
1925     ### one line above the rule
1926     my $comment    = '';
1927     my $target_str = '';
1928     for my $key (qw(sid msg classtype reference priority rev)) {
1929         if (defined $opts_hr->{$key}) {
1930             $comment .= qq|$key:$opts_hr->{$key}; |;
1931         }
1932     }
1933     $comment =~ s/\s*$//;
1934     $comment =~ s/,$//;
1935
1936     ### append the fwsnort version as "FWS:$version"
1937     $comment .= " FWS:$version;";
1938
1939     ### build up the logging prefix and comment match
1940     if (defined $opts_hr->{'sid'}) {
1941         unless ($no_ipt_comments) {
1942             ### add the Snort msg (and other) fields to the iptables rule
1943             ### with the 'comment' match (which can handle up to 255 chars
1944             ### and is set/verified by the ipt_find_max_comment_len()
1945             ### function).
1946             $comment =~ s|\"||g;
1947             $comment =~ s|/\*||g;
1948             $comment =~ s|\*/||g;
1949             if (length($comment) < $ipt_max_comment_len) {
1950                 $target_str = qq| -m comment --comment "$comment"|;
1951             }
1952         }
1953         ### increment chain counter and add in if necessary
1954         $chain_ctr{$chain}++;
1955
1956         if ($queue_mode or $nfqueue_mode) {
1957             if ($queue_mode) {
1958                 $target_str .= qq| -j QUEUE|;
1959             } else {
1960                 $target_str .= qq| -j NFQUEUE|;
1961                 if ($nfqueue_num) {
1962                     $target_str .= " --queue-num $nfqueue_num";
1963                 }
1964             }
1965         } else {
1966             if ($hdr_hr->{'action'} eq 'log' or $ulog_mode) {
1967                 $target_str .= qq| -j ULOG --ulog-nlgroup $ulog_nlgroup --ulog-prefix "|;
1968             } else {
1969                 $target_str .= ' -j LOG ';
1970                 $target_str .= '--log-ip-options ' unless $no_ipt_log_ip_opts;
1971                 if ($hdr_hr->{'proto'} eq 'tcp') {
1972                     $target_str .= '--log-tcp-options ' unless $no_ipt_log_tcp_opts;
1973                     $target_str .= '--log-tcp-sequence ' if $ipt_log_tcp_seq;
1974                 }
1975                 $target_str .= qq|--log-prefix |;
1976             }
1977
1978
1979             ### build up the LOG prefix string
1980             my $prefix_str = '';
1981
1982             unless ($no_ipt_rule_nums) {
1983                 $prefix_str .= "[$chain_ctr{$chain}] ";
1984             }
1985
1986             if ($ipt_drop) {
1987                 $prefix_str .= 'DRP ';
1988             } elsif ($ipt_reject) {
1989                 $prefix_str .= 'REJ ';
1990             }
1991
1992             ### always add the sid
1993             $prefix_str .= qq|SID$opts_hr->{'sid'} |;
1994             if ($flow_logging_prefix) {
1995                 $prefix_str .= 'ESTAB ';
1996             }
1997
1998             if (length($prefix_str) >= $ipt_max_log_prefix_len) {
1999                 $prefix_str = qq|SID$opts_hr->{'sid'} |;
2000                 if (length($prefix_str) >= $ipt_max_log_prefix_len) {
2001                     return 0 unless $ipt_content_criteria;
2002                 }
2003             }
2004
2005             $target_str .= qq|"| . $prefix_str . qq|"|;
2006         }
2007     }
2008
2009     ### print the snort rules type header to the fwsnort.sh script
2010     unless ($ipt_print_type) {
2011         &ipt_type($snort_type);
2012         $ipt_print_type = 1;
2013     }
2014
2015     ### write the rule out to the iptables script
2016     &ipt_add_rule($hdr_hr, $opts_hr, $orig_snort_rule,
2017         $rule, $target_str, "### $comment", $add_snort_comment,
2018         $perl_trigger_str, $add_perl_trigger, $chain, $fwsnort_chain);
2019     return 1;
2020 }
2021
2022 sub build_content_matches() {
2023     my ($opts_hr, $patterns_ar) = @_;
2024
2025     my $fast_pattern_index   = 0;
2026     my $fast_pattern_is_set  = 0;
2027     my $ipt_content_criteria = '';
2028     my $perl_trigger_command = '';
2029     my @content_fast_pattern_order = ();
2030
2031     $perl_trigger_command = q|perl -e 'print "| if $include_perl_triggers;
2032
2033     if ($no_fast_pattern_order) {
2034         $patterns_ar->[0]->{'fast_pattern'} = 1;
2035     }
2036
2037     for (my $index=0; $index <= $#$patterns_ar; $index++) {
2038         if ($patterns_ar->[$index]->{'fast_pattern'}) {
2039             $fast_pattern_index  = $index;
2040             $fast_pattern_is_set = 1;
2041             last;
2042         }
2043     }
2044
2045     unless ($fast_pattern_is_set) {
2046         ### in this case, the 'fast_pattern' option was not used, so pick the
2047         ### longest pattern to match first (this should help with performance
2048         ### of signature matches on average)
2049         my $max_len = 0;
2050         my $max_len_index = 0;
2051         PATTERN: for (my $index=0; $index <= $#$patterns_ar; $index++) {
2052             my $pat_ar = $patterns_ar->[$index];
2053
2054             if ($pat_ar->{'length'} > $max_len) {
2055
2056                 ### make sure it is not a relative match
2057                 next PATTERN if defined $pat_ar->{'distance'};
2058                 next PATTERN if defined $pat_ar->{'within'};
2059
2060                 if ($index < $#$patterns_ar) {
2061                     my $next_pat_ar = $patterns_ar->[$index+1];
2062                     next PATTERN if defined $next_pat_ar->{'distance'};
2063                     next PATTERN if defined $next_pat_ar->{'within'};
2064                 }
2065
2066                 $max_len = $pat_ar->{'length'};
2067                 $max_len_index = $index;
2068             }
2069         }
2070         $fast_pattern_index = $max_len_index;
2071     }
2072
2073     $content_fast_pattern_order[0] = $patterns_ar->[$fast_pattern_index];
2074     for (my $i=0; $i <= $#$patterns_ar; $i++) {
2075         next if $i == $fast_pattern_index;
2076         push @content_fast_pattern_order, $patterns_ar->[$i];
2077     }
2078
2079     for (my $i=0; $i <= $#content_fast_pattern_order; $i++) {
2080
2081         if (($queue_mode or $nfqueue_mode) and $queue_pre_match_max > 0) {
2082             ### limit the number of content matches to perform within the
2083             ### kernel before sending the packet to a userspace Snort
2084             ### instance
2085             last if $i >= $queue_pre_match_max;
2086         }
2087
2088         my $pattern_hr = $content_fast_pattern_order[$i];
2089         my $content_str = $pattern_hr->{'ipt_pattern'};
2090
2091         if ($content_str =~ /\|.+\|/) {
2092             ### there is hex data in the content
2093             if ($pattern_hr->{'negative_match'}) {
2094                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2095                     {'content'}{'iptopt'} . ' ' .
2096                     qq{! --hex-string "$content_str"};
2097             } else {
2098                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2099                     {'content'}{'iptopt'} . ' ' .
2100                     qq{--hex-string "$content_str"};
2101             }
2102         } else {
2103             ### there is no hex data in the content
2104             if ($pattern_hr->{'negative_match'}) {
2105                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2106                     {'content'}{'iptopt'} . ' ' .
2107                     qq{! --string "$content_str"};
2108             } else {
2109                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2110                     {'content'}{'iptopt'} . ' ' .
2111                     qq{--string "$content_str"};
2112             }
2113         }
2114
2115         if (defined $opts_hr->{'replace'}) {
2116             my $replace_str = $opts_hr->{'replace'};
2117             $replace_str =~ s/`/\\`/g;
2118             if ($replace_str =~ /\|.+\|/) {  ### there is hex data in the content
2119                 $ipt_content_criteria
2120                         .= qq{ --replace-hex-string "$replace_str"};
2121             } else {
2122                 $ipt_content_criteria
2123                         .= qq{ --replace-string "$replace_str"};
2124             }
2125         }
2126
2127         if ($kernel_ver ne '2.4') {
2128             $ipt_content_criteria .= " --algo $string_match_alg";
2129
2130             ### see if we have any offset, depth, distance, or within
2131             ### criteria
2132
2133             if (defined $pattern_hr->{'offset'}) {
2134
2135                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2136                     {'offset'}{'iptopt'} . " $pattern_hr->{'offset'}";
2137                 $perl_trigger_command .= 'A'x$pattern_hr->{'offset'}
2138                     if $include_perl_triggers;
2139
2140             } elsif (defined $pattern_hr->{'distance'}) {  ### offset trumps distance
2141
2142                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2143                     {'distance'}{'iptopt'} . " $pattern_hr->{'distance'}";
2144
2145                 $perl_trigger_command .= 'A'x$pattern_hr->{'distance'}
2146                     if $include_perl_triggers;
2147             }
2148
2149             if (defined $pattern_hr->{'depth'}) {
2150
2151                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2152                     {'depth'}{'iptopt'} . " $pattern_hr->{'depth'}";
2153
2154                 $perl_trigger_command .= 'A'x$pattern_hr->{'depth'}
2155                     if $include_perl_triggers;
2156
2157             } elsif (defined $pattern_hr->{'within'}) {  ### depth trumps within
2158
2159                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2160                     {'within'}{'iptopt'} . " $pattern_hr->{'within'}";
2161
2162                 $perl_trigger_command .= 'A'x$pattern_hr->{'within'}
2163                     if $include_perl_triggers;
2164             }
2165
2166             ### see if we need to match the string match case insensitive
2167             if ($pattern_hr->{'nocase'}) {
2168                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2169                     {'nocase'}{'iptopt'};
2170             }
2171
2172             ### if the --payload option is available for
2173             ### the string match extension
2174             $ipt_content_criteria .= ' --payload'
2175                 if $ipt_has_string_payload_offset_opt;
2176         }
2177
2178         if ($include_perl_triggers) {
2179             ### now append the perl trigger command bytes
2180             if ($pattern_hr->{'negative_match'}) {
2181                 $perl_trigger_command .= 'A'x($pattern_hr->{'length'});
2182             } else {
2183                 $perl_trigger_command .= &translate_perl_trigger($content_str);
2184             }
2185         }
2186     }
2187
2188     if ($include_perl_triggers) {
2189         $perl_trigger_command .= qq|"'|;
2190     }
2191
2192     return $ipt_content_criteria, $perl_trigger_command;
2193 }
2194
2195 sub convert_pattern_for_iptables() {
2196     my $snort_str = shift;
2197
2198     my $rv = 0;
2199     my $ipt_str = $snort_str;
2200     my $log_str = '';
2201
2202     my %pattern = (
2203         'orig_snort_str' => $snort_str,
2204         'ipt_pattern'    => '',
2205         'negative_match' => 0,
2206         'fast_pattern'   => 0,
2207         'nocase'         => 0,
2208     );
2209
2210     if ($ipt_str =~ /\s*\!\s*"/) {
2211         $ipt_str =~ s/\s*\!\s*"//;
2212         $pattern{'negative_match'} = 1;
2213     }
2214
2215     $ipt_str =~ s/`/|60|/g;     ### ` -> |60|
2216     $ipt_str =~ s/'/|27|/g;     ### ' -> |27|
2217     $ipt_str =~ s/\x2d/|2d|/g;  ### - -> |2d|
2218     $ipt_str =~ s/\x24/|24|/g;  ### $ -> |24|
2219
2220     ### convert all escaped chars to their hex equivalents
2221     $ipt_str =~ s/\x5c(.)/sprintf "|%02x|", (ord($1))/eg;
2222
2223     my $ctr = 0;
2224     while ($ipt_str =~ m/\|\|/) {
2225         ### consolidate consecutive hex blocks
2226         if ($ipt_str =~ /\|.+?\|\|.+?\|/) {
2227             $ipt_str =~ s/\|(.+?)\|\|(.+?)\|/|$1 $2|/;
2228         }
2229         $ctr++;
2230         last if $ctr > 10;
2231     }
2232
2233     ### remove all spaces between hex codes (they simply waste space
2234     ### on the command line, and they aren't part of the string to
2235     ### search in network traffic anyway).
2236     $ipt_str = &consolidate_hex_spaces($ipt_str);
2237
2238     ### handles length of hex blocks
2239     my $content_len = &get_content_len($ipt_str);
2240     if ($content_len >= $ipt_max_str_len
2241             or $content_len >= $config{'MAX_STRING_LEN'}) {
2242         return 0, "pattern too long: $snort_str", \%pattern;
2243     }
2244
2245     $pattern{'ipt_pattern'} = $ipt_str;
2246     $pattern{'length'}      = $content_len;
2247
2248     return 1, $log_str, \%pattern;
2249 }
2250
2251 sub define_offsets() {
2252     my ($patterns_ar, $avg_hdr_len, $opt, $val) = @_;
2253
2254     my $current_index = $#$patterns_ar;
2255
2256     ### the option should not be defined - if so, there is a duplicate
2257     ### Snort keyword in the signature
2258     if (defined $patterns_ar->[$current_index]->{$opt}) {
2259         return 0, "duplicate keyword: $opt";
2260     }
2261
2262     ### store the value that was in the original Snort rule
2263     $patterns_ar->[$current_index]->{"${opt}_snort_orig"} = $val;
2264
2265     ### store the value and account for average header length if
2266     ### necessary
2267     if ($ipt_has_string_payload_offset_opt) {
2268         $patterns_ar->[$current_index]->{$opt} = $val;
2269     } else {
2270         $patterns_ar->[$current_index]->{$opt}
2271             = $val + $avg_hdr_len + $MAC_HDR_LEN;
2272     }
2273
2274     return 1, '';
2275 }
2276
2277 sub update_offsets_relative_matches() {
2278     my $patterns_ar = shift;
2279
2280     for (my $i=0; $i <= $#$patterns_ar; $i++) {
2281
2282         my $pat_hr = $patterns_ar->[$i];
2283         my $snort_offset   = -1;
2284         my $snort_depth    = -1;
2285         my $snort_distance = -1;
2286         my $snort_within   = -1;
2287
2288         $snort_offset = $pat_hr->{'offset_snort_orig'}
2289             if defined $pat_hr->{'offset_snort_orig'};
2290         $snort_depth = $pat_hr->{'depth_snort_orig'}
2291             if defined $pat_hr->{'depth_snort_orig'};
2292         $snort_distance = $pat_hr->{'distance_snort_orig'}
2293             if defined $pat_hr->{'distance_snort_orig'};
2294         $snort_within = $pat_hr->{'within_snort_orig'}
2295             if defined $pat_hr->{'within_snort_orig'};
2296
2297         if ($snort_depth > -1) {
2298             ### if there is also an offset, then the depth begins after
2299             ### the offset starts
2300             if ($snort_offset > -1) {
2301                 $pat_hr->{'depth'} += $snort_offset;
2302             } elsif ($snort_distance > -1) {
2303                 $pat_hr->{'depth'} += $snort_distance;
2304             }
2305             if ($snort_depth < $pat_hr->{'length'}) {
2306                 return 0, "depth: $snort_depth less than " .
2307                     "pattern length: $pat_hr->{'length'}";
2308             }
2309         }
2310
2311         if ($snort_distance > -1) {
2312             ### see if we need to increase the distance based
2313             ### on the length of the previous pattern match and offset
2314             if ($i > 0) {
2315                 my $prev_pat_hr = $patterns_ar->[$i-1];
2316                 $pat_hr->{'distance'} += $prev_pat_hr->{'length'};
2317                 if (defined $prev_pat_hr->{'offset_snort_orig'}) {
2318                     $pat_hr->{'distance'} += $prev_pat_hr->{'offset_snort_orig'};
2319                 }
2320             }
2321         }
2322
2323         if ($snort_within > -1) {
2324             if ($snort_offset > -1) {
2325                 $pat_hr->{'within'} += $snort_offset;
2326             } elsif ($snort_distance > -1) {
2327                 $pat_hr->{'within'} += $snort_distance;
2328             }
2329             if ($i > 0) {
2330                 my $prev_pat_hr = $patterns_ar->[$i-1];
2331                 $pat_hr->{'within'} += $prev_pat_hr->{'length'};
2332                 if (defined $prev_pat_hr->{'offset_snort_orig'}) {
2333                     $pat_hr->{'within'} += $prev_pat_hr->{'offset_snort_orig'};
2334                 }
2335             }
2336         }
2337     }
2338     return 1, '';
2339 }
2340
2341 sub get_avg_hdr_len() {
2342     my $hdr_hr = shift;
2343
2344     my $avg_hdr_len = $config{'AVG_IP_HEADER_LEN'};
2345     if (defined $hdr_hr->{'proto'}) {
2346         if ($hdr_hr->{'proto'} =~ /udp/i) {
2347             $avg_hdr_len += $UDP_HDR_LEN;  ### udp header is 8 bytes
2348         } elsif ($hdr_hr->{'proto'} =~ /icmp/i) {
2349             $avg_hdr_len += $ICMP_HDR_LEN;  ### icmp header is 8 bytes
2350         } else {
2351             ### default to TCP
2352             $avg_hdr_len += $config{'AVG_TCP_HEADER_LEN'};
2353         }
2354     } else {
2355         ### don't know what the average transport layer (if there
2356         ### is one) length will be; add 10 bytes just to be safe
2357         $avg_hdr_len += 10;
2358     }
2359     return $avg_hdr_len;
2360 }
2361
2362 ### handles length of hex blocks
2363 sub get_content_len() {
2364     my $str = shift;
2365     my $len = 0;
2366     my $hex_mode = 0;
2367     my @chars = split //, $str;
2368     for (my $i=0; $i<=$#chars; $i++) {
2369         if ($chars[$i] eq '|') {
2370             $hex_mode == 0 ? ($hex_mode = 1) : ($hex_mode = 0);
2371             next;
2372         }
2373         if ($hex_mode) {
2374             next if $chars[$i] eq ' ';
2375             $len++;
2376             $i++;
2377         } else {
2378             $len++;
2379         }
2380     }
2381     return $len;
2382 }
2383
2384 sub consolidate_hex_spaces() {
2385     my $str = shift;
2386     my $new_str = '';
2387     my $hex_mode = 0;
2388     my @chars = split //, $str;
2389     for my $char (@chars) {
2390         if ($char eq '|') {
2391             $hex_mode == 0 ? ($hex_mode = 1) : ($hex_mode = 0);
2392         }
2393         if ($hex_mode) {
2394             next if $char eq ' ';
2395         }
2396         $new_str .= $char;
2397     }
2398     return $new_str;
2399 }
2400
2401 sub translate_perl_trigger() {
2402     my $str = shift;
2403     my $trigger_str = '';
2404     my $hex_mode = 0;
2405     my $append_hex_str = '';
2406     my $append_non_hex_str = '';
2407
2408     my @chars = split //, $str;
2409
2410     for my $char (@chars) {
2411         if ($char eq '|') {
2412             if ($hex_mode) {
2413                 if ($append_hex_str) {
2414                     while ($append_hex_str =~ /(.{2})/g) {
2415                         $trigger_str .= "\\x$1";
2416                     }
2417                     $append_hex_str = '';
2418                 }
2419                 $hex_mode = 0;
2420             } else {
2421                 if ($append_non_hex_str) {
2422                     $trigger_str .= qq|$append_non_hex_str|;
2423                     $append_non_hex_str = '';
2424                 }
2425                 $hex_mode = 1;
2426             }
2427             next;
2428         }
2429
2430         if ($hex_mode) {
2431             $append_hex_str .= $char;
2432         } else {
2433             $append_non_hex_str .= $char;
2434         }
2435     }
2436
2437     if ($append_hex_str) {
2438         while ($append_hex_str =~ /(.{2})/g) {
2439             $trigger_str .= "\\x$1";
2440         }
2441     }
2442     $trigger_str .= qq|$append_non_hex_str| if $append_non_hex_str;
2443
2444     return $trigger_str;
2445 }
2446
2447 sub ipt_add_rule() {
2448     my ($hdr_hr, $opts_hr, $orig_snort_rule, $rule_base,
2449         $target_str, $comment, $add_snort_comment,
2450         $perl_trigger_str, $add_perl_trigger, $chain, $fwsnort_chain) = @_;
2451
2452     my $action_rule = '';
2453     if ($hdr_hr->{'proto'} eq 'tcp') {
2454         if ($hdr_hr->{'action'} eq 'pass') {
2455             $action_rule = "$rule_base -j ACCEPT";
2456         } else {
2457             if (defined $opts_hr->{'resp'}
2458                     and $opts_hr->{'resp'} =~ /rst/i) {
2459                 ### iptables can only send tcp resets to the connection
2460                 ### client, so we can't support rst_rcv, but we should
2461                 ### try to tear the connection down anyway.
2462                 $action_rule = "$rule_base -j REJECT " .
2463                     "--reject-with tcp-reset";
2464             } elsif ($ipt_drop) {
2465                 $action_rule = "$rule_base -j DROP";
2466             } elsif ($ipt_reject) {
2467                 $action_rule = "$rule_base -j REJECT " .
2468                     "--reject-with tcp-reset";
2469             }
2470         }
2471     } elsif ($hdr_hr->{'proto'} eq 'udp') {
2472         if ($hdr_hr->{'action'} eq 'pass') {
2473             $action_rule = "$rule_base -j ACCEPT";
2474         } else {
2475             if (defined $opts_hr->{'resp'}
2476                     and $opts_hr->{'resp'} =~ /icmp/i) {
2477                 if ($opts_hr->{'resp'} =~ /all/i) {  ### icmp_all
2478                     $action_rule = "$rule_base -j REJECT " .
2479                         "--reject-with icmp-port-unreachable";
2480                 } elsif ($opts_hr->{'resp'} =~ /net/i) {  ### icmp_net
2481                     $action_rule = "$rule_base -j REJECT " .
2482                         "--reject-with icmp-net-unreachable";
2483                 } elsif ($opts_hr->{'resp'} =~ /host/i) {  ### icmp_host
2484                     $action_rule = "$rule_base -j REJECT " .
2485                         "--reject-with icmp-host-unreachable";
2486                 } elsif ($opts_hr->{'resp'} =~ /port/i) {  ### icmp_port
2487                     $action_rule = "$rule_base -j REJECT " .
2488                         "--reject-with icmp-port-unreachable";
2489                 }
2490             } elsif ($ipt_drop) {
2491                 $action_rule = "$rule_base -j DROP";
2492             } elsif ($ipt_reject) {
2493                 $action_rule = "$rule_base -j REJECT " .
2494                     "--reject-with icmp-port-unreachable";
2495             }
2496         }
2497     } else {
2498         if ($hdr_hr->{'action'} eq 'pass') {
2499             $action_rule = "$rule_base -j ACCEPT";
2500         } else {
2501             $action_rule = "$rule_base -j DROP";
2502         }
2503     }
2504     my $ipt_rule = $rule_base . $target_str;
2505
2506     push @ipt_script_lines, "\n### $orig_snort_rule" if $add_snort_comment;
2507     if ($include_perl_triggers and $add_perl_trigger) {
2508         push @ipt_script_lines, "### $perl_trigger_str";
2509     }
2510     if ($verbose) {
2511         push @ipt_script_lines, qq|\$ECHO "[+] rule $ipt_rule_ctr"|;
2512     }
2513
2514     ### save format handling
2515     my $save_format_ipt_rule    = $ipt_rule . " \n";
2516     my $save_format_action_rule = $action_rule . " \n";
2517
2518     $save_format_ipt_rule    =~ s|\$${ipt_var_str}\s+\-A|-A|;
2519     $save_format_action_rule =~ s|\$${ipt_var_str}\s+\-A|-A|;
2520
2521     if ($hdr_hr->{'action'} ne 'pass') {
2522         if ($queue_mode or $nfqueue_mode) {
2523
2524             push @ipt_script_lines, $ipt_rule;
2525             push @{$save_format_rules{$fwsnort_chain}}, $save_format_ipt_rule;
2526
2527         } else {
2528
2529             push @ipt_script_lines, $ipt_rule unless $no_ipt_log;
2530             push @{$save_format_rules{$fwsnort_chain}}, $save_format_ipt_rule
2531                 unless $no_ipt_log;
2532         }
2533     }
2534
2535     if ($action_rule and ($ipt_drop or $ipt_reject or
2536             $hdr_hr->{'action'} eq 'pass' or defined $opts_hr->{'resp'})) {
2537
2538         push @ipt_script_lines, $action_rule;
2539         push @{$save_format_rules{$fwsnort_chain}}, $save_format_action_rule;
2540     }
2541
2542     $ipt_rule_ctr++;
2543     return;
2544 }
2545
2546 sub save_format_append_rules() {
2547
2548      for my $chain (sort keys %ipt_save_existing_chains) {
2549
2550         next unless &is_fwsnort_chain($chain, $MATCH_EQUIV);
2551
2552         ### make sure that whitelist/blacklist and established jump rules
2553         ### are added at the beginning of each chain in save format
2554         &save_format_add_prereqs($chain);
2555
2556         for my $rule (@{$save_format_rules{$chain}}) {
2557
2558             push @fwsnort_save_lines, $rule;
2559         }
2560     }
2561
2562     ### now append any last lines from the iptables-save output that
2563     ### had nothing to do with fwsnort (other custom chains, etc.)
2564     for (my $i = $ipt_save_index; $i <= $#ipt_save_lines; $i++) {
2565         next if &is_fwsnort_chain($ipt_save_lines[$i], $MATCH_SUBSTR);
2566         push @fwsnort_save_lines, $ipt_save_lines[$i];
2567     }
2568
2569     return;
2570 }
2571
2572 sub save_format_add_prereqs() {
2573     my $chain = shift;
2574
2575     return if defined $save_format_prereqs{$chain};
2576
2577     ### add whitelist
2578     if (defined $save_format_whitelist{$chain}) {
2579         for my $whitelist_rule (@{$save_format_whitelist{$chain}}) {
2580             push @fwsnort_save_lines, "$whitelist_rule \n";
2581         }
2582     }
2583
2584     ### add blacklist
2585     if (defined $save_format_blacklist{$chain}) {
2586         for my $blacklist_rule (@{$save_format_blacklist{$chain}}) {
2587             push @fwsnort_save_lines, "$blacklist_rule \n";
2588         }
2589     }
2590
2591     ### add jump rules into the connection tracking fwsnort chains
2592     if (defined $save_format_conntrack_jumps{$chain}) {
2593         for my $jump_rule (@{$save_format_conntrack_jumps{$chain}}) {
2594             push @fwsnort_save_lines, "$jump_rule \n";
2595         }
2596     }
2597
2598     $save_format_prereqs{$chain} = '';
2599
2600     return;
2601 }
2602
2603 sub ipt_whitelist() {
2604     my @whitelist_addrs = ();
2605
2606     for my $whitelist_line (@{$config{'WHITELIST'}}) {
2607         for my $addr (@{&expand_addresses($whitelist_line)}) {
2608             push @whitelist_addrs, $addr;
2609         }
2610     }
2611
2612     return unless $#whitelist_addrs >= 0;
2613
2614     push @ipt_script_lines, "\n###\n############ Add IP/network " .
2615         "WHITELIST rules. ############\n###";
2616
2617     for my $addr (@whitelist_addrs) {
2618         for my $chain (keys %process_chains) {
2619             next unless $process_chains{$chain};
2620
2621             if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
2622                 my $rule_str = qq|-A $config{"FWSNORT_$chain"} | .
2623                     "-s $addr -j RETURN";
2624                 push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2625
2626                 push @{$save_format_whitelist{$config{"FWSNORT_$chain"}}},
2627                     $rule_str;
2628             }
2629             if ($chain eq 'OUTPUT' or $chain eq 'FORWARD') {
2630                 my $rule_str = qq|-A $config{"FWSNORT_$chain"} | .
2631                     "-d $addr -j RETURN";
2632                 push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2633
2634                 push @{$save_format_whitelist{$config{"FWSNORT_$chain"}}},
2635                     $rule_str;
2636             }
2637         }
2638     }
2639     return;
2640 }
2641
2642 sub ipt_blacklist() {
2643
2644     my $printed_intro = 0;
2645
2646     for my $blacklist_line (@{$config{'BLACKLIST'}}) {
2647
2648         my @blacklist_addrs = ();
2649         my $target = 'DROP';  ### default
2650
2651         if ($blacklist_line =~ /\s+REJECT/) {
2652             $target = 'REJECT';
2653         }
2654
2655         for my $addr (@{&expand_addresses($blacklist_line)}) {
2656             push @blacklist_addrs, $addr;
2657         }
2658
2659         return unless $#blacklist_addrs >= 0;
2660
2661         unless ($printed_intro) {
2662             push @ipt_script_lines, "\n###\n############ Add IP/network " .
2663                 "BLACKLIST rules. ############\n###";
2664             $printed_intro = 1;
2665         }
2666
2667         for my $addr (@blacklist_addrs) {
2668             for my $chain (keys %process_chains) {
2669                 next unless $process_chains{$chain};
2670
2671                 if ($target eq 'DROP') {
2672                     if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
2673
2674                         my $rule_str = qq|-A $config{"FWSNORT_$chain"} | .
2675                             "-s $addr -j DROP";
2676
2677                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2678                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2679                             $rule_str;
2680                     }
2681
2682                     if ($chain eq 'OUTPUT' or $chain eq 'FORWARD') {
2683
2684                         my $rule_str = qq|-A $config{"FWSNORT_$chain"} | .
2685                             "-d $addr -j DROP";
2686                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2687                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2688                             $rule_str;
2689                     }
2690                 } else {
2691                     if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
2692                         my $rule_str = qq|-A $config{"FWSNORT_$chain"} -s $addr | .
2693                             "-p tcp -j REJECT --reject-with tcp-reset";
2694
2695                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2696                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2697                             $rule_str;
2698
2699                         $rule_str = qq|-A $config{"FWSNORT_$chain"} -s $addr | .
2700                             "-p udp -j REJECT --reject-with icmp-port-unreachable";
2701
2702                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2703                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2704                             $rule_str;
2705
2706                         $rule_str = qq|-A $config{"FWSNORT_$chain"} -s $addr | .
2707                             "-p icmp -j REJECT --reject-with icmp-host-unreachable";
2708
2709                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2710                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2711                             $rule_str;
2712                     }
2713                     if ($chain eq 'OUTPUT' or $chain eq 'FORWARD') {
2714                         my $rule_str = qq|-A $config{"FWSNORT_$chain"} -d $addr | .
2715                             "-p tcp -j REJECT --reject-with tcp-reset";
2716
2717                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2718                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2719                             $rule_str;
2720
2721                         $rule_str = qq|-A $config{"FWSNORT_$chain"} -d $addr | .
2722                             "-p udp -j REJECT --reject-with icmp-port-unreachable";
2723
2724                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2725                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2726                             $rule_str;
2727
2728                         $rule_str = qq|-A $config{"FWSNORT_$chain"} -d $addr | .
2729                             "-p icmp -j REJECT --reject-with icmp-host-unreachable";
2730
2731                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2732                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2733                             $rule_str;
2734                     }
2735                 }
2736             }
2737         }
2738     }
2739     return;
2740 }
2741
2742 sub ipt_add_chains() {
2743
2744     ### save format
2745     my %ipt_save_built_in_chains = ();
2746     my $look_for_chains = 0;
2747     for (@ipt_save_lines) {
2748         unless ($look_for_chains) {
2749             push @fwsnort_save_lines, $_;
2750         }
2751         if (/^\*filter/) {
2752             $look_for_chains = 1;
2753         } elsif ($look_for_chains and $_ =~ /^:(\S+)/) {
2754             my $chain = $1;
2755             if ($chain eq 'INPUT'
2756                     or $chain eq 'OUTPUT'
2757                     or $chain eq 'FORWARD') {
2758                 $ipt_save_built_in_chains{$chain} = $_;
2759             } else {
2760                 ### don't preserve any old fwsnort chains, but preserve
2761                 ### any other existing custom chains
2762                 unless (&is_fwsnort_chain($chain, $MATCH_EQUIV)) {
2763                     $ipt_save_existing_chains{$chain} = $_;
2764                 }
2765             }
2766         } elsif ($look_for_chains and $_ !~ /^:(\S+)/) {
2767             last;
2768         }
2769         $ipt_save_index++;
2770     }
2771
2772     ### save format - add the built-in chains first
2773     for my $chain (qw(INPUT FORWARD OUTPUT)) {
2774         ### should always be defined unless we're not running as root
2775         next unless defined $ipt_save_built_in_chains{$chain};
2776         push @fwsnort_save_lines, $ipt_save_built_in_chains{$chain};
2777     }
2778
2779     ### add the fwsnort chains
2780     push @ipt_script_lines, "\n###\n############ Create " .
2781         "fwsnort $ipt_str chains. ############\n###";
2782
2783     for my $built_in_chain (qw(INPUT FORWARD OUTPUT)) {
2784
2785         ### see if any of the "FWSNORT_<built-in-chain>" chains need to be
2786         ### excluded
2787         next unless $process_chains{$built_in_chain};
2788
2789         for my $chain ($config{"FWSNORT_$built_in_chain"},
2790                 $config{"FWSNORT_${built_in_chain}_ESTAB"}) {
2791             if ($no_ipt_conntrack) {
2792                 next if $chain eq
2793                     $config{"FWSNORT_${built_in_chain}_ESTAB"};
2794             }
2795             push @ipt_script_lines,
2796                 "\$${ipt_var_str} -N $chain 2> /dev/null",
2797                 "\$${ipt_var_str} -F $chain\n";
2798
2799             ### save format
2800             $ipt_save_existing_chains{$chain} = ":$chain - [0:0]\n";
2801         }
2802     }
2803
2804     ### save format - add the custom chains
2805     for my $chain (sort keys %ipt_save_existing_chains) {
2806         push @fwsnort_save_lines, $ipt_save_existing_chains{$chain};
2807     }
2808
2809     ### save format - add in the jump rules from the
2810     ### built-in chains here
2811     &save_format_add_jump_rules();
2812
2813     ### add in any rules from custom chains that alphabetically come
2814     ### before the first fwsnort chain
2815     &save_format_add_early_custom_chains();
2816
2817     return;
2818 }
2819
2820 sub save_format_add_jump_rules() {
2821
2822     ### add the jump rule for each built-in chain
2823     for my $built_in_chain (qw(INPUT FORWARD OUTPUT)) {
2824
2825         next unless defined $process_chains{$built_in_chain}
2826             and $process_chains{$built_in_chain};
2827
2828         ### get the current $chain rules (if any), and then see where to add
2829         ### the fwsnort jump rule
2830
2831         my @existing_chain_rules = ();
2832
2833         for (my $i = $ipt_save_index; $i < $#ipt_save_lines; $i++) {
2834
2835             ### delete any existing fwsnort jump rules
2836             if ($ipt_save_lines[$i] =~ /^\-A\s$built_in_chain\s/) {
2837                 if ($ipt_save_lines[$i] !~ /\-j\sFWSNORT_/) {
2838                     push @existing_chain_rules, $ipt_save_lines[$i];
2839                 }
2840             } else {
2841                 last;
2842             }
2843             $ipt_save_index++;
2844         }
2845
2846         my $ctr = 1;
2847         my $added_jump_rule = 0;
2848         for my $existing_rule (@existing_chain_rules) {
2849             if ($ctr == $config{"FWSNORT_${built_in_chain}_JUMP"}) {
2850                 &save_format_add_chain_jump_rule($built_in_chain);
2851                 $added_jump_rule = 1;
2852             }
2853
2854             push @fwsnort_save_lines, $existing_rule;
2855             $ctr++;
2856         }
2857
2858         ### the chain may have been empty
2859         unless ($added_jump_rule) {
2860             &save_format_add_chain_jump_rule($built_in_chain);
2861         }
2862     }
2863
2864     return;
2865 }
2866
2867 sub save_format_add_chain_jump_rule() {
2868     my $built_in_chain = shift;
2869     my $fwsnort_chain = "FWSNORT_${built_in_chain}";
2870     if (%restrict_interfaces) {
2871         for my $intf (keys %restrict_interfaces) {
2872             if ($built_in_chain eq 'INPUT' or $built_in_chain eq 'FORWARD') {
2873                 push @fwsnort_save_lines, "-A $built_in_chain -i $intf " .
2874                     "-j $fwsnort_chain \n";
2875             } elsif ($built_in_chain eq 'OUTPUT') {
2876                 push @fwsnort_save_lines, "-A $built_in_chain -o $intf " .
2877                     "-j $fwsnort_chain \n";
2878             }
2879         }
2880     } else {
2881         if ($no_exclude_loopback) {
2882             push @fwsnort_save_lines, "-A $built_in_chain " .
2883                 "-j $fwsnort_chain \n";
2884         } else {
2885             if ($built_in_chain eq 'INPUT' or $built_in_chain eq 'FORWARD') {
2886                 push @fwsnort_save_lines, "-A $built_in_chain ! -i lo " .
2887                     "-j $fwsnort_chain \n";
2888             } elsif ($built_in_chain eq 'OUTPUT') {
2889                 push @fwsnort_save_lines, "-A $built_in_chain ! -o lo " .
2890                     "-j $fwsnort_chain \n";
2891             }
2892         }
2893     }
2894
2895     return;
2896 }
2897
2898 sub save_format_add_early_custom_chains() {
2899
2900     for my $chain (sort keys %ipt_save_existing_chains) {
2901         last if &is_fwsnort_chain($chain, $MATCH_EQUIV);
2902
2903         for (my $i = $ipt_save_index; $i <= $#ipt_save_lines; $i++) {
2904             last if &is_fwsnort_chain($ipt_save_lines[$i], $MATCH_SUBSTR);
2905             push @fwsnort_save_lines, $ipt_save_lines[$i];
2906             $ipt_save_index++;
2907         }
2908     }
2909
2910     return;
2911 }
2912
2913 sub is_fwsnort_chain() {
2914     my ($str, $match_style) = @_;
2915     my $rv = 0;
2916
2917     for my $fwsnort_chain ($config{'FWSNORT_INPUT'},
2918             $config{'FWSNORT_INPUT_ESTAB'},
2919             $config{'FWSNORT_FORWARD'},
2920             $config{'FWSNORT_FORWARD_ESTAB'},
2921             $config{'FWSNORT_OUTPUT'},
2922             $config{'FWSNORT_OUTPUT_ESTAB'}) {
2923
2924         if ($match_style eq $MATCH_SUBSTR) {
2925             if ($str =~ /$fwsnort_chain/) {
2926                 $rv = 1;
2927                 last;
2928             }
2929         } elsif ($match_style eq $MATCH_EQUIV) {
2930             if ($str eq $fwsnort_chain) {
2931                 $rv = 1;
2932                 last;
2933             }
2934         }
2935     }
2936
2937     return $rv;
2938 }
2939
2940 sub ipt_add_conntrack_jumps() {
2941     ### jump ESTABLISHED tcp traffic to each of the _ESTAB
2942     ### chains
2943     push @ipt_script_lines, "\n###\n############ Inspect $conntrack_state " .
2944         "tcp connections. ############\n###";
2945
2946     for my $chain (keys %process_chains) {
2947         next unless $process_chains{$chain};
2948
2949         my $rule_str = '';
2950
2951         if ($have_conntrack) {
2952             $rule_str = qq|-A $config{"FWSNORT_$chain"} -p tcp -m conntrack | .
2953                 qq|--ctstate $conntrack_state -j | .
2954                 qq|$config{"FWSNORT_${chain}_ESTAB"}|;
2955         } elsif ($have_state) {
2956             $rule_str = qq|-A $config{"FWSNORT_$chain"} -p tcp -m state | .
2957                 qq|--state $conntrack_state -j | .
2958                 qq|$config{"FWSNORT_${chain}_ESTAB"}|;
2959         }
2960
2961         push @ipt_script_lines, qq|\$${ipt_var_str} $rule_str|;
2962         push @{$save_format_conntrack_jumps{$config{"FWSNORT_$chain"}}},
2963             $rule_str;
2964     }
2965     return;
2966 }
2967
2968 sub ipt_jump_chain() {
2969     push @ipt_script_lines, "\n###\n############ Jump traffic " .
2970         "to the fwsnort chains. ############\n###";
2971     if (%restrict_interfaces) {
2972         for my $intf (keys %restrict_interfaces) {
2973             for my $chain (keys %process_chains) {
2974                 next unless $process_chains{$chain};
2975
2976                 if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
2977                     ### delete any existing jump rule so that fwsnort.sh can
2978                     ### be executed many times in a row without adding several
2979                     ### jump rules
2980                     push @ipt_script_lines, "\$${ipt_var_str} -D $chain " .
2981                         qq|-i $intf -j $config{"FWSNORT_$chain"}| .
2982                         ' 2> /dev/null';
2983
2984                     ### now add the jump rule
2985                     push @ipt_script_lines, "\$${ipt_var_str} -I $chain " .
2986                         qq|$config{"FWSNORT_${chain}_JUMP"} -i | .
2987                         qq|$intf -j $config{"FWSNORT_$chain"}|;
2988                 } elsif ($chain eq 'OUTPUT') {
2989
2990                     push @ipt_script_lines, "\$${ipt_var_str} -D $chain " .
2991                         qq|-o $intf -j $config{'FWSNORT_OUTPUT'}| .
2992                         ' 2> /dev/null';
2993
2994                     push @ipt_script_lines, "\$${ipt_var_str} -I $chain " .
2995                         qq|$config{'FWSNORT_OUTPUT_JUMP'} -o | .
2996                         qq|$intf -j $config{'FWSNORT_OUTPUT'}|;
2997                 }
2998             }
2999         }
3000     } else {
3001         for my $chain (keys %process_chains) {
3002             next unless $process_chains{$chain};
3003
3004             if ($no_exclude_loopback) {
3005
3006                 push @ipt_script_lines, "\$${ipt_var_str} -D $chain " .
3007                     qq|-j $config{"FWSNORT_$chain"}| .
3008                     ' 2> /dev/null';
3009
3010                 push @ipt_script_lines, "\$${ipt_var_str} -I $chain " .
3011                     qq|$config{"FWSNORT_${chain}_JUMP"} | .
3012                     qq|-j $config{"FWSNORT_$chain"}|;
3013             } else {
3014                 if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
3015
3016                     push @ipt_script_lines, "\$${ipt_var_str} -D $chain " .
3017                         qq|! -i lo -j $config{"FWSNORT_$chain"}| .
3018                         ' 2> /dev/null';
3019
3020                     push @ipt_script_lines, "\$${ipt_var_str} -I $chain " .
3021                         qq|$config{"FWSNORT_${chain}_JUMP"} ! -i lo | .
3022                         qq|-j $config{"FWSNORT_$chain"}|;
3023
3024                 } elsif ($chain eq 'OUTPUT') {
3025
3026                     push @ipt_script_lines, "\$${ipt_var_str} -D $chain " .
3027                         qq|! -o lo -j $config{"FWSNORT_$chain"}| .
3028                         ' 2> /dev/null';
3029
3030                     push @ipt_script_lines, "\$${ipt_var_str} -I $chain " .
3031                         qq|$config{"FWSNORT_${chain}_JUMP"} ! -o lo | .
3032                         qq|-j $config{"FWSNORT_$chain"}|;
3033                 }
3034             }
3035         }
3036     }
3037     return;
3038 }
3039
3040 sub hdr_lines() {
3041     return
3042         "#!$cmds{'sh'}\n#", '#'x76,
3043         "#\n# File:  $config{'FWSNORT_SCRIPT'}",
3044         "#\n# Purpose:  This script was auto-" .
3045         "generated by fwsnort, and implements",
3046         "#           an $ipt_str ruleset based upon " .
3047         "Snort rules.  For more",
3048         "#           information see the fwsnort man " .
3049         "page or the documentation",
3050         "#           available at " .
3051         "http://www.cipherdyne.org/fwsnort/",
3052         "#\n# Generated with:     fwsnort @argv_cp",
3053         "# Generated on host:  " . hostname(),
3054         "# Time stamp:         " . localtime(),
3055         "#\n# Author:  Michael Rash <mbr\@cipherdyne.org>",
3056         "#\n# Version: $version",
3057         "#", '#'x76, "#\n";
3058 }
3059
3060 sub ipt_hdr() {
3061     push @ipt_script_lines, &hdr_lines();
3062     push @ipt_save_script_lines, $ipt_script_lines[$#ipt_script_lines];
3063
3064     ### add paths to system binaries (iptables included)
3065     &ipt_config_section();
3066     return;
3067 }
3068
3069 sub ipt_config_section() {
3070     ### build the config section of the iptables script
3071     push @ipt_script_lines,
3072         '#==================== config ====================',
3073         "ECHO=$cmds{'echo'}",
3074         "${ipt_var_str}=$ipt_bin",
3075         "#================== end config ==================\n";
3076     push @ipt_save_script_lines, $ipt_script_lines[$#ipt_script_lines];
3077     return;
3078 }
3079
3080 sub ipt_type() {
3081     my $type = shift;
3082     push @ipt_script_lines, "\n###\n############ ${type}.rules #######" .
3083         "#####\n###", "\$ECHO \"[+] Adding $type rules:\"";
3084     return;
3085 }
3086
3087 sub check_type() {
3088     for my $type_hr (\%include_types, \%exclude_types) {
3089         for my $type (keys %$type_hr) {
3090             my $found = 0;
3091             my @valid_types = ();
3092             for my $dir (split /\,/, $config{'RULES_DIR'}) {
3093                 if (-e "$dir/${type}.rules") {
3094                     $found = 1;
3095                 } else {
3096                     opendir D, $dir or die "[*] Could not open $dir: $!";
3097                     for my $file (readdir D) {
3098                         if ($file =~ /(\S+)\.rules/) {
3099                             push @valid_types, $1;
3100                         }
3101                     }
3102                 }
3103             }
3104             unless ($found) {
3105                 print "[-] \"$type\" is not a valid type.\n",
3106                 "    Choose from the following available signature types:\n";
3107                 for my $type (sort @valid_types) {
3108                     print "        $type\n";
3109                 }
3110                 die "[-] Exiting.";
3111             }
3112         }
3113     }
3114     return;
3115 }
3116
3117 sub import_config() {
3118     open C, "< $fwsnort_conf" or die "[*] Could not open $fwsnort_conf: $!";
3119     my @lines = <C>;
3120     close C;
3121     my $l_ctr = 0;
3122     for my $line (@lines) {
3123         $l_ctr++;
3124         chomp $line;
3125         next if $line =~ /^\s*#/;
3126         next unless $line =~ /\S/;
3127         if ($line =~ /^\s*(\S+)Cmd\s+(\S+);/) {  ### e.g. "iptablesCmd"
3128             $cmds{$1} = $2;
3129         } elsif ($line =~ /^\s*(\S+)\s+(.*?);/) {
3130             my $var = $1;
3131             my $val = $2;
3132             die "[*] $fwsnort_conf: Variable \"$var\" is set to\n",
3133                 "    _CHANGEME_ at line $l_ctr.  Edit $fwsnort_conf.\n"
3134                 if $val eq '_CHANGEME_';
3135             if (defined $multi_line_vars{$var}) {
3136                 push @{$config{$var}}, $val;
3137             } else {
3138                 ### may have already been defined in existing snort.conf
3139                 ### file if --snort-conf was given.
3140                 $config{$var} = $val unless defined $config{$var};
3141             }
3142         }
3143     }
3144
3145     &expand_vars();
3146
3147     return;
3148 }
3149
3150 sub ipt_list() {
3151     for my $chain (
3152         $config{'FWSNORT_INPUT'},
3153         $config{'FWSNORT_INPUT_ESTAB'},
3154         $config{'FWSNORT_OUTPUT'},
3155         $config{'FWSNORT_OUTPUT_ESTAB'},
3156         $config{'FWSNORT_FORWARD'},
3157         $config{'FWSNORT_FORWARD_ESTAB'}
3158     ) {
3159         my $cmd = "$ipt_bin -v -n -L $chain";
3160         my $exists = (system "$cmd > /dev/null 2>&1") >> 8;
3161         if ($exists == 0) {
3162             print "[+] Listing $chain chain...\n";
3163             system $cmd;
3164             print "\n";
3165         } else {
3166             print "[-] Chain $chain does not exist...\n";
3167         }
3168     }
3169     exit 0;
3170 }
3171
3172 sub ipt_flush() {
3173     for my $chain (
3174         $config{'FWSNORT_INPUT'},
3175         $config{'FWSNORT_INPUT_ESTAB'},
3176         $config{'FWSNORT_OUTPUT'},
3177         $config{'FWSNORT_OUTPUT_ESTAB'},
3178         $config{'FWSNORT_FORWARD'},
3179         $config{'FWSNORT_FORWARD_ESTAB'}
3180     ) {
3181         my $exists = (system "$ipt_bin -n -L " .
3182             "$chain > /dev/null 2>&1") >> 8;
3183         if ($exists == 0) {
3184             print "[+] Flushing $chain chain...\n";
3185             system "$ipt_bin -F $chain";
3186             if ($ipt_del_chains) {
3187                 ### must remove any jump rules from the built-in
3188                 ### chains
3189                 &del_jump_rule($chain);
3190
3191                 print "    Deleting $chain chain...\n";
3192                 system "$ipt_bin -X $chain";
3193             }
3194         } else {
3195             print "[-] Chain $chain does not exist...\n";
3196         }
3197     }
3198     exit 0;
3199 }
3200
3201 sub cache_ipt_save_policy() {
3202
3203     return unless $is_root;
3204
3205     open IPT, "$save_bin -t filter |" or die "[*] Could not execute $save_bin";
3206     while (<IPT>) {
3207         push @ipt_save_lines, $_;
3208     }
3209     close IPT;
3210
3211     ### also write out the current iptables policy so that we can
3212     ### revert to it if necessary (iptables does a good job of not committing
3213     ### a policy via iptables-save if there is a problem with a rule though).
3214     &archive($config{'IPT_BACKUP_SAVE_FILE'});
3215     open F, "> $config{'IPT_BACKUP_SAVE_FILE'}" or die "[*] Could not " .
3216         "open $config{'IPT_BACKUP_SAVE_FILE'}: $!";
3217     print F for @ipt_save_lines;
3218     close F;
3219
3220     ### remove the last two lines (the 'COMMIT' and '# Completed ...' lines
3221     ### so they can be added later).
3222     $ipt_save_completed_line = $ipt_save_lines[$#ipt_save_lines];
3223     pop @ipt_save_lines;
3224     pop @ipt_save_lines;
3225
3226     return;
3227 }
3228
3229 sub del_jump_rule() {
3230     my $chain = shift;
3231
3232     my $ipt = new IPTables::Parse 'iptables' => $cmds{'iptables'}
3233         or die "[*] Could not acquire IPTables::Parse object: $!";
3234
3235     for my $built_in_chain (qw(INPUT OUTPUT FORWARD)) {
3236         my $rules_ar = $ipt->chain_rules('filter', $built_in_chain, $ipt_file);
3237
3238         for (my $i=0; $i <= $#$rules_ar; $i++) {
3239             my $rule_num = $i+1;
3240             if ($rules_ar->[$i]->{'target'} eq $chain) {
3241                 system "$ipt_bin -D $built_in_chain $rule_num";
3242                 last;
3243             }
3244         }
3245     }
3246
3247     return;
3248 }
3249
3250 sub fwsnort_init() {
3251
3252     ### set umask to -rw-------
3253     umask 0077;
3254
3255     ### turn off buffering
3256     $| = 1;
3257
3258     &set_non_root_values() unless $is_root;
3259
3260     ### read in configuration info from the config file
3261     &import_config();
3262
3263     ### make sure the commands are where the
3264     ### config file says they are
3265     &chk_commands();
3266
3267     ### make sure all of the required variables are defined
3268     ### in the config file
3269     &required_vars();
3270
3271     $non_host    = $NON_HOST;
3272     $ipt_bin     = $cmds{'iptables'};
3273     $restore_bin = $cmds{'iptables-restore'};
3274     $save_bin    = $cmds{'iptables-save'};
3275
3276     if ($enable_ip6tables) {
3277         for my $opt (qw(itype icode ttl tos ipopts)) {
3278             $snort_opts{'unsupported'}{$opt}
3279                 = $snort_opts{'filter'}{$opt};
3280             delete $snort_opts{'filter'}{$opt};
3281         }
3282         $non_host    = $NON_IP6_HOST;
3283         $save_str    = 'ip6tables-save';
3284         $ipt_str     = 'ip6tables';
3285         $ipt_bin     = $cmds{'ip6tables'};
3286         $restore_bin = $cmds{'ip6tables-restore'};
3287         $save_bin    = $cmds{'ip6tables-save'};
3288     }
3289
3290     unless ($is_root) {
3291         $no_ipt_test = 1;
3292     }
3293
3294     if ($ipt_exec) {
3295         die "[*] You need to be root for --ipt-apply" unless $is_root;
3296         if (-e $config{'FWSNORT_SAVE_EXEC_FILE'}) {
3297             print "[+] Executing $config{'FWSNORT_SAVE_EXEC_FILE'}\n";
3298             system $config{'FWSNORT_SAVE_EXEC_FILE'};
3299             exit 0;
3300         } else {
3301             die "[*] $config{'FWSNORT_SAVE_EXEC_FILE'} does not exist.";
3302         }
3303     }
3304
3305     if ($enable_ip6tables) {
3306         ### switch to ip6tables
3307         $ipt_var_str = 'IP6TABLES';
3308     }
3309
3310     $process_chains{'INPUT'}   = 0 if $no_ipt_input;
3311     $process_chains{'FORWARD'} = 0 if $no_ipt_forward;
3312     $process_chains{'OUTPUT'}  = 0 if $no_ipt_output;
3313
3314     ### import HOME_NET, etc. from existing Snort config file.
3315     &import_snort_conf() if $snort_conf_file;
3316
3317     if ($rules_types) {
3318         my @types = split /\,/, $rules_types;
3319         for my $type (@types) {
3320             $include_types{$type} = '';
3321         }
3322     }
3323     if ($exclude_types) {
3324         my @types = split /\,/, $exclude_types;
3325         for my $type (@types) {
3326             $exclude_types{$type} = '';
3327         }
3328     }
3329     if ($include_sids) {
3330         ### disable iptables policy parsing if we are translating a
3331         ### specific set of Snort sids.
3332         $ipt_sync = 0;
3333
3334         my @sids = split /\,/, $include_sids;
3335         for my $sid (@sids) {
3336             $include_sids{$sid} = '';
3337         }
3338     }
3339     if ($exclude_sids) {
3340         my @sids = split /\,/, $exclude_sids;
3341         for my $sid (@sids) {
3342             $exclude_sids{$sid} = '';
3343         }
3344     }
3345     if ($ipt_restrict_intf) {
3346         my @interfaces = split /\,/, $ipt_restrict_intf;
3347         for my $intf (@interfaces) {
3348             $restrict_interfaces{$intf} = '';
3349         }
3350     }
3351
3352     if ($include_re) {
3353         if ($include_re_caseless) {
3354             $include_re = qr|$include_re|i;
3355         } else {
3356             $include_re = qr|$include_re|;
3357         }
3358     }
3359     if ($exclude_re) {
3360         if ($exclude_re_caseless) {
3361             $exclude_re = qr|$exclude_re|i;
3362         } else {
3363             $exclude_re = qr|$exclude_re|;
3364         }
3365     }
3366
3367     ### flush all fwsnort chains.
3368     &ipt_flush() if $ipt_flush or $ipt_del_chains;
3369
3370     ### list all fwsnort chains.
3371     &ipt_list() if $ipt_list;
3372
3373     ### download latest snort rules from snort.org
3374     &update_rules() if $update_rules;
3375
3376     ### make sure some directories exist, etc.
3377     &setup();
3378
3379     ### get kernel version (this is mainly used to know whether
3380     ### the "--algo bm" argument is required for the string match
3381     ### extension in the 2.6.14 (and later) kernels.  Also, the
3382     ### string match extension as of 2.6.14 supports the Snort
3383     ### offset and depth keywords via --from and --to
3384     &get_kernel_ver();
3385
3386     ### may have been specified on the command line
3387     $home_net = $config{'HOME_NET'} unless $home_net;
3388     $ext_net  = $config{'EXTERNAL_NET'} unless $ext_net;
3389
3390     &get_local_addrs() unless $no_addr_check;
3391
3392     if ($strict) {
3393         ### make the snort options parser very strict
3394         for my $opt (qw(uricontent pcre
3395                 distance within http_uri http_method urilen)) {
3396             $snort_opts{'unsupported'}{$opt}
3397                 = $snort_opts{'filter'}{$opt};
3398             delete $snort_opts{'filter'}{$opt};
3399         }
3400         my @ignore = (qw(nocase));
3401
3402         if ($kernel_ver eq '2.4') {
3403             push @ignore, 'offset', 'depth';
3404         }
3405         for my $opt (@ignore) {
3406             next unless defined $snort_opts{'ignore'}{$opt};
3407             $snort_opts{'unsupported'}{$opt}
3408                 = $snort_opts{'ignore'}{$opt};
3409             delete $snort_opts{'ignore'}{$opt};
3410         }
3411     }
3412     if ($no_pcre) {
3413         ### skip trying to translate basic PCRE's
3414         $snort_opts{'unsupported'}{'pcre'}
3415             = $snort_opts{'filter'}{'pcre'};
3416         delete $snort_opts{'filter'}{'pcre'};
3417     }
3418
3419     if ($no_fast_pattern_order) {
3420         $snort_opts{'ignore'}{'fast_pattern'}
3421             = $snort_opts{'filter'}{'fast_pattern'}{'regex'};
3422         delete $snort_opts{'filter'}{'fast_pattern'};
3423     }
3424     return;
3425 }
3426
3427 sub get_kernel_ver() {
3428     die "[*] uname command: $cmds{'uname'} is not executable."
3429         unless -x $cmds{'uname'};
3430     open U, "$cmds{'uname'} -a |" or die "[*] Could not run ",
3431         "$cmds{'uname'} -a";
3432     my $out = <U>;
3433     close U;
3434     ### Linux orthanc 2.6.12.5 #2 Tue Sep 27 22:43:02 EDT 2005 i686 \
3435     ### Pentium III (Coppermine) GenuineIntel GNU/Linux
3436     if ($out =~ /\s2\.6/) {
3437         $kernel_ver = '2.6';
3438     }
3439     return;
3440 }
3441
3442 sub handle_cmd_line() {
3443
3444     ### make Getopts case sensitive
3445     Getopt::Long::Configure('no_ignore_case');
3446
3447     die "[*] Use --help for usage information.\n" unless (GetOptions(
3448         'ipt-apply'      => \$ipt_exec,     # Apply the generated ruleset.
3449         'ipt-drop'       => \$ipt_drop,     # Add iptables DROP rules.
3450         'ipt-reject'     => \$ipt_reject,   # Add iptables REJECT rules.
3451         'ipt-script=s'   => \$ipt_script,   # Manually specify the path to the
3452                                             # generated iptables script.
3453         'ipt-log-tcp-seq' => \$ipt_log_tcp_seq, # Log TCP seq/ack values.
3454         'ipt-flush'      => \$ipt_flush,    # Flush any existing fwsnort chains.
3455         'ipt-check-capabilities' =>\$ipt_check_capabilities, # Check capabilities
3456                                                              # and exit.
3457         'Flush'          => \$ipt_flush,    # Synonym for --ipt-flush
3458         'ipt-list'       => \$ipt_list,     # List any existing fwsnort chains.
3459         'List'           => \$ipt_list,     # Synonym for --ipt-list
3460         'ipt-del'        => \$ipt_del_chains, # Delete fwsnort chains.
3461         'ip6tables'      => \$enable_ip6tables, # Turn on ip6tables mode.
3462         '6'              => \$enable_ip6tables, # Synonym for --ip6tables.
3463         'X'              => \$ipt_del_chains, # Synonym for --ipt-del.
3464         'ipt-file=s'     => \$ipt_file,     # Read iptables policy from a file.
3465         'Home-net=s'     => \$home_net,     # Manually specify home network.
3466         'External-net=s' => \$ext_net,      # Manually specify external network.
3467         'snort-sid=s'    => \$include_sids, # Parse only these particular snort rules.
3468         'snort-sids=s'   => \$include_sids, # Synonum for --snort-sid
3469         'string-match-alg=s' => \$string_match_alg,
3470         'exclude-sid=s'  => \$exclude_sids, # Exclude these particular snort rules.
3471         'snort-conf=s'   => \$snort_conf_file, # Get HOME_NET, etc. vars from
3472                                             # existing Snort config file.
3473         'include-perl-triggers' => \$include_perl_triggers, # perl commands to
3474                                             # trigger signature matches.
3475         'include-type=s' => \$rules_types,  # Process only this type of snort rule
3476                                             # (e.g. "ddos")
3477         'exclude-type=s' => \$exclude_types,# Exclude specified types (e.g. "ddos").
3478         'include-regex=s' => \$include_re,  # Include only those signatures that
3479                                             # match the specified regex.
3480         'include-re-caseless' => \$include_re_caseless, # make include regex case
3481                                                         # insensitive
3482         'exclude-regex=s' => \$exclude_re,  # Exclude those signatures that
3483                                             # match the specified regex.
3484         'exclude-re-caseless' => \$exclude_re_caseless, # make exclude regex case
3485                                                         # insensitive
3486         'snort-rdir=s'   => \$rules_dir,    # Manually specify the snort rules
3487                                             # directory.
3488         'snort-rfile=s'  => \$rules_file,   # Translate a single rules file.
3489         'no-pcre'        => \$no_pcre,      # Make no attempt to translate PCRE's.
3490         'no-addresses'   => \$no_addr_check, # Don't check local ifconfig output.
3491         'no-ipt-sync'    => \$ignore_opt,   # Do not sync with the iptables policy.
3492         'ipt-sync'       => \$ipt_sync,     # Sync fwsnort rules with the iptables
3493                                             # policy.
3494         'no-ipt-log'     => \$no_ipt_log,   # Do not generate iptables logging rules.
3495         'no-ipt-test'    => \$no_ipt_test,  # Don't perform any checks for
3496                                             # iptables capabilities.
3497         'no-ipt-jumps'   => \$no_ipt_jumps, # Don't jump packets from the INPUT or
3498                                             # FORWARD chains.
3499         'no-ipt-conntrack' => \$no_ipt_conntrack, # Don't use iptables connection
3500                                             # tracking (falls back to ACK flag test).
3501         'Conntrack-state=s' => \$conntrack_state, ### Specify conntrack state for
3502                                                   ### 'flow' keyword emulation
3503                                                   ### (default is ESTABLISHED).
3504         'no-ipt-INPUT'   => \$no_ipt_input, # Disable fwsnort rules processed via
3505                                             # the INPUT chain.
3506         'no-ipt-OUTPUT'  => \$no_ipt_output, # Disable fwsnort rules processed via
3507                                              # the OUTPUT chain.
3508         'no-ipt-FORWARD' => \$no_ipt_forward, # Disable fwsnort rules processed via
3509                                               # the FORWARD chain.
3510         'no-ipt-comments' => \$no_ipt_comments, # Don't include msg fields
3511                                                 # with the comment match
3512         'no-ipt-rule-nums' => \$no_ipt_rule_nums, # Exclude rule numbers from
3513                                                   # logging prefixes.
3514         'no-exclude-lo'  => \$no_exclude_loopback, # include loopback interface
3515         'no-log-ip-opts' => \$no_ipt_log_ip_opts, # Don't log IP options
3516         'no-log-tcp-opts' => \$no_ipt_log_tcp_opts, # Don't log TCP options
3517         'no-fast-pattern-order' => \$no_fast_pattern_order, ### Don't alter
3518                                               # pattern match ordering based on
3519                                               # pattern length, and ignore the
3520                                               # explicit 'fast_pattern' keyword
3521         'restrict-intf=s' => \$ipt_restrict_intf, # Restrict iptables rules to an
3522                                             # individual interface (supports a
3523                                             # comma separate list).
3524         'update-rules'   => \$update_rules, # Download latest snort rules.
3525         'rules-url=s'    => \$rules_url,    # Specify rules URL.
3526         'add-deleted'    => \$add_deleted,  # Add deleted rules.
3527         'strict'         => \$strict,       # Strict mode.
3528         'debug'          => \$debug,        # Debug mode.
3529         'dumper'         => \$dumper,       # Dumper mode for IPTables::Parse
3530                                             # hashes.
3531         'Dump-conf'      => \$dump_conf,    # Display config variables
3532         'Dump-ipt'       => \$dump_ipt,     # Dump iptables rules on STDOUT.
3533         'Dump-snort'     => \$dump_snort,   # Dump snort rules on STDOUT.
3534         'config=s'       => \$fwsnort_conf, # Manually specify the config file
3535         'Ulog'           => \$ulog_mode,    # Force ULOG mode.
3536         'ulog-nlgroup=i' => \$ulog_nlgroup, # Specify the ulogd nl group.
3537         'QUEUE'          => \$queue_mode,   # Specify QUEUE mode; this pulls out
3538                                             #  all kernel-matchable features from
3539                                             #  original Snort rules and creates a
3540                                             #  a modified rule set based on this.
3541         'NFQUEUE'        => \$nfqueue_mode, # Same as QUEUE mode, except use the
3542                                             #  updated NFQUEUE target.
3543         'queue-rules-dir=s' => \$queue_rules_dir, # Change the path to the generated
3544                                                   # rules directory in --QUEUE or
3545                                                   # --NFQUEUE mode.
3546         'queue-num=i'    => \$nfqueue_num,  # Specifies the NFQUEUE number.
3547         'queue-pre-match-max=i' => \$queue_pre_match_max,  ### max number of patterns
3548                                                   ### to match within the kernel before
3549                                                   ### queuing a packet to userspace
3550                                                   ### Snort
3551         'Home-dir=s'     => \$cmdl_homedir,
3552         'Last-cmd'       => \$run_last,
3553         'lib-dir=s'      => \$lib_dir,      # Specify path to lib directory.
3554         'verbose'        => \$verbose,
3555         'logfile=s'      => \$logfile,      # Specify the logfile path.
3556         'stdout'         => \$stdout,       # Print log messages to stdout.
3557         'Version'        => \$print_ver,
3558         'help'           => \$help
3559     ));
3560
3561     &get_homedir();
3562
3563     &usage(0) if $help;
3564
3565     &save_args() unless $run_last;
3566
3567     ### Print the version number and exit if -V given on the command line.
3568     if ($print_ver) {
3569         print "[+] fwsnort v$version by Michael Rash <mbr\@cipherdyne.org>\n";
3570         exit 0;
3571     }
3572
3573     if (($queue_mode or $nfqueue_mode) and ($ipt_drop or $ipt_reject)) {
3574         die
3575 "[*] --NFQUEUE and --QUEUE modes are not compatible with --ipt-drop or\n",
3576 "    --ipt-reject; a userland process should set the verdict. If you can\n",
3577 "    always use fwsnort with --ipt-drop or --ipt-reject and add an NFQUEUE\n",
3578 "    or QUEUE rule manually to a built-in chain. This allows the fwsnort\n",
3579 "    policy to DROP or REJECT packets that match signatures before they are\n",
3580 "    communicated to userland (hence speeding up Snort_inline).\n";
3581     }
3582
3583     if ($nfqueue_num != 0) {
3584         unless ($nfqueue_num > 0 and $nfqueue_num < 65536) {
3585             die "[*] --queue-num must be between 0 and 65535 (inclusive)";
3586         }
3587         unless ($nfqueue_mode) {
3588             die "[*] Must also specifiy --NFQUEUE mode if using --queue-num";
3589         }
3590     }
3591
3592     if ($no_ipt_log and not ($ipt_drop or $ipt_reject)) {
3593         die "[*] --ipt-no-log option can only be used ",
3594             "with --ipt-drop or --ipt-reject";
3595     }
3596
3597     if ($ipt_drop and $ipt_reject) {
3598         die "[*] Cannot specify both --ipt-drop and --ipt-reject";
3599     }
3600
3601     return;
3602 }
3603
3604 sub import_snort_conf() {
3605     unless (-e $snort_conf_file) {
3606         die "[*] Snort config file $snort_conf_file does not exist.";
3607     }
3608     open F, "< $snort_conf_file" or die "[*] Could not open Snort ",
3609         "config $snort_conf_file: $!";
3610     my @lines = <F>;
3611     close F;
3612     for my $line (@lines) {
3613         chomp $line;
3614         next if $line =~ /^\s*#/;
3615         if ($line =~ /^\s*var\s+(\w+)\s+(.*)\s*/) {
3616             $config{$1} = $2;
3617         }
3618     }
3619     return;
3620 }
3621
3622 sub expand_vars() {
3623
3624     my $has_sub_var = 1;
3625     my $resolve_ctr = 0;
3626
3627     while ($has_sub_var) {
3628         $resolve_ctr++;
3629         $has_sub_var = 0;
3630         if ($resolve_ctr >= 20) {
3631             die "[*] Exceeded maximum variable resolution counter.";
3632         }
3633         for my $hr (\%config, \%cmds) {
3634             for my $var (keys %$hr) {
3635                 my $val = $hr->{$var};
3636                 if ($val =~ m|\$(\w+)|) {
3637                     my $sub_var = $1;
3638                     die "[*] sub-ver $sub_var not allowed within same ",
3639                         "variable $var" if $sub_var eq $var;
3640                     if (defined $config{$sub_var}) {
3641                         $val =~ s|\$$sub_var|$config{$sub_var}|;
3642                         $hr->{$var} = $val;
3643                     } else {
3644                         die "[*] sub-var \"$sub_var\" not defined in ",
3645                             "config for var: $var.";
3646                     }
3647                     $has_sub_var = 1;
3648                 }
3649             }
3650         }
3651     }
3652     return;
3653 }
3654
3655 sub required_vars() {
3656     my @required_vars = (qw(
3657         HOME_NET EXTERNAL_NET HTTP_SERVERS SMTP_SERVERS DNS_SERVERS
3658         SQL_SERVERS TELNET_SERVERS AIM_SERVERS HTTP_PORTS SHELLCODE_PORTS
3659         SSH_PORTS ORACLE_PORTS WHITELIST BLACKLIST AVG_IP_HEADER_LEN
3660         AVG_TCP_HEADER_LEN MAX_FRAME_LEN FWSNORT_INPUT FWSNORT_INPUT_ESTAB
3661         FWSNORT_OUTPUT FWSNORT_OUTPUT_ESTAB FWSNORT_FORWARD
3662         FWSNORT_FORWARD_ESTAB FWSNORT_INPUT_JUMP FWSNORT_OUTPUT_JUMP
3663         FWSNORT_FORWARD_JUMP MAX_STRING_LEN CONF_DIR RULES_DIR ARCHIVE_DIR
3664         QUEUE_RULES_DIR LOG_DIR LIBS_DIR CONF_FILE FWSNORT_SCRIPT LOG_FILE
3665         FWSNORT_SAVE_FILE FWSNORT_SAVE_EXEC_FILE IPT_BACKUP_SAVE_FILE
3666         UPDATE_RULES_URL STATE_DIR
3667     ));
3668     for my $var (@required_vars) {
3669         die "[*] Variable $var not defined in $fwsnort_conf. Exiting.\n"
3670             unless defined $config{$var};
3671     }
3672     return;
3673 }
3674
3675 sub ipt_capabilities() {
3676
3677     my $test_rule_rv = -1;
3678
3679     ### create test chain
3680     &create_test_chain();
3681
3682     ### test for the LOG target.
3683     if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3684             "$non_host -j LOG") == $IPT_SUCCESS) {
3685         print "[+] $ipt_str has 'LOG' target support...\n"
3686             if $verbose or $ipt_check_capabilities;
3687
3688         ### check for the max --log-prefix string length
3689         &ipt_find_max_log_prefix_len();
3690
3691     } else {
3692         &delete_test_chain();
3693         die "[*] $ipt_str has not been compiled with logging support.  ",
3694             "If you want to\n    have fwsnort generate an $ipt_str script ",
3695             "    anyway then specify the\n    --no-ipt-test option. ",
3696             "Exiting.\n"
3697             unless $no_ipt_log;
3698     }
3699
3700     ### test for the comment match (where Snort msg fields are placed)
3701     if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3702             qq|$non_host -m comment --comment "testing the comment match" | .
3703             qq|-j LOG|) == $IPT_SUCCESS) {
3704         print "[+] $ipt_str has 'comment' match support...\n"
3705             if $verbose or $ipt_check_capabilities;
3706
3707         ### now find the maximum comment length that is supported by iptables
3708         &ipt_find_max_comment_len();
3709
3710     } else {
3711         unless ($no_ipt_comments) {
3712             print"[-] It looks like the $ipt_str 'comment' match is not ",
3713                 "available, disabling.\n";
3714             $no_ipt_comments = 1;
3715         }
3716     }
3717
3718     ### test for the ipv4options extension.
3719     if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -p icmp -m " .
3720             "ipv4options --rr -s $non_host -j LOG") == $IPT_SUCCESS) {
3721         print "[+] $ipt_str has the 'ipv4options' extension...\n"
3722             if $verbose or $ipt_check_capabilities;
3723     } else {
3724
3725         &logr("[-] $ipt_str ipv4options extension not available, " .
3726             "disabling ipopts translation.");
3727         ### put ipopts in the unsupported list
3728         if (defined $snort_opts{'filter'}{'ipopts'}) {
3729             $snort_opts{'unsupported'}{'ipopts'} =
3730                 $snort_opts{'filter'}{'ipopts'}{'regex'};
3731             delete $snort_opts{'filter'}{'ipopts'};
3732         } else {
3733             $snort_opts{'unsupported'}{'ipopts'} = '[\s;]ipopts:\s*(\w+)\s*;';
3734         }
3735         print "[-] $ipt_str does not have the 'ipv4options' extension, " .
3736             "disabling...\n" if $verbose or $ipt_check_capabilities;
3737     }
3738
3739     ### test for the ttl match.
3740     if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -p icmp " .
3741             "-s $non_host -m ttl --ttl-eq 1 -j LOG") == $IPT_SUCCESS) {
3742         print "[+] $ipt_str has the 'ttl' match...\n"
3743             if $verbose or $ipt_check_capabilities;
3744     } else {
3745         ### put ttl in the unsupported list
3746         &logr("[-] $ipt_str TTL match not available, " .
3747             "disabling ttl translation.");
3748         if (defined $snort_opts{'filter'}{'ttl'}) {
3749             $snort_opts{'unsupported'}{'ttl'} =
3750                 $snort_opts{'filter'}{'ttl'}{'regex'};
3751             delete $snort_opts{'filter'}{'ttl'};
3752         } else {
3753             $snort_opts{'unsupported'}{'ttl'} = '[\s;]ttl:\s*(.*?)\s*;';
3754         }
3755         print "[+] $ipt_str does not have the 'ttl' match, " .
3756             "disabling...\n" if $verbose or $ipt_check_capabilities;
3757     }
3758
3759     ### test for the TOS match.
3760     if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -p icmp " .
3761             "-s $non_host -m tos --tos 8 -j LOG") == $IPT_SUCCESS) {
3762         print "[+] $ipt_str has the 'tos' match...\n"
3763             if $verbose or $ipt_check_capabilities;
3764     } else {
3765         ### put tos in the unsupported list
3766         &logr("[-] $ipt_str TOS match not available, " .
3767             "disabling tos translation.");
3768         if (defined $snort_opts{'filter'}{'tos'}) {
3769             $snort_opts{'unsupported'}{'tos'} =
3770                 $snort_opts{'filter'}{'tos'}{'regex'};
3771             delete $snort_opts{'filter'}{'tos'};
3772         } else {
3773             $snort_opts{'unsupported'}{'tos'} = '[\s;]tos:\s*(.*?)\s*;';
3774         }
3775         print "[+] $ipt_str does not have the 'tos' match, " .
3776             "disabling...\n" if $verbose or $ipt_check_capabilities;
3777     }
3778
3779     ### test for the length match.
3780     if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -p icmp " .
3781             "-s $non_host -m length --length 256 -j LOG") == $IPT_SUCCESS) {
3782         print "[+] $ipt_str has the 'length' match...\n"
3783             if $verbose or $ipt_check_capabilities;
3784     } else {
3785         ### put length in the unsupported list
3786         &logr("[-] $ipt_str length match not available, " .
3787             "disabling length translation.");
3788         if (defined $snort_opts{'filter'}{'dsize'}) {
3789             $snort_opts{'unsupported'}{'dsize'} =
3790                 $snort_opts{'filter'}{'dsize'}{'regex'};
3791             delete $snort_opts{'filter'}{'dsize'};
3792         } else {
3793             $snort_opts{'unsupported'}{'dsize'} = '[\s;]dsize:\s*(.*?)\s*;';
3794         }
3795         print "[+] $ipt_str does not have the 'length' match, " .
3796             "disabling...\n" if $verbose or $ipt_check_capabilities;
3797     }
3798
3799     ### test for the multiport match.
3800     if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -p tcp " .
3801             "-s $non_host -m multiport --dports 53,123:500 -j LOG") == $IPT_SUCCESS) {
3802         print "[+] $ipt_str has the 'multiport' match...\n"
3803             if $verbose or $ipt_check_capabilities;
3804         $ipt_have_multiport_match = 1;
3805
3806         ### find the maximum number of supported ports (usually 15)
3807         &ipt_find_max_multiport_supported_ports();
3808
3809     } else {
3810         print "[-] $ipt_str does not have the 'multiport' match...\n"
3811             if $verbose or $ipt_check_capabilities;
3812         &logr("[-] $ipt_str multiport match not available");
3813     }
3814
3815     ### test for string match support.
3816     if ($kernel_ver ne '2.4') {
3817
3818         ### default to include "--algo bm"
3819         $test_rule_rv = &ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3820             qq|$non_host -m string --string "test" | .
3821             qq|--algo $string_match_alg -j LOG|);
3822     } else {
3823         $test_rule_rv = &ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3824             qq|$non_host -m string --string "test" -j LOG|);
3825     }
3826
3827     if ($test_rule_rv == $IPT_SUCCESS) {
3828
3829         print "[+] $ipt_str has the 'string' match...\n"
3830             if $verbose or $ipt_check_capabilities;
3831
3832         ### now find the maximum string length that is supported by iptables
3833         &ipt_find_max_string_len();
3834
3835         ### test for case insensitive string matching
3836         $test_rule_rv = &ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3837             qq|$non_host -m string --string "test" --icase | .
3838             qq|--algo $string_match_alg -j LOG|);
3839         unless ($test_rule_rv == $IPT_SUCCESS) {
3840             $snort_opts{'ignore'}{'nocase'}
3841                 = $snort_opts{'filter'}{'nocase'}{'regex'};
3842             delete $snort_opts{'filter'}{'fast_pattern'};
3843         }
3844
3845         ### test for --replace-string support (only available for 2.4 kernels
3846         ### if the replace-string patch has been applied).
3847         if ($kernel_ver eq '2.4') {
3848             unless (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3849                     qq|$non_host -m string --string "test" --replace-string | .
3850                     qq|"repl" -j LOG|) == $IPT_SUCCESS) {
3851                 if (defined $snort_opts{'filter'}{'replace'}) {
3852                     $snort_opts{'unsupported'}{'replace'} =
3853                         $snort_opts{'filter'}{'replace'}{'regex'};
3854                     delete $snort_opts{'filter'}{'replace'};
3855                 } else {
3856                     $snort_opts{'unsupported'}{'replace'}
3857                         = '[\s;]replace:\s*(.*?)\s*;';
3858                 }
3859             }
3860         } else {
3861             $snort_opts{'unsupported'}{'replace'}
3862                 = '[\s;]replace:\s*(.*?)\s*;';
3863         }
3864     } else {
3865         &delete_test_chain();
3866         die
3867 "[*] It does not appear that string match support has been compiled into\n",
3868 "    the kernel.  Fwsnort will not be of very much use without this.\n",
3869 "    ** NOTE: If you want to have fwsnort generate an $ipt_str policy\n",
3870 "    anyway, use the --no-ipt-test option.  Exiting.\n";
3871     }
3872
3873     ### test for --hex-string
3874     if ($kernel_ver ne '2.4') {
3875         $test_rule_rv = &ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM " .
3876             "-s $non_host " .
3877             qq{-m string --hex-string "|0a 5d|" --algo $string_match_alg -j LOG});
3878     } else {
3879         $test_rule_rv = &ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM " .
3880             "-s $non_host " .
3881             qq{-m string --hex-string "|0a 5d|" -j LOG});
3882     }
3883
3884     if ($test_rule_rv == $IPT_SUCCESS) {
3885         print "[+] $ipt_str has --hex-string support...\n"
3886             if $verbose or $ipt_check_capabilities;
3887     } else {
3888         &delete_test_chain();
3889         die
3890 "[*] It does not appear that the --hex-string patch has been applied.\n",
3891 "    fwsnort will not be of very much use without this. ** NOTE: If you\n",
3892 "    want to have fwsnort generate an $ipt_str policy anyway, then\n",
3893 "    use the --no-ipt-test option.  Exiting.\n";
3894     }
3895
3896     ### test for the --payload option
3897     if ($kernel_ver ne '2.4'
3898             and &ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM " .
3899             "-s $non_host -m string --string " .
3900             qq|"test" --algo $string_match_alg --to 50 --payload -j LOG|) == $IPT_SUCCESS) {
3901         $ipt_has_string_payload_offset_opt = 1;
3902     }
3903
3904     if ($queue_mode or $nfqueue_mode) {
3905         ### test for the QUEUE or NFQUEUE target
3906         if ($nfqueue_mode) {
3907             if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3908                     "$non_host -p tcp --dport 3001 -j NFQUEUE")
3909                         == $IPT_SUCCESS) {
3910                 print "[+] $ipt_str has NFQUEUE support....\n"
3911                     if $verbose or $ipt_check_capabilities;
3912             } else {
3913                 die "[*] The NFQUEUE target does not appear to be available ",
3914                     "in iptables.";
3915             }
3916         } else {
3917             if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3918                     "$non_host -p tcp --dport 3001 -j QUEUE")
3919                         == $IPT_SUCCESS) {
3920                 print "[+] $ipt_str has QUEUE support....\n"
3921                     if $verbose or $ipt_check_capabilities;
3922             } else {
3923                 die "[*] The QUEUE target does not appear to be available ",
3924                     "in iptables.";
3925             }
3926         }
3927     }
3928
3929     unless ($no_ipt_conntrack) {
3930
3931         ### test for connection tracking support (conntrack
3932         ### match first then state match if not available)
3933         if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3934                 "$non_host -p tcp --dport 3001 -m conntrack " .
3935                 "--ctstate ESTABLISHED -j LOG")
3936                     == $IPT_SUCCESS) {
3937             print "[+] $ipt_str has conntrack state tracking support...\n"
3938                 if $verbose or $ipt_check_capabilities;
3939
3940             if ($conntrack_state ne 'ESTABLISHED') {
3941
3942                 ### check to make sure the specified state is supported
3943                 if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3944                         "$non_host -p tcp --dport 3001 -m conntrack " .
3945                         "--ctstate $conntrack_state -j LOG")
3946                             != $IPT_SUCCESS) {
3947                      die "[*] The connection state $conntrack_state does not ",
3948                         "appear to be supported by iptables.\n";
3949                 }
3950             }
3951
3952             $have_conntrack = 1;
3953
3954         } elsif (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3955                 "$non_host -p tcp --dport 3001 -m state " .
3956                 "--state ESTABLISHED -j LOG")
3957                     == $IPT_SUCCESS) {
3958             print "[+] $ipt_str has state tracking support...\n"
3959                 if $verbose or $ipt_check_capabilities;
3960
3961             if ($conntrack_state ne 'ESTABLISHED') {
3962
3963                 ### check to make sure the specified state is supported
3964                 if (&ipt_rule_test("-I $TEST_CHAIN $IPT_TEST_RULE_NUM -s " .
3965                         "$non_host -p tcp --dport 3001 -m state " .