bumped version to 1.6.2
[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
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';
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 $default_icmp_type = 8;  ### echo request
387 my $ipt_print_type = 0;
388 my $ipt_check_capabilities = 0;
389 my $ipt_rule_ctr   = 1;
390 my $ipt_sync       = 0;
391 my $ipt_flush      = 0;
392 my $ipt_del_chains = 0;
393 my $ipt_list       = 0;
394 my $ipt_file       = '';
395 my $no_pcre        = 0;
396 my $no_ipt_log     = 0;
397 my $no_ipt_test    = 0;
398 my $no_ipt_jumps   = 0;
399 my $no_ipt_input   = 0;
400 my $no_ipt_output  = 0;
401 my $no_addr_check  = 0;
402 my $no_ipt_forward = 0;
403 my $ignore_opt     = 0;
404 my $include_sids   = '';
405 my $exclude_sids   = '';
406 my $add_deleted    = 0;
407 my $rules_types    = '';
408 my $exclude_types  = '';
409 my $snort_type     = '';
410 my $ulog_nlgroup   = 1;
411 my $queue_mode     = 0;
412 my $nfqueue_mode   = 0;
413 my $nfqueue_num    = 0;
414 my $ulog_mode      = 0;
415 my $exclude_re     = '';
416 my $include_re     = '';
417 my $include_re_caseless = 0;
418 my $exclude_re_caseless = 0;
419 my $enable_ip6tables  = 0;
420 my $ipt_var_str       = 'IPTABLES';
421 my $no_ipt_conntrack  = 0;
422 my $conntrack_state   = 'ESTABLISHED';
423 my $have_conntrack    = 0;
424 my $have_state        = 0;
425 my $snort_conf_file   = '';
426 my $ipt_restrict_intf = '';
427 my $no_ipt_comments  = 0;
428 my $no_ipt_rule_nums = 0;
429 my $no_exclude_loopback = 0;
430 my $no_ipt_log_ip_opts  = 0;
431 my $no_ipt_log_tcp_opts = 0;
432 my $ipt_log_tcp_seq     = 0;
433 my $include_perl_triggers = 0;
434 my $duplicate_last_build  = 0;
435 my $ipt_max_str_len = 1;
436 my $ipt_max_log_prefix_len = 1;
437 my $ipt_max_comment_len = 1;
438 my $no_fast_pattern_order = 0;
439 my $ipt_have_multiport_match = 0;
440 my $ipt_multiport_max = 2;
441
442 ### to be added to the string match extension
443 my $ipt_has_string_payload_offset_opt = 0;
444
445 ### default to processing these filter chains
446 my %process_chains = (
447     'INPUT'   => 1,
448     'FORWARD' => 1,
449     'OUTPUT'  => 1,
450 );
451 my $TEST_CHAIN = 'FWS_CAP_TEST';
452
453 my %chain_ctr = ();
454
455 ### save a copy of the command line args
456 my @argv_cp = @ARGV;
457
458 ### see if we are running as root
459 &is_root();
460
461 ### handle the command line args
462 &handle_cmd_line();
463
464 &run_last_cmdline() if $run_last;
465
466 ### import config, initialize various things, etc.
467 &fwsnort_init();
468
469 ### if we are running with $chk_ipt_policy, then cache
470 ### the current iptables policy
471 &cache_ipt_policy() if $ipt_sync;
472
473 ### truncate old fwsnort log
474 &truncate_logfile();
475
476 ### check to make sure iptables has various functionality available
477 ### such as the LOG target, --hex-strings, the comment match, etc.
478 &ipt_capabilities() unless $no_ipt_test;
479
480 ### cache the running iptables policy in iptables-save format
481 &cache_ipt_save_policy();
482
483 ### print a header at the top of the iptables ruleset
484 ### script
485 &ipt_hdr();
486
487 ### now that we have the interfaces, add the iptables
488 ### chains to the fwsnort shell script
489 &ipt_add_chains();
490
491 ### add any WHITELIST rules to the main fwsnort chains
492 ### with the RETURN target
493 &ipt_whitelist();
494
495 ### add any BLACKLIST rules to the main fwsnort chains
496 ### with the DROP or REJECT targets
497 &ipt_blacklist();
498
499 ### add jump rules for established tcp connections to
500 ### the fwsnort state tracking chains
501 &ipt_add_conntrack_jumps() unless $no_ipt_conntrack;
502
503 ### display the config on STDOUT
504 &dump_conf() if $dump_conf;
505
506 ### make sure <type>.rules file exists if --type was
507 ### specified on the command line
508 &check_type() if $rules_types;
509
510 &logr("[+] Begin parsing cycle.");
511
512 ### parse snort rules (signatures)
513 if ($include_sids) {
514     print "[+] Parsing Snort rules files...\n";
515 } else {
516     if ($ipt_sync) {
517         print "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=",
518             "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n",
519             sprintf("%-30s%-10s%-10s%-10s%-10s", '    Snort Rules File',
520                 'Success', 'Fail', 'Ipt_apply', 'Total'), "\n\n";
521     } else {
522         print "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=",
523             "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n",
524             sprintf("%-30s%-10s%-10s%-10s", '    Snort Rules File',
525                 'Success', 'Fail', 'Total'), "\n\n";
526     }
527 }
528
529 ### main subroutine to parse snort rules and add them to the
530 ### fwsnort.sh script.
531 &parse_snort_rules();
532
533 ### append all translated rules to the iptables-save formatted array
534 &save_format_append_rules();
535
536 ### jump packets (as appropriate) from the INPUT and
537 ### FORWARD chains to our fwsnort chains
538 &ipt_jump_chain() unless $no_ipt_jumps;
539
540 push @ipt_script_lines, qq|\n\$ECHO "[+] Finished."|, '### EOF ###';
541 push @ipt_save_script_lines, $ipt_script_lines[$#ipt_script_lines];
542
543 print "\n[+] Logfile: $config{'LOG_FILE'}\n";
544
545 if ($ipt_rule_ctr > 1) {
546
547     ### write the iptables script out to disk
548     &write_ipt_script();
549
550     if ($queue_mode or $nfqueue_mode) {
551         print "[+] Snort rule set directory for rules to be queued ",
552             "to userspace:\n        $config{'QUEUE_RULES_DIR'}\n";
553     }
554     print "[+] $ipt_str script (individual commands): " .
555         "$config{'FWSNORT_SCRIPT'}\n";
556
557 } else {
558     die "[-] No Snort rules could be translated, exiting\n";
559 }
560
561 &write_save_file();
562
563 &print_final_message();
564
565 exit 0;
566 #===================== end main ======================
567
568 sub parse_snort_rules() {
569
570     my @rfiles = ();
571
572     my $cwd = cwd();
573
574     if ($rules_file) {
575         @rfiles = split /\,/, $rules_file;
576     } else {
577         for my $dir (split /\,/, $config{'RULES_DIR'}) {
578             opendir D, $dir or die "[*] Could not opendir $dir";
579             for my $file (readdir D) {
580                 push @rfiles, "$dir/$file";
581             }
582             closedir D;
583         }
584     }
585
586     my $sabs_num = 0;
587     my $tot_ipt_apply = 0;
588     my $tot_unsup_ctr = 0;
589     FILE: for my $rfile (sort @rfiles) {
590         $rfile = $cwd . '/' . $rfile unless $rfile =~ m|^/|;
591         my $type = '';
592         my $filename = '';
593         if ($rfile =~ m|.*/(\S+\.rules)$|) {
594             $filename = $1;
595         }
596         if ($rfile =~ m|.*/(\S+)\.rules$|) {
597             $type = $1;
598         } else {
599             next FILE;
600         }
601         $ipt_print_type = 0;
602         if ($rules_types) {
603             next FILE unless defined $include_types{$type};
604         }
605         if ($exclude_types) {
606             next FILE if defined $exclude_types{$type};
607         }
608         if ($rfile eq 'deleted.rules') {
609             next FILE unless $add_deleted;
610         }
611         ($snort_type) = ($rfile =~ m|.*/(\S+)\.rules|);
612         printf("%-30s", "[+] $filename") unless $include_sids;
613
614         &logr("[+] Parsing $rfile");
615         open R, "< $rfile" or die "[*] Could not open: $rfile";
616         my @lines = <R>;
617         close R;
618
619         ### contains Snort rules that will be used by Snort_inline
620         ### if fwsnort is building a QUEUE policy; these rules have
621         ### met the criteria that at least one "content" match is
622         ### required.
623         my @queue_rules = ();
624
625         my $line_num   = 0;
626         my $rule_num   = 0;
627         my $parsed_ctr = 0;
628         my $unsup_ctr  = 0;
629         my $ipt_apply_ctr = 0;
630         my $ipt_rules_ctr = 0;
631
632         RULE: for my $rule (@lines) {
633             chomp $rule;
634             my $rule_hdr;
635             my $rule_options;
636             $line_num++;
637
638             ### pass == ACCEPT, log == ULOG
639             unless ($rule =~ /^\s*alert/ or $rule =~ /^\s*pass/
640                     or $rule =~ /^\s*log/) {
641                 next RULE;
642             }
643
644             ### regex filters
645             if ($exclude_re) {
646                 next RULE unless $rule =~ $exclude_re;
647             }
648
649             if ($include_re) {
650                 next RULE unless $rule =~ $include_re;
651             }
652
653             $rule_num++;  ### keep track of the abs num of rules
654             $sabs_num++;
655
656             if ($rule =~ m|^(.*?)\s+\((.*)\)|) {
657                 $rule_hdr     = $1;
658                 $rule_options = " $2 ";  ### allows out-of-order options
659             } else {
660                 &logr("[-] Unrecognized rule format at line: $line_num. " .
661                     "Skipping.");
662                 next RULE;
663             }
664
665             ### skip all icmp "Undefined Code" rules; psad properly
666             ### handles this, but not fwsnort (see the icmp-info.rules
667             ### file).
668             if ($filename =~ /icmp/ and $rule_options =~ /undefined\s+code/i) {
669                 $unsup_ctr++;
670                 $tot_unsup_ctr++;
671                 next RULE;
672             }
673
674             ### parse header portion of Snort rule
675             my $hdr_hr = &parse_rule_hdr($rule_hdr, $line_num);
676             unless (keys %$hdr_hr) {
677                 &logr("[-] Unrecognized rule header: \"$rule_hdr\" at " .
678                     "line: $line_num, skipping.");
679                 $unsup_ctr++;
680                 $tot_unsup_ctr++;
681                 next RULE;
682             }
683
684             ### parse options portion of Snort rule
685             my ($parse_rv, $opts_hr, $patterns_ar)
686                             = &parse_rule_options($rule_options,
687                                     &get_avg_hdr_len($hdr_hr),
688                                     $line_num);
689
690             unless ($parse_rv) {
691                 $unsup_ctr++;
692                 $tot_unsup_ctr++;
693                 next RULE;
694             }
695             if ($include_sids) {
696                 print "[+] Found sid: $opts_hr->{'sid'} in $filename\n";
697             }
698
699             if ($queue_mode or $nfqueue_mode) {
700
701                 ### In general, it is not easy to modify the signatures that
702                 ### snort_inline would use; one would think that an optimzation
703                 ### would be to remove all "content" keywords since the kernel
704                 ### itself is doing this now, but consider the following:
705
706                 ### Suppose there are two original Snort signatures like so:
707                 ###
708                 ###     msg: "SIG1"; content: "abc"; pcre: "(d|e)";
709                 ###     msg: "SIG2"; content: "xyz"; pcre: "(e|f)";
710                 ###
711                 ### Now, suppose there is a packet with the following data:
712                 ###
713                 ###     packet data: "xyz------------e------"
714                 ###
715                 ### Then the SIG1 matches when it shouldn't because the packet
716                 ### does not contain "abc" (assuming the "abc" string is
717                 ### removed from the signature that is actually deployed with
718                 ### snort_inline).  There does not seem to be a good solution
719                 ### for this problem if pcre criteria are involved because the
720                 ### two pcre's would have to be interpreted to see if there is
721                 ### any data that could satisfy both at the same time.
722
723                 ### However, performing the duplicate string matching is far
724                 ### less expensive than not sending a large portion of network
725                 ### traffic to userspace for analysis by snort_inline in the
726                 ### first place.  This is the real benefit of letting fwsnort
727                 ### build a smarter iptables queueing policy.  This does come
728                 ### with a penalty against detection, since snort_inline is
729                 ### only receiving individual packets that match one of the
730                 ### content keywords in a signature; it does not get the
731                 ### entire stream.  But, this may be worth it for large sites
732                 ### where performance is the primary concern.  Also, there is
733                 ### some potential for removing a subset of the content
734                 ### matches if done in the right way; this is the reason the
735                 ### queue_get_rule() function is stubbed in below.
736                 my $queue_rule = &queue_get_rule($rule_hdr, $rule_options);
737
738                 push @queue_rules, $queue_rule if $queue_rule;
739             }
740
741             ### construct the equivalent iptables rule and add it
742             ### to $config{'FWSNORT_SCRIPT'}
743             my ($ipt_rv, $num_rules) = &ipt_build($hdr_hr,
744                     $opts_hr, $patterns_ar, $rule);
745
746             if ($ipt_rv) {
747                 $ipt_apply_ctr++;
748                 $tot_ipt_apply++;
749                 ### may have the rule in several chains
750                 $ipt_rules_ctr += $num_rules;
751                 if ($include_sids) {
752                     print "    Successful translation.\n";
753                 }
754             } else {
755                 if ($include_sids) {
756                     print "    Unsuccessful translation.\n";
757                 }
758             }
759             $parsed_ctr++;  ### keep track of successfully parsed rules
760             $abs_num++;;
761         }
762
763         if (($queue_mode or $nfqueue_mode) and @queue_rules) {
764             open M, "> $config{'QUEUE_RULES_DIR'}/$filename" or die "[*] Could not ",
765                 "open $config{'QUEUE_RULES_DIR'}/$filename: $!";
766             print M "#\n### This file generated with: fwsnort @argv_cp\n#\n\n";
767             print M "$_\n", for @queue_rules;
768             print M "\n### EOF ###\n";
769             close F;
770         }
771
772         if ($ipt_rules_ctr) {
773             $ipt_rules_ctr *= 2 if $ipt_drop;
774             $ipt_rules_ctr *= 2 if $ipt_reject;
775             push @ipt_script_lines,
776                 qq|\$ECHO "    Rules added: $ipt_rules_ctr"|;
777         }
778
779         unless ($include_sids) {
780             if ($ipt_sync) {
781                 printf("%-10s%-10s%-10s%-10s\n", $parsed_ctr, $unsup_ctr,
782                     $ipt_apply_ctr, $rule_num);
783             } else {
784                 printf("%-10s%-10s%-10s\n", $parsed_ctr, $unsup_ctr,
785                     $rule_num);
786             }
787         }
788     }
789     unless ($include_sids) {
790         if ($ipt_sync) {
791             printf("%30s", ' ');
792             print "=======================================\n";
793             printf("%30s%-10s%-10s%-10s%-10s\n", ' ',
794                 $abs_num, $tot_unsup_ctr, $tot_ipt_apply, $sabs_num);
795         } else {
796             printf("%30s", ' ');
797             print "=============================\n";
798             printf("%30s%-10s%-10s%-10s\n", ' ',
799                 $abs_num, $tot_unsup_ctr, $sabs_num);
800         }
801         print "\n";
802         if ($abs_num) {  ### we parsed at least one rule
803             print "[+] Generated $ipt_str rules for $abs_num out of ",
804                 "$sabs_num signatures: ",
805                 sprintf("%.2f", $abs_num/$sabs_num*100), "%\n";
806         } else {
807             print "[+] No rules parsed.\n";
808         }
809         if ($ipt_sync) {
810             print "[+] Found $tot_ipt_apply applicable snort rules to your " .
811                 "current $ipt_str\n    policy.\n";
812         }
813     }
814     return;
815 }
816
817 sub parse_rule_options() {
818     my ($rule_options, $avg_hdr_len, $line_num) = @_;
819
820     my $sid      = -1;
821     my %opts     = ();
822     my @patterns = ();
823
824     ### get the sid here for logging purposes
825     if ($rule_options =~ /$snort_opts{'logprefix'}{'sid'}/) {
826         $sid = $1;
827     } else {
828         return 0, \%opts, \@patterns;
829     }
830
831     if (%exclude_sids) {
832         return 0, \%opts, \@patterns if defined $exclude_sids{$sid};
833     }
834     if (%include_sids) {
835         if (defined $include_sids{$sid}) {
836             &logr("[+] matched sid:$sid: $rule_options");
837         } else {
838             return 0, \%opts, \@patterns;
839         }
840     }
841
842     unless ($queue_mode or $nfqueue_mode) {
843
844         ### if we're queuing packets to userspace Snort, then we don't have to
845         ### disqualify a signature based on an option that is not supported by
846         ### iptables
847         my $found_unsupported = '';
848         for my $opt (keys %{$snort_opts{'unsupported'}}) {
849             ### see if we match a regex belonging to an unsupported option
850             if ($rule_options =~ /$snort_opts{'unsupported'}{$opt}/) {
851                 $found_unsupported .= "'$opt', ";
852             }
853         }
854         if ($found_unsupported) {
855             $found_unsupported =~ s/,\s+$//;
856             &logr("[-] SID: $sid  Unsupported option(s): $found_unsupported " .
857                 "at line: $line_num, skipping.");
858             if (%include_sids and defined $include_sids{$sid}) {
859                 print "[-] SID: $sid contain the unsupported option(s): ",
860                     "$found_unsupported at line: $line_num\n";
861             }
862             return 0, \%opts, \@patterns;
863         }
864     }
865
866     if ($rule_options =~ /ip_proto\s*:.*ip_proto\s*:/) {
867         &logr("[-] SID: $sid, unsupported multiple ip_proto fields at " .
868             "line: $line_num, skipping.");
869         return 0, \%opts, \@patterns;
870     }
871
872     for my $opt (keys %{$snort_opts{'filter'}}) {
873         ### see if we match the option regex
874         if ($rule_options =~ /$snort_opts{'filter'}{$opt}{'regex'}/) {
875             $opts{$opt} = 1;
876             $opts{$opt} = $1 if defined $1;  ### some keywords may not have an option
877         }
878     }
879
880     my $found_content = 0;
881     while ($rule_options =~ /(\w+):?\s*((?:.*?[^\x5c]?))\s*;/g) {
882         my $opt = $1;
883         my $val = 1;
884         $val = $2 if defined $2;  ### some keywords may not have an argument
885
886         if ($opt eq 'content' or $opt eq 'uricontent') {
887             return 0, \%opts, \@patterns unless $val =~ /"$/;
888             $val =~ s/^\s*"//;
889             $val =~ s/"\s*$//;
890             return 0, \%opts, \@patterns unless $val =~ /\S/;
891
892             ### convert the string into a form that is more compatible
893             ### for iptables
894             my ($rv, $log_str, $ipt_pattern_hr)
895                     = &convert_pattern_for_iptables($val);
896
897             if ($rv) {
898                 $found_content = 1;
899                 push @patterns, $ipt_pattern_hr;
900             } else {
901                 &logr("[-] SID: $sid, $log_str");
902                 return 0, \%opts, \@patterns;
903             }
904
905         } elsif ($opt eq 'pcre') {
906
907             $val =~ s|^\s*"/||;
908             $val =~ s|/\w{0,3}"$||;
909
910             ### see if this pcre only has strings separated with ".*" or ".+"
911             ### and if so translate to multple string matches
912             my ($pcre_rv, $pcre_strings_ar) = &parse_pcre($val);
913             if ($pcre_rv) {
914                 for my $str (@$pcre_strings_ar) {
915                     push @patterns, $str;
916                 }
917             } else {
918                 unless ($queue_mode or $nfqueue_mode) {
919                     &logr("[-] SID: $sid, unsupported complex pcre: $val");
920                     return 0, \%opts, \@patterns;
921                 }
922             }
923
924         } elsif ($opt eq 'fast_pattern') {
925             if ($no_fast_pattern_order) {
926                 ### force it to be the first pattern so no reordering
927                 ### will happen
928                 $patterns[0]->{'fast_pattern'} = 1;
929             } else {
930                 $patterns[$#patterns]->{'fast_pattern'} = 1;
931             }
932         } elsif ($opt eq 'nocase') {
933             unless (defined $snort_opts{'ignore'}{'nocase'}) {
934                 $patterns[$#patterns]->{'nocase'} = 1;
935             }
936         } else {
937             for my $key (qw(offset depth within distance)) {
938                 if ($opt eq $key) {
939                     my ($offsets_rv, $log_str)
940                             = &define_offsets(\@patterns,
941                                 $avg_hdr_len, $key, $val);
942                     unless ($offsets_rv) {
943                         &logr("[-] SID: $sid, $log_str");
944                         return 0, \%opts, \@patterns;
945                     }
946                     last;
947                 }
948             }
949         }
950     }
951
952     if (($queue_mode or $nfqueue_mode) and not $found_content) {
953         my $queue_str = 'QUEUE';
954         $queue_str = 'NFQUEUE' if $nfqueue_mode;
955         &logr("[-] SID: $sid  In --$queue_str mode signature must have " .
956                 "'content' or 'uricontent' keyword " .
957                 "at line: $line_num, skipping.");
958         if (%include_sids and defined $include_sids{$sid}) {
959             print "[-] SID: $sid does not contain 'content' ",
960                   "or 'uricontent'\n";
961         }
962         return 0, \%opts, \@patterns;
963     }
964
965     ### update offset, depth, within, and distance values for relative
966     ### matches
967     my ($offsets_rv, $log_str) = &update_offsets_relative_matches(\@patterns);
968     unless ($offsets_rv) {
969         &logr("[-] SID: $sid, $log_str");
970         return 0, \%opts, \@patterns;
971     }
972
973     for my $opt (keys %{$snort_opts{'logprefix'}}) {
974         if ($rule_options =~ /$snort_opts{'logprefix'}{$opt}/) {
975             $opts{$opt} = $1;
976         }
977     }
978
979     unless ($queue_mode or $nfqueue_mode) {
980         while ($rule_options =~ /(\w+):\s*.*?;/g) {
981             my $option = $1;
982             if (not defined $opts{$option}
983                     and not defined $snort_opts{'ignore'}{$option}) {
984                 &logr("[-] SID: $sid bad option: \"$option\" at line: $line_num " .
985                     "-- $rule_options");
986                 return 0, \%opts, \@patterns;
987             }
988         }
989
990         if (defined $opts{'ipopts'}
991                 and $opts{'ipopts'} ne 'rr'
992                 and $opts{'ipopts'} ne 'ts'
993                 and $opts{'ipopts'} ne 'ssrr'
994                 and $opts{'ipopts'} ne 'lsrr'
995                 and $opts{'ipopts'} ne 'any') {
996             &logr("[-] SID: $sid, unsupported ipopts field at " .
997                 "line: $line_num, skipping.");
998             return 0, \%opts, \@patterns;
999         }
1000
1001         if (defined $opts{'itype'}
1002                 and ($opts{'itype'} =~ m|<| or $opts{'itype'} =~ m|>|)) {
1003             &logr("[-] SID: $sid, unsupported range operator in itype field " .
1004                 "line: $line_num, skipping.");
1005             return 0, \%opts, \@patterns;
1006         }
1007         if (defined $opts{'icode'}
1008                 and ($opts{'icode'} =~ m|<| or $opts{'icode'} =~ m|>|)) {
1009             &logr("[-] SID: $sid, unsupported range operator in icode field " .
1010                 "line: $line_num, skipping.");
1011             return 0, \%opts, \@patterns;
1012         }
1013         if (defined $opts{'ip_proto'}
1014                 and ($opts{'ip_proto'} =~ m|<| or $opts{'ip_proto'} =~ m|>|)) {
1015             &logr("[-] SID: $sid, unsupported range operator in ip_proto field " .
1016                 "line: $line_num, skipping.");
1017             return 0, \%opts, \@patterns;
1018         }
1019     }
1020
1021     ### success
1022     return 1, \%opts, \@patterns;
1023 }
1024
1025 sub parse_rule_hdr() {
1026     my ($rule_hdr, $line_num) = @_;
1027     my $bidir = 0;
1028     my $action = 'alert';  ### default
1029     if ($rule_hdr =~ /^\s*pass/) {
1030         $action = 'pass';
1031     } elsif ($rule_hdr =~ /^\s*log/) {
1032         $action = 'log';
1033     }
1034     if ($rule_hdr =~ m|^\s*\w+\s+(\S+)\s+(\S+)\s+(\S+)
1035                         \s+(\S+)\s+(\S+)\s+(\S+)|ix) {
1036         my $proto  = lc($1);
1037         my $src    = $2;
1038         my $sport  = $3;
1039         my $bidir  = $4;
1040         my $dst    = $5;
1041         my $dport  = $6;
1042
1043         unless ($proto =~ /^\w+$/) {
1044             &logr("[-] Unsupported protocol: \"$proto\" at line: " .
1045                 "$line_num, skipping.");
1046             return {};
1047         }
1048
1049         ### in --ip6tables mode make sure we're not looking at IPv4 addresses
1050         if ($enable_ip6tables
1051                 and ($src =~ /\b$ip_re\b/ or $dst =~ /\b$ip_re\b/)) {
1052             &logr("[-] --ip6tables mode enabled but IPv4 " .
1053                 "address in rule variable at line: $line_num.");
1054             return {};
1055         }
1056
1057         ### in --ip6tables mode exclude the icmp protocol - maybe should
1058         ### change to icmp6 in the future
1059         if ($enable_ip6tables and $proto eq 'icmp') {
1060             &logr("[-] --ip6tables mode enabled, so excluding " .
1061                 "icmp (non-icmp6) siganture at line: $line_num.");
1062             return {};
1063         }
1064
1065         my $bidir_flag = 0;
1066         $bidir_flag = 1 if $bidir eq '<>';
1067
1068         my %hsh = (
1069             'action' => $action,
1070             'proto'  => $proto,
1071             'src'    => $src,
1072             'sport'  => $sport,
1073             'bidir'  => $bidir_flag,
1074             'dst'    => $dst,
1075             'dport'  => $dport,
1076         );
1077
1078         ### map to expanded values (e.g. $HOME -> "any" or whatever
1079         ### is defined in fwsnort.conf)
1080         for my $var (qw(src sport dst dport)) {
1081             my $val = $hsh{$var};
1082             my $negate_flag = 0;
1083             $negate_flag = 1 if $val =~ m|!|;
1084             while ($val =~ /\$(\w+)/) {
1085                 $val = $1;
1086                 if (defined $config{$val}) {
1087                     $val = $config{$val};
1088                     if ($enable_ip6tables and $val =~ /\b$ip_re\b/) {
1089                         &logr("[-] --ip6tables mode enabled but IPv4 " .
1090                             "address in rule variable at line: $line_num.");
1091                         return {};
1092                     }
1093                 } else {
1094                     &logr("[-] Undefined variable $val in rule header " .
1095                         "at line: $line_num.");
1096                     return {};
1097                 }
1098             }
1099             if ($negate_flag and $val !~ m|!|) {
1100                 $hsh{$var} = "!$val";
1101             } else {
1102                 $hsh{$var} = $val;
1103             }
1104         }
1105
1106         for my $var (qw(sport dport)) {
1107             next unless $hsh{$var} =~ /,/;
1108             if ($ipt_have_multiport_match) {
1109                 $hsh{$var} =~ s/\[//;
1110                 $hsh{$var} =~ s/\]//;
1111                 my $ctr = 1;
1112                 my @ports = split /\s*,\s*/, $hsh{$var};
1113                 my $ports_str = '';
1114                 for my $port (@ports) {
1115                     if ($port =~ /\d+:$/) {
1116                         $ports_str .= "${port}65535,";
1117                     } else {
1118                         $ports_str .= "${port},";
1119                     }
1120                     $ctr++;
1121                     $ctr++ if $port =~ /\:/;  ### a range counts for two ports
1122                     ### multiport is limited to 15 ports
1123                     last if $ctr >= $ipt_multiport_max;
1124                 }
1125                 $ports_str =~ s/,$//;
1126                 $hsh{$var} = $ports_str;
1127             } else {
1128                 &logr("[-] Warning: taking the first port in the list " .
1129                     "$hsh{$var} until the $ipt_str multiport match is supported " .
1130                     "at line: $line_num.");
1131                 $hsh{$var} =~ s/,.*//;
1132                 $hsh{$var} =~ s/\[//;
1133                 $hsh{$var} =~ s/\]//;
1134             }
1135         }
1136
1137         return \%hsh;
1138     }
1139     return {};
1140 }
1141
1142 sub parse_pcre() {
1143     my $pcre = shift;
1144     my $rv = 0;
1145     my @patterns = ();
1146
1147     if ($pcre =~ m|^\w+$|) {
1148         push @patterns, (&convert_pattern_for_iptables($pcre))[2];
1149         $rv = 1;
1150     } elsif ($pcre =~ m|UNION\x5c\x73\x2bSELECT|) {
1151         ### a bunch of Emerging Threats rules contain "UNION\s+SELECT"
1152         ### as a PCRE.  Sure, the translation below can be evaded, but
1153         ### it is better than nothing.
1154         push @patterns, (&convert_pattern_for_iptables('UNION SELECT'))[2];
1155         $rv = 1;
1156     } else {
1157         my @ar = ();
1158         if ($pcre =~ m|\.\*|) {
1159             @ar = split /\.\*/, $pcre;
1160             $rv = 1;
1161         } elsif ($pcre =~ m|\.\+|) {
1162             @ar = split /\.\+/, $pcre;
1163             $rv = 1;
1164         } elsif ($pcre =~ m|\x5b\x5e\x5c\x6e\x5d\x2b|) {  ### [^\n]+
1165             @ar = split /\x5b\x5e\x5c\x6e\x5d\x2b/, $pcre;
1166             $rv = 1;
1167         }
1168         if ($rv == 1) {
1169             for my $part (@ar) {
1170                 next unless $part;  ### some Snort pcre's begin with .* or .+
1171                                     ### (which seems useless)
1172
1173                 ### Replace "\(" with hex equivalent in PCRE's
1174                 ### like: /.+ASCII\(.+SELECT/
1175                 $part =~ s/\x5c\x28/|5c 28|/;
1176
1177                 ### Replace "\:" with hex equivalent in PCRE's
1178                 ### like: /User-Agent\:[^\n]+spyaxe/
1179                 $part =~ s/\x5c\x3a/|5c 3a|/;
1180
1181                 my $basic = $part;
1182                 $basic =~ s/\|5c 28\|//;
1183                 $basic =~ s/\|5c 3a\|//;
1184
1185                 if ($basic =~ /^[\w\x20]+$/) {
1186                     push @patterns, (&convert_pattern_for_iptables($part))[2];
1187                 } elsif ($basic eq 'User-Agent') {
1188                     push @patterns, (&convert_pattern_for_iptables($part))[2];
1189                 } else {
1190                     $rv = 0;
1191                 }
1192             }
1193         }
1194     }
1195     return $rv, \@patterns;
1196 }
1197
1198 sub queue_get_rule() {
1199     my ($rule_hdr, $rule_opts) = @_;
1200
1201     ### FIXME: the following commented out code would need to be
1202     ### drastically improved to ensure that the remaining signatures
1203     ### are completely unique in userspace.  For now, just return
1204     ### the original Snort rule
1205     ###     Remove all of the following keywords since they are handled
1206     ###     within the kernel directly.
1207 #    for my $key qw/uricontent content offset depth within distance/ {
1208 #        $rule_opts =~ s/([\s;])$key:\s*.*?\s*;\s*/$1/g;
1209 #    }
1210
1211     $rule_opts =~ s/^\s*//;
1212     $rule_opts =~ s/\s*$//;
1213
1214     return "$rule_hdr ($rule_opts)";
1215 }
1216
1217 sub ipt_allow_traffic() {
1218     my ($hdr_hr, $opts_hr, $chain, $orig_snort_rule) = @_;
1219
1220     my $rule_ctr = 0;
1221
1222     if ($dump_snort) {
1223         print "\n[+] Snort rule: $orig_snort_rule"
1224                 unless defined $snort_dump_cache{$orig_snort_rule};
1225         $snort_dump_cache{$orig_snort_rule} = '';
1226     }
1227
1228     ### check to see if the header is allowed through the chain,
1229     ### and if not we don't really care about matching traffic
1230     ### because iptables doesn't allow it anyway
1231     RULE: for my $rule_hr (@{$ipt_policy{$chain}}) {
1232         $rule_ctr++;
1233
1234         if ($dumper and $verbose) {
1235             print "[+] RULE: $rule_ctr:\n",
1236                 Dumper($rule_hr);
1237         }
1238         if ($dump_ipt) {
1239             print "[+] $ipt_str rule: $rule_hr->{'raw'}\n"
1240                 unless defined $ipt_dump_cache{$rule_hr->{'raw'}};
1241             $ipt_dump_cache{$rule_hr->{'raw'}} = '';
1242         }
1243
1244         ### don't match on rules to/from the loopback interface
1245         unless ($no_exclude_loopback) {
1246             if ($rule_hr->{'intf_in'} eq 'lo'
1247                     or $rule_hr->{'intf_out'} eq 'lo') {
1248                 print "[-] Skipping $chain rule $rule_ctr: loopback rule\n"
1249                     if $debug;
1250                 next RULE;
1251             }
1252         }
1253
1254         ### don't match on rules that build state
1255         if ($rule_hr->{'extended'} =~ /state/) {
1256             print "[-] Skipping $chain rule $rule_ctr: state rule\n"
1257                 if $debug;
1258             next RULE;
1259         }
1260
1261         ### match protocol
1262         unless (($hdr_hr->{'proto'} eq $rule_hr->{'proto'}
1263                 or $rule_hr->{'proto'} eq 'all')) {
1264             print "[-] Skipping $chain rule $rule_ctr: $hdr_hr->{'proto'} ",
1265                 "!= $rule_hr->{'proto'}\n" if $debug;
1266             next RULE;
1267         }
1268
1269         ### match src/dst IP/network
1270         unless (&match_addr($hdr_hr->{'src'}, $rule_hr->{'src'})) {
1271             print "[-] Skipping $chain rule $rule_ctr: src $hdr_hr->{'src'} ",
1272                 "not part of $rule_hr->{'src'}\n" if $debug;
1273             next RULE;
1274         }
1275         unless (&match_addr($hdr_hr->{'dst'}, $rule_hr->{'dst'})) {
1276             print "[-] Skipping $chain rule $rule_ctr: dst $hdr_hr->{'dst'} ",
1277                 "not part of $rule_hr->{'dst'}\n" if $debug;
1278             next RULE;
1279         }
1280
1281         ### match src/dst ports
1282         if ($hdr_hr->{'proto'} ne 'icmp') {
1283             unless (&match_port($hdr_hr->{'sport'},
1284                     $rule_hr->{'sport'})) {
1285                 print "[-] Skipping $chain rule $rule_ctr: sport ",
1286                     "$hdr_hr->{'sport'} not part of $rule_hr->{'sport'}\n"
1287                     if $debug;
1288                 next RULE;
1289             }
1290             unless (&match_port($hdr_hr->{'dport'},
1291                     $rule_hr->{'dport'})) {
1292                 print "[-] Skipping $chain rule $rule_ctr: dport ",
1293                     "$hdr_hr->{'dport'} not part of $rule_hr->{'dport'}\n"
1294                     if $debug;
1295                 next RULE;
1296             }
1297         }
1298
1299         if (defined $opts_hr->{'flow'} and $rule_hr->{'state'}) {
1300             if ($opts_hr->{'flow'} eq 'established') {
1301                 unless ($rule_hr->{'state'} =~ /ESTABLISHED/) {
1302                     print "[-] Skipping $chain rule $rule_ctr: state ",
1303                         "$opts_hr->{'flow'} not part of $rule_hr->{'state'}\n"
1304                         if $debug;
1305                     next RULE;
1306                 }
1307             }
1308         }
1309
1310         ### if we make it here, then this rule matches the signature
1311         ### (from a header perspective)
1312         if ($rule_hr->{'target'} eq 'DROP'
1313                 or $rule_hr->{'target'} eq 'REJECT') {
1314
1315             print "[-] Matching $ipt_str rule has DROP or REJECT target; ",
1316                 "$ipt_str policy does not allow this Snort rule.\n"
1317                 if $debug;
1318             if ($dumper) {
1319                 print "\n[-] RULE $chain DROP:\n",
1320                     Dumper($hdr_hr),
1321                     Dumper($opts_hr),
1322                     Dumper($rule_hr),
1323                     "\n";
1324             }
1325             return 0;
1326         } elsif ($rule_hr->{'target'} eq 'ACCEPT') {
1327             if ($dumper) {
1328                 print "\n[+] RULE $chain ACCEPT:\n",
1329                     Dumper($hdr_hr),
1330                     Dumper($opts_hr),
1331                     Dumper($rule_hr),
1332                     "\n";
1333             }
1334             print "[-] Matching $ipt_str rule has ACCEPT target; ",
1335                 "$ipt_str policy allows this Snort rule.\n" if $debug;
1336             return 1;
1337         }  ### we don't support other targets besides DROP, REJECT,
1338            ### or ACCEPT for now.
1339     }
1340
1341     ### if we make it here, then no specific ACCEPT rule matched the header,
1342     ### so return false if the chain policy is set to DROP (or there is
1343     ### a default drop rule). Otherwise there is no rule that would block
1344     ### the traffic.
1345     if (defined $ipt_default_policy_setting{$chain}) {
1346         if ($ipt_default_policy_setting{$chain} eq 'ACCEPT') {
1347             if (defined $ipt_default_drop{$chain}) {
1348                 if (defined $ipt_default_drop{$chain}{'all'}) {
1349                     print "[-] Default DROP rule applies to this Snort rule.\n"
1350                         if $debug;
1351                     return 0;
1352                 } elsif (defined $ipt_default_drop{$chain}
1353                         {$hdr_hr->{'proto'}}) {
1354                     print "[-] Default DROP rule applies to this Snort rule.\n"
1355                         if $debug;
1356                     return 0;
1357                 }
1358             }
1359             if ($dumper) {
1360                 print "\nACCEPT $chain, no $ipt_str matching rule\n",
1361                     Dumper($hdr_hr),
1362                     Dumper($opts_hr),
1363                     "\n";
1364             }
1365             return 1;
1366         }
1367     }
1368     if ($dumper) {
1369         print "\nDROP $chain, no $ipt_str matching rule\n",
1370             Dumper($hdr_hr),
1371             Dumper($opts_hr),
1372             "\n";
1373     }
1374
1375     ### maybe a "strict" option should be added here?
1376     return 0;
1377 }
1378
1379 sub match_addr() {
1380     my ($hdr_src, $rule_src) = @_;
1381     return 1 if $rule_src eq '0.0.0.0/0';
1382     return 1 if $hdr_src =~ /any/i;
1383     return 1 if $hdr_src eq $rule_src;
1384
1385     my $ipt_ip   = '';
1386     my $ipt_mask = '32';
1387     my $negate = 0;
1388
1389     my $s_obj   = '';
1390     my $ipt_obj = '';
1391
1392     $negate = 1 if $hdr_src =~ /\!/;
1393
1394     if ($rule_src =~ /\!/) {
1395         if ($negate) {
1396             ### if both hdr_src and rule_src are negated
1397             ### then revert to normal match.
1398             $negate = 0;
1399         } else {
1400             $negate = 1;
1401         }
1402     }
1403
1404     if ($rule_src =~ m|($ip_re)/($ip_re)|) {
1405         $ipt_ip   = $1;
1406         $ipt_mask = $2;
1407     } elsif ($rule_src =~ m|($ip_re)/(\d+)|) {
1408         $ipt_ip   = $1;
1409         $ipt_mask = $2;
1410     } elsif ($rule_src =~ m|($ip_re)|) {
1411         $ipt_ip = $1;
1412     }
1413
1414     $ipt_obj = new NetAddr::IP($ipt_ip, $ipt_mask);
1415
1416     for my $addr (@{&expand_addresses($hdr_src)}) {
1417         my $src_ip   = '';
1418         my $src_mask = '32';
1419         if ($addr =~ m|($ip_re)/($ip_re)|) {
1420             $src_ip   = $1;
1421             $src_mask = $2;
1422         } elsif ($addr =~ m|($ip_re)/(\d+)|) {
1423             $src_ip   = $1;
1424             $src_mask = $2;
1425         } elsif ($addr =~ m|($ip_re)|) {
1426             $src_ip = $1;
1427         }
1428         $s_obj = new NetAddr::IP($src_ip, $src_mask);
1429         if ($negate) {
1430             return 1 unless $ipt_obj->within($s_obj);
1431         } else {
1432             return 1 if $ipt_obj->within($s_obj);
1433         }
1434     }
1435     return 0;
1436 }
1437
1438 sub match_port() {
1439     my ($snort_port, $ipt_port) = @_;
1440     return 1 if $ipt_port eq '0:0';
1441     return 1 if $snort_port =~ /any/i;
1442     return 1 if $ipt_port eq $snort_port;
1443     my $ipt_start = 0;
1444     my $ipt_end   = 65535;
1445     my $h_start   = 0;
1446     my $h_end     = 65535;
1447
1448     if ($ipt_port =~ /:/) {
1449         if ($ipt_port =~ /(\d+):/) {
1450             $ipt_start = $1;
1451         }
1452         if ($ipt_port =~ /:(\d+)/) {
1453             $ipt_end = $1;
1454         }
1455     } elsif ($ipt_port =~ /(\d+)/) {
1456         $ipt_start = $ipt_end = $1;
1457     }
1458
1459     if ($snort_port =~ /:/) {
1460         if ($snort_port =~ /(\d+):/) {
1461             $h_start = $1;
1462         }
1463         if ($snort_port =~ /:(\d+)/) {
1464             $h_end = $1;
1465         }
1466     } elsif ($snort_port =~ /(\d+)/) {
1467         $h_start = $h_end = $1;
1468     }
1469
1470     if ($ipt_port =~ /!/) {
1471         if ($snort_port =~ /!/) {
1472             return 0;
1473         } else {
1474             return 1 if (($h_start < $ipt_start and $h_end < $ipt_start)
1475                     or ($h_start > $ipt_end and $h_end > $ipt_end));
1476         }
1477     } else {
1478         if ($snort_port =~ /!/) {
1479             return 1 if (($ipt_start < $h_start and $ipt_end < $h_start)
1480                     or ($ipt_start > $h_end and $ipt_end > $h_end));
1481         } else {
1482             return 1 if $h_start >= $ipt_start and $h_end <= $ipt_end;
1483         }
1484     }
1485     return 0;
1486 }
1487
1488 sub cache_ipt_policy() {
1489
1490     my $ipt = new IPTables::Parse 'iptables' => $cmds{'iptables'}
1491         or die "[*] Could not acquire IPTables::Parse object: $!";
1492
1493     for my $chain (keys %process_chains) {
1494         next unless $process_chains{$chain};
1495
1496         $ipt_policy{$chain} = $ipt->chain_rules('filter',
1497             $chain, $ipt_file);
1498
1499         $ipt_default_policy_setting{$chain}
1500             = $ipt->chain_policy('filter', $chain, $ipt_file);
1501
1502         my ($def_drop_hr, $ipt_rv)
1503             = $ipt->default_drop('filter', $chain, $ipt_file);
1504
1505         if ($ipt_rv) {
1506             $ipt_default_drop{$chain} = $def_drop_hr;
1507         }
1508     }
1509     return;
1510 }
1511
1512 sub ipt_build() {
1513     my ($snort_hdr_hr, $snort_opts_hr, $patterns_ar, $orig_snort_rule) = @_;
1514
1515     my $found_rule = 0;
1516     my $num_rules  = 0;
1517
1518     my %process_rules = ();
1519
1520     ### define iptables source and destination
1521     if ($snort_hdr_hr->{'dst'} =~ /any/i) {
1522         if ($snort_hdr_hr->{'src'} =~ /any/i) {
1523             push @{$process_rules{'INPUT'}}, '' if $process_chains{'INPUT'};
1524             push @{$process_rules{'FORWARD'}}, ''
1525                 if $process_chains{'FORWARD'};
1526         } else {
1527             my $addr_ar = &expand_addresses($snort_hdr_hr->{'src'});
1528             my $negate = '';
1529             $negate = '! ' if $snort_hdr_hr->{'src'} =~ m|!|;
1530             unless ($addr_ar) {
1531                 &logr("[-] No valid source IPs/networks in Snort " .
1532                     "rule header.");
1533                 return 0, 0;
1534             }
1535             for my $src (@$addr_ar) {
1536                 if (&is_local($src)) {
1537                     push @{$process_rules{'OUTPUT'}},
1538                             "$negate$ipt_hdr_opts{'src'} ${src}"
1539                             if $process_chains{'OUTPUT'};
1540                 } else {
1541                     push @{$process_rules{'INPUT'}},
1542                             "$negate$ipt_hdr_opts{'src'} ${src}"
1543                             if $process_chains{'INPUT'};
1544                 }
1545                 push @{$process_rules{'FORWARD'}},
1546                         "$negate$ipt_hdr_opts{'src'} ${src}"
1547                         if $process_chains{'FORWARD'};
1548             }
1549         }
1550     } else {
1551         my $dst_addr_ar = &expand_addresses($snort_hdr_hr->{'dst'});
1552         unless ($dst_addr_ar) {
1553             &logr("[-] No valid destination IPs/networks in Snort rule " .
1554                 "header.");
1555             return 0, 0;
1556         }
1557         if ($snort_hdr_hr->{'src'} =~ /any/i) {
1558             my $negate = '';
1559             $negate = '! ' if $snort_hdr_hr->{'dst'} =~ m|!|;
1560             for my $dst (@$dst_addr_ar) {
1561                 if (&is_local($dst)) {
1562                     push @{$process_rules{'INPUT'}},
1563                             "$negate$ipt_hdr_opts{'dst'} ${dst}"
1564                             if $process_chains{'INPUT'};
1565                 } else {
1566                     push @{$process_rules{'OUTPUT'}},
1567                             "$negate$ipt_hdr_opts{'dst'} ${dst}"
1568                             if $process_chains{'OUTPUT'};
1569                 }
1570                 push @{$process_rules{'FORWARD'}},
1571                         "$negate$ipt_hdr_opts{'dst'} ${dst}"
1572                         if $process_chains{'FORWARD'};
1573             }
1574         } else {
1575             my $src_addr_ar = &expand_addresses($snort_hdr_hr->{'src'});
1576             my $negate_src = '';
1577             $negate_src = '! ' if $snort_hdr_hr->{'src'} =~ m|!|;
1578             my $negate_dst = '';
1579             $negate_dst = '! ' if $snort_hdr_hr->{'dst'} =~ m|!|;
1580             unless ($src_addr_ar) {
1581                 &logr("[-] No valid source IPs/networks in Snort rule " .
1582                     "header.");
1583                 return 0, 0;
1584             }
1585             for my $src (@$src_addr_ar) {
1586                 for my $dst (@$dst_addr_ar) {
1587                     if (&is_local($dst)) {
1588                         push @{$process_rules{'INPUT'}},
1589                             "$negate_src$ipt_hdr_opts{'src'} ${src}" .
1590                             " $negate_dst$ipt_hdr_opts{'dst'} ${dst}"
1591                             if $process_chains{'INPUT'};
1592                     } else {
1593                         push @{$process_rules{'OUTPUT'}},
1594                             "$negate_src$ipt_hdr_opts{'src'} ${src}" .
1595                             " $negate_dst$ipt_hdr_opts{'dst'} ${dst}"
1596                             if $process_chains{'OUTPUT'};
1597                     }
1598                     push @{$process_rules{'FORWARD'}},
1599                         "$negate_src$ipt_hdr_opts{'src'} ${src}" .
1600                         " $negate_dst$ipt_hdr_opts{'dst'} ${dst}"
1601                         if $process_chains{'FORWARD'};
1602                 }
1603             }
1604         }
1605     }
1606
1607     ### determine which chain (e.g. stateful/stateless)
1608     my $flow_established = '';
1609     unless ($no_ipt_conntrack) {
1610         if (defined $snort_hdr_hr->{'proto'}
1611                 and $snort_hdr_hr->{'proto'} =~ /tcp/i
1612                 and defined $snort_opts_hr->{'flow'}
1613                 and $snort_opts_hr->{'flow'} =~ /established/i) {
1614             $flow_established = 'ESTABLISHED';
1615         }
1616     }
1617
1618     my $add_snort_comment = 1;
1619     my $add_perl_trigger  = 1;
1620     for my $chain (keys %process_chains) {
1621
1622         next unless $process_chains{$chain} and $process_rules{$chain};
1623
1624         for my $src_dst (@{$process_rules{$chain}}) {
1625
1626             my $rule = "\$${ipt_var_str} -A ";
1627             my $save_rule = '-A';
1628             my $fwsnort_chain = '';
1629
1630             ### see if we can jump to the ESTABLISHED inspection chain.
1631             if ($flow_established) {
1632                 $rule .= $config{"FWSNORT_${chain}_ESTAB"};
1633                 $fwsnort_chain = $config{"FWSNORT_${chain}_ESTAB"};
1634             } else {
1635                 $rule .= $config{"FWSNORT_$chain"};
1636                 $fwsnort_chain = $config{"FWSNORT_$chain"};
1637             }
1638
1639             ### append interface restriction if necessary
1640             if ($src_dst =~ m|127\.0\.0\.\d/|) {
1641                 if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
1642                     $rule .= ' ! -i lo';
1643                 } elsif ($chain eq 'OUTPUT') {
1644                     $rule .= ' ! -o lo';
1645                 }
1646             }
1647
1648             ### append source and destination criteria
1649             if ($src_dst) {
1650                 if ($chain eq 'FORWARD') {
1651                     $rule .= " $src_dst";
1652                 } elsif ($chain eq 'INPUT') {
1653                     ### we always treat the INPUT chain as part of the HOME_NET;
1654                     ### the system running iptables may have an interface on the
1655                     ### external network and hence may not be part of the HOME_NET
1656                     ### as defined in the fwsnort.conf file so we don't necessarily
1657                     ### append the IP criteria
1658                     if ($src_dst ne "$ipt_hdr_opts{'dst'} $config{'HOME_NET'}") {
1659                         $rule .= " $src_dst";
1660                     }
1661                 } elsif ($chain eq 'OUTPUT') {
1662                     if ($src_dst ne "$ipt_hdr_opts{'src'} $config{'HOME_NET'}") {
1663                         $rule .= " $src_dst";
1664                     }
1665                 }
1666             }
1667
1668             my $rv = &ipt_build_rule(
1669                 $chain,
1670                 $fwsnort_chain,
1671                 $rule,
1672                 $snort_hdr_hr,
1673                 $snort_opts_hr,
1674                 $patterns_ar,
1675                 $orig_snort_rule,
1676                 $flow_established,
1677                 $add_snort_comment,
1678                 $add_perl_trigger
1679             );
1680             if ($rv) {
1681                 $found_rule        = 1;
1682                 $add_snort_comment = 0;
1683                 $add_perl_trigger  = 0;
1684                 $num_rules++;
1685             }
1686         }
1687     }
1688     return $found_rule, $num_rules;
1689 }
1690
1691 sub is_local() {
1692     my $addr = shift;
1693
1694     return 1 if $no_addr_check;
1695
1696     my $ip   = '';
1697     my $mask = '32';
1698
1699     if ($addr =~ m|($ip_re)/($ip_re)|) {
1700         $ip   = $1;
1701         $mask = $2;
1702     } elsif ($addr =~ m|($ip_re)/(\d+)|) {
1703         $ip   = $1;
1704         $mask = $2;
1705     } elsif ($addr =~ m|($ip_re)|) {
1706         $ip = $1;
1707     }
1708
1709     my $ip_obj = new NetAddr::IP($ip, $mask);
1710
1711     for my $local_ar (@local_addrs) {
1712         my $local_ip   = $local_ar->[0];
1713         my $local_mask = $local_ar->[1];
1714
1715         my $local_obj = new NetAddr::IP($local_ip, $local_mask);
1716
1717         return 1 if $ip_obj->within($local_obj);
1718     }
1719     return 0;
1720 }
1721
1722 sub get_local_addrs() {
1723     open IFC, "$cmds{'ifconfig'} -a |" or die "[*] Could not run ",
1724         "$cmds{'ifconfig'}: $!";
1725     my @lines = <IFC>;
1726     close IFC;
1727
1728     my $intf_name = '';
1729     for my $line (@lines) {
1730         if ($line =~ /^(\w+)\s+Link/) {
1731             $intf_name = $1;
1732             next;
1733         }
1734         next if $intf_name eq 'lo';
1735         next if $intf_name =~ /dummy/i;
1736         if ($line =~ /^\s+inet.*?:($ip_re).*:($ip_re)/i) {
1737             push @local_addrs, [$1, $2];
1738         }
1739     }
1740     return;
1741 }
1742
1743 sub ipt_build_rule() {
1744     my ($chain, $fwsnort_chain, $rule, $hdr_hr, $opts_hr, $patterns_ar,
1745             $orig_snort_rule, $flow_logging_prefix, $add_snort_comment,
1746             $add_perl_trigger) = @_;
1747
1748     ### $chain is used only to see whether or not we need to add the
1749     ### rule to the iptables script based on whether the built-in chain
1750     ### will pass the traffic in the first place.
1751     if ($ipt_sync) {
1752         return 0 unless &ipt_allow_traffic($hdr_hr,
1753                 $opts_hr, $chain, $orig_snort_rule);
1754     }
1755
1756     ### append the protocol to the rule
1757     if (defined $opts_hr->{'ip_proto'}) {
1758         return 0 unless $opts_hr->{'ip_proto'} =~ /^\w+$/;
1759         $rule .= " $snort_opts{'filter'}{'ip_proto'}{'iptopt'} " .
1760             "$opts_hr->{'ip_proto'}";
1761     } else {
1762         return 0 unless $hdr_hr->{'proto'} =~ /^\w+$/;
1763         if ((($hdr_hr->{'sport'} !~ /any/i and $hdr_hr->{'sport'} ne '')
1764                 or ($hdr_hr->{'dport'} !~ /any/i
1765                 and $hdr_hr->{'dport'} ne ''))
1766                 and $hdr_hr->{'proto'} !~ /tcp/i
1767                 and $hdr_hr->{'proto'} !~ /udp/i) {
1768             ### force to tcp because iptables does not like src/dst
1769             ### ports with anything other than tcp or udp
1770             $hdr_hr->{'proto'} = 'tcp';
1771         }
1772
1773         if ($hdr_hr->{'proto'} =~ /ip/) {
1774             $rule .= " $ipt_hdr_opts{'proto'} $hdr_hr->{'proto'}";
1775         } else {
1776             $rule .= " $ipt_hdr_opts{'proto'} $hdr_hr->{'proto'} " .
1777                 "-m $hdr_hr->{'proto'}";
1778         }
1779     }
1780
1781     ### append the source and destination ports
1782     for my $type (qw(sport dport)) {
1783         if (defined $hdr_hr->{$type} and $hdr_hr->{$type} !~ /any/i) {
1784             my $negate = '';
1785             my $port = $hdr_hr->{$type};
1786             $negate = '! ' if $port =~ m|!|;
1787             $port =~ s/\!\s*(\d)/$1/;
1788             if ($port =~ /,/) {
1789                 ### multiport match
1790                 $rule .= " -m multiport ${negate}--${type}s $port";
1791             } else {
1792                 $rule .= " ${negate}$ipt_hdr_opts{$type} $port";
1793             }
1794         }
1795     }
1796
1797     my $rv = &ipt_build_opts($rule, $hdr_hr, $opts_hr, $patterns_ar,
1798         $orig_snort_rule, $flow_logging_prefix, $chain, $fwsnort_chain,
1799         $add_snort_comment, $add_perl_trigger);
1800
1801     return $rv;
1802 }
1803
1804 sub ipt_build_opts() {
1805     my ($rule, $hdr_hr, $opts_hr, $patterns_ar, $orig_snort_rule,
1806             $flow_logging_prefix, $chain, $fwsnort_chain, $add_snort_comment,
1807             $add_perl_trigger) = @_;
1808
1809     ### append tcp flags
1810     if (defined $opts_hr->{'flags'}) {
1811         my $f_str = '';
1812
1813         $f_str .= 'URG,' if $opts_hr->{'flags'} =~ /U/i;
1814         $f_str .= 'ACK,' if $opts_hr->{'flags'} =~ /A/i;
1815         $f_str .= 'PSH,' if $opts_hr->{'flags'} =~ /P/i;
1816         $f_str .= 'RST,' if $opts_hr->{'flags'} =~ /R/i;
1817         $f_str .= 'SYN,' if $opts_hr->{'flags'} =~ /S/i;
1818         $f_str .= 'FIN,' if $opts_hr->{'flags'} =~ /F/i;
1819         $f_str =~ s/\,$//;
1820
1821         if ($opts_hr->{'flags'} =~ /\+/) {
1822             ### --tcp-flags ACK ACK
1823             $rule .= " $snort_opts{'filter'}{'flags'}{'iptopt'} " .
1824                 "$f_str $f_str";
1825         } else {
1826             ### --tcp-flags ALL URG,PSH,SYN,FIN
1827             $rule .= " $snort_opts{'filter'}{'flags'}{'iptopt'} " .
1828                 "ALL $f_str";
1829         }
1830     }
1831
1832     if ($no_ipt_conntrack) {
1833         ### fall back to appending --tcp-flags ACK ACK if flow=established.
1834         ### NOTE: we can't really handle "flow" in the same way snort can,
1835         ### since there is no way to keep track of which side initiated the
1836         ### tcp session (where the SYN packet came from), but older versions
1837         ### of snort (pre 1.9) just used tcp flags "A+" to keep track of
1838         ### this... we need to do the same.
1839         if (defined $opts_hr->{'flow'} && ! defined $opts_hr->{'flags'}) {
1840             if ($opts_hr->{'flow'} =~ /established/i) {
1841                 ### note that this ignores the "stateless" keyword
1842                 ### as it should...
1843                 $rule .= " $snort_opts{'filter'}{'flow'}{'iptopt'} ACK ACK";
1844             }
1845         }
1846     }
1847
1848     ### append icmp type
1849     if ($hdr_hr->{'proto'} =~ /icmp/i) {
1850         if (defined $opts_hr->{'itype'}) {
1851             $rule .= " $snort_opts{'filter'}{'itype'}{'iptopt'} " .
1852                 "$opts_hr->{'itype'}";
1853             ### append icmp code (becomes "--icmp-type type/code")
1854             if (defined $opts_hr->{'icode'}) {
1855                 $rule .= "/$opts_hr->{'icode'}";
1856             }
1857         } else {
1858             ### append the default icmp type since some recent versions of
1859             ### iptables (such as 1.4.12 on Fedora 16) require it - an error
1860             ### like the following will be thrown if it's not there:
1861             ### iptables-restore v1.4.12: icmp: option "--icmp-type" must be specified
1862             $rule .= " $snort_opts{'filter'}{'itype'}{'iptopt'} " .
1863                 $default_icmp_type;
1864         }
1865     }
1866
1867     ### append ip options
1868     if (defined $opts_hr->{'ipopts'}) {
1869         $rule .= " $snort_opts{'filter'}{'ipopts'}{'iptopt'} " .
1870             "--$opts_hr->{'ipopts'}"
1871     }
1872
1873     ### append tos (requires CONFIG_IP_NF_MATCH_TOS)
1874     if (defined $opts_hr->{'tos'}) {
1875         $rule .= " $snort_opts{'filter'}{'tos'}{'iptopt'} " .
1876             "$opts_hr->{'tos'}"
1877     }
1878
1879
1880     ### append ttl (requires CONFIG_IP_NF_MATCH_TTL)
1881     if (defined $opts_hr->{'ttl'}) {
1882         if ($opts_hr->{'ttl'} =~ /\<\s*(\d+)/) {
1883             $rule .= " $snort_opts{'filter'}{'ttl'}{'iptopt'} --ttl-lt $1";
1884         } elsif ($opts_hr->{'ttl'} =~ /\>\s*(\d+)/) {
1885             $rule .= " $snort_opts{'filter'}{'ttl'}{'iptopt'} --ttl-gt $1";
1886         } else {
1887             $rule .= " $snort_opts{'filter'}{'ttl'}{'iptopt'} " .
1888                 "--ttl-eq $opts_hr->{'ttl'}";
1889         }
1890     }
1891
1892     my $avg_hdr_len = &get_avg_hdr_len($hdr_hr);
1893
1894     ### append dsize option (requires CONFIG_IP_NF_MATCH_LENGTH)
1895     if (defined $opts_hr->{'dsize'}) {
1896         ### get the average packet header size based on the protocol
1897         ### (the iptables length match applies to the network header
1898         ### and up).
1899         if ($opts_hr->{'dsize'} =~ m|(\d+)\s*<>\s*(\d+)|) {
1900             my $iptables_len1 = $1 + $avg_hdr_len;
1901             my $iptables_len2 = $2 + $avg_hdr_len;
1902             $rule .= " $snort_opts{'filter'}{'dsize'}{'iptopt'} " .
1903                 "$iptables_len1:$iptables_len2";
1904         } elsif ($opts_hr->{'dsize'} =~ m|<\s*(\d+)|) {
1905             my $iptables_len = $1 + $avg_hdr_len;
1906             $rule .= " $snort_opts{'filter'}{'dsize'}{'iptopt'} " .
1907                 "$avg_hdr_len:$iptables_len";
1908         } elsif ($opts_hr->{'dsize'} =~ m|>\s*(\d+)|) {
1909             my $iptables_len = $1 + $avg_hdr_len;
1910             if ($iptables_len < $config{'MAX_FRAME_LEN'} + $avg_hdr_len) {
1911                 $rule .= " $snort_opts{'filter'}{'dsize'}{'iptopt'} " .
1912                     "$iptables_len:" .
1913                     ($config{'MAX_FRAME_LEN'} + $avg_hdr_len);
1914             }
1915         } elsif ($opts_hr->{'dsize'} =~ m|(\d+)|) {
1916             my $iptables_len = $1 + $avg_hdr_len;
1917             $rule .= " $snort_opts{'filter'}{'dsize'}{'iptopt'} " .
1918                 $iptables_len;
1919         }
1920     }
1921
1922     ### append snort content options
1923     my $ipt_content_criteria = 0;
1924     my $perl_trigger_str = '';
1925
1926     if ($#$patterns_ar > -1) {
1927         ($ipt_content_criteria, $perl_trigger_str)
1928             = &build_content_matches($opts_hr, $patterns_ar);
1929
1930         return 0 unless $ipt_content_criteria;
1931         $rule .= $ipt_content_criteria;
1932     }
1933
1934     ### print the rest of the logprefix snort options in a comment
1935     ### one line above the rule
1936     my $comment    = '';
1937     my $target_str = '';
1938     for my $key (qw(sid msg classtype reference priority rev)) {
1939         if (defined $opts_hr->{$key}) {
1940             $comment .= qq|$key:$opts_hr->{$key}; |;
1941         }
1942     }
1943     $comment =~ s/\s*$//;
1944     $comment =~ s/,$//;
1945
1946     ### append the fwsnort version as "FWS:$version"
1947     $comment .= " FWS:$version;";
1948
1949     ### build up the logging prefix and comment match
1950     if (defined $opts_hr->{'sid'}) {
1951         unless ($no_ipt_comments) {
1952             ### add the Snort msg (and other) fields to the iptables rule
1953             ### with the 'comment' match (which can handle up to 255 chars
1954             ### and is set/verified by the ipt_find_max_comment_len()
1955             ### function).
1956             $comment =~ s|\"||g;
1957             $comment =~ s|/\*||g;
1958             $comment =~ s|\*/||g;
1959             if (length($comment) < $ipt_max_comment_len) {
1960                 $target_str = qq| -m comment --comment "$comment"|;
1961             }
1962         }
1963         ### increment chain counter and add in if necessary
1964         $chain_ctr{$chain}++;
1965
1966         if ($queue_mode or $nfqueue_mode) {
1967             if ($queue_mode) {
1968                 $target_str .= qq| -j QUEUE|;
1969             } else {
1970                 $target_str .= qq| -j NFQUEUE|;
1971                 if ($nfqueue_num) {
1972                     $target_str .= " --queue-num $nfqueue_num";
1973                 }
1974             }
1975         } else {
1976             if ($hdr_hr->{'action'} eq 'log' or $ulog_mode) {
1977                 $target_str .= qq| -j ULOG --ulog-nlgroup $ulog_nlgroup --ulog-prefix "|;
1978             } else {
1979                 $target_str .= ' -j LOG ';
1980                 $target_str .= '--log-ip-options ' unless $no_ipt_log_ip_opts;
1981                 if ($hdr_hr->{'proto'} eq 'tcp') {
1982                     $target_str .= '--log-tcp-options ' unless $no_ipt_log_tcp_opts;
1983                     $target_str .= '--log-tcp-sequence ' if $ipt_log_tcp_seq;
1984                 }
1985                 $target_str .= qq|--log-prefix |;
1986             }
1987
1988
1989             ### build up the LOG prefix string
1990             my $prefix_str = '';
1991
1992             unless ($no_ipt_rule_nums) {
1993                 $prefix_str .= "[$chain_ctr{$chain}] ";
1994             }
1995
1996             if ($ipt_drop) {
1997                 $prefix_str .= 'DRP ';
1998             } elsif ($ipt_reject) {
1999                 $prefix_str .= 'REJ ';
2000             }
2001
2002             ### always add the sid
2003             $prefix_str .= qq|SID$opts_hr->{'sid'} |;
2004             if ($flow_logging_prefix) {
2005                 $prefix_str .= 'ESTAB ';
2006             }
2007
2008             if (length($prefix_str) >= $ipt_max_log_prefix_len) {
2009                 $prefix_str = qq|SID$opts_hr->{'sid'} |;
2010                 if (length($prefix_str) >= $ipt_max_log_prefix_len) {
2011                     return 0 unless $ipt_content_criteria;
2012                 }
2013             }
2014
2015             $target_str .= qq|"| . $prefix_str . qq|"|;
2016         }
2017     }
2018
2019     ### print the snort rules type header to the fwsnort.sh script
2020     unless ($ipt_print_type) {
2021         &ipt_type($snort_type);
2022         $ipt_print_type = 1;
2023     }
2024
2025     ### write the rule out to the iptables script
2026     &ipt_add_rule($hdr_hr, $opts_hr, $orig_snort_rule,
2027         $rule, $target_str, "### $comment", $add_snort_comment,
2028         $perl_trigger_str, $add_perl_trigger, $chain, $fwsnort_chain);
2029     return 1;
2030 }
2031
2032 sub build_content_matches() {
2033     my ($opts_hr, $patterns_ar) = @_;
2034
2035     my $fast_pattern_index   = 0;
2036     my $fast_pattern_is_set  = 0;
2037     my $ipt_content_criteria = '';
2038     my $perl_trigger_command = '';
2039     my @content_fast_pattern_order = ();
2040
2041     $perl_trigger_command = q|perl -e 'print "| if $include_perl_triggers;
2042
2043     if ($no_fast_pattern_order) {
2044         $patterns_ar->[0]->{'fast_pattern'} = 1;
2045     }
2046
2047     for (my $index=0; $index <= $#$patterns_ar; $index++) {
2048         if ($patterns_ar->[$index]->{'fast_pattern'}) {
2049             $fast_pattern_index  = $index;
2050             $fast_pattern_is_set = 1;
2051             last;
2052         }
2053     }
2054
2055     unless ($fast_pattern_is_set) {
2056         ### in this case, the 'fast_pattern' option was not used, so pick the
2057         ### longest pattern to match first (this should help with performance
2058         ### of signature matches on average)
2059         my $max_len = 0;
2060         my $max_len_index = 0;
2061         PATTERN: for (my $index=0; $index <= $#$patterns_ar; $index++) {
2062             my $pat_ar = $patterns_ar->[$index];
2063
2064             if ($pat_ar->{'length'} > $max_len) {
2065
2066                 ### make sure it is not a relative match
2067                 next PATTERN if defined $pat_ar->{'distance'};
2068                 next PATTERN if defined $pat_ar->{'within'};
2069
2070                 if ($index < $#$patterns_ar) {
2071                     my $next_pat_ar = $patterns_ar->[$index+1];
2072                     next PATTERN if defined $next_pat_ar->{'distance'};
2073                     next PATTERN if defined $next_pat_ar->{'within'};
2074                 }
2075
2076                 $max_len = $pat_ar->{'length'};
2077                 $max_len_index = $index;
2078             }
2079         }
2080         $fast_pattern_index = $max_len_index;
2081     }
2082
2083     $content_fast_pattern_order[0] = $patterns_ar->[$fast_pattern_index];
2084     for (my $i=0; $i <= $#$patterns_ar; $i++) {
2085         next if $i == $fast_pattern_index;
2086         push @content_fast_pattern_order, $patterns_ar->[$i];
2087     }
2088
2089     for (my $i=0; $i <= $#content_fast_pattern_order; $i++) {
2090
2091         if (($queue_mode or $nfqueue_mode) and $queue_pre_match_max > 0) {
2092             ### limit the number of content matches to perform within the
2093             ### kernel before sending the packet to a userspace Snort
2094             ### instance
2095             last if $i >= $queue_pre_match_max;
2096         }
2097
2098         my $pattern_hr = $content_fast_pattern_order[$i];
2099         my $content_str = $pattern_hr->{'ipt_pattern'};
2100
2101         if ($content_str =~ /\|.+\|/) {
2102             ### there is hex data in the content
2103             if ($pattern_hr->{'negative_match'}) {
2104                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2105                     {'content'}{'iptopt'} . ' ' .
2106                     qq{! --hex-string "$content_str"};
2107             } else {
2108                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2109                     {'content'}{'iptopt'} . ' ' .
2110                     qq{--hex-string "$content_str"};
2111             }
2112         } else {
2113             ### there is no hex data in the content
2114             if ($pattern_hr->{'negative_match'}) {
2115                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2116                     {'content'}{'iptopt'} . ' ' .
2117                     qq{! --string "$content_str"};
2118             } else {
2119                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2120                     {'content'}{'iptopt'} . ' ' .
2121                     qq{--string "$content_str"};
2122             }
2123         }
2124
2125         if (defined $opts_hr->{'replace'}) {
2126             my $replace_str = $opts_hr->{'replace'};
2127             $replace_str =~ s/`/\\`/g;
2128             if ($replace_str =~ /\|.+\|/) {  ### there is hex data in the content
2129                 $ipt_content_criteria
2130                         .= qq{ --replace-hex-string "$replace_str"};
2131             } else {
2132                 $ipt_content_criteria
2133                         .= qq{ --replace-string "$replace_str"};
2134             }
2135         }
2136
2137         if ($kernel_ver ne '2.4') {
2138             $ipt_content_criteria .= " --algo $string_match_alg";
2139
2140             ### see if we have any offset, depth, distance, or within
2141             ### criteria
2142
2143             if (defined $pattern_hr->{'offset'}) {
2144
2145                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2146                     {'offset'}{'iptopt'} . " $pattern_hr->{'offset'}";
2147                 $perl_trigger_command .= 'A'x$pattern_hr->{'offset'}
2148                     if $include_perl_triggers;
2149
2150             } elsif (defined $pattern_hr->{'distance'}) {  ### offset trumps distance
2151
2152                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2153                     {'distance'}{'iptopt'} . " $pattern_hr->{'distance'}";
2154
2155                 $perl_trigger_command .= 'A'x$pattern_hr->{'distance'}
2156                     if $include_perl_triggers;
2157             }
2158
2159             if (defined $pattern_hr->{'depth'}) {
2160
2161                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2162                     {'depth'}{'iptopt'} . " $pattern_hr->{'depth'}";
2163
2164                 $perl_trigger_command .= 'A'x$pattern_hr->{'depth'}
2165                     if $include_perl_triggers;
2166
2167             } elsif (defined $pattern_hr->{'within'}) {  ### depth trumps within
2168
2169                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2170                     {'within'}{'iptopt'} . " $pattern_hr->{'within'}";
2171
2172                 $perl_trigger_command .= 'A'x$pattern_hr->{'within'}
2173                     if $include_perl_triggers;
2174             }
2175
2176             ### see if we need to match the string match case insensitive
2177             if ($pattern_hr->{'nocase'}) {
2178                 $ipt_content_criteria .= ' ' . $snort_opts{'filter'}
2179                     {'nocase'}{'iptopt'};
2180             }
2181
2182             ### if the --payload option is available for
2183             ### the string match extension
2184             $ipt_content_criteria .= ' --payload'
2185                 if $ipt_has_string_payload_offset_opt;
2186         }
2187
2188         if ($include_perl_triggers) {
2189             ### now append the perl trigger command bytes
2190             if ($pattern_hr->{'negative_match'}) {
2191                 $perl_trigger_command .= 'A'x($pattern_hr->{'length'});
2192             } else {
2193                 $perl_trigger_command .= &translate_perl_trigger($content_str);
2194             }
2195         }
2196     }
2197
2198     if ($include_perl_triggers) {
2199         $perl_trigger_command .= qq|"'|;
2200     }
2201
2202     return $ipt_content_criteria, $perl_trigger_command;
2203 }
2204
2205 sub convert_pattern_for_iptables() {
2206     my $snort_str = shift;
2207
2208     my $rv = 0;
2209     my $ipt_str = $snort_str;
2210     my $log_str = '';
2211
2212     my %pattern = (
2213         'orig_snort_str' => $snort_str,
2214         'ipt_pattern'    => '',
2215         'negative_match' => 0,
2216         'fast_pattern'   => 0,
2217         'nocase'         => 0,
2218     );
2219
2220     if ($ipt_str =~ /\s*\!\s*"/) {
2221         $ipt_str =~ s/\s*\!\s*"//;
2222         $pattern{'negative_match'} = 1;
2223     }
2224
2225     $ipt_str =~ s/`/|60|/g;     ### ` -> |60|
2226     $ipt_str =~ s/'/|27|/g;     ### ' -> |27|
2227     $ipt_str =~ s/\x2d/|2d|/g;  ### - -> |2d|
2228     $ipt_str =~ s/\x24/|24|/g;  ### $ -> |24|
2229
2230     ### convert all escaped chars to their hex equivalents
2231     $ipt_str =~ s/\x5c(.)/sprintf "|%02x|", (ord($1))/eg;
2232
2233     my $ctr = 0;
2234     while ($ipt_str =~ m/\|\|/) {
2235         ### consolidate consecutive hex blocks
2236         if ($ipt_str =~ /\|.+?\|\|.+?\|/) {
2237             $ipt_str =~ s/\|(.+?)\|\|(.+?)\|/|$1 $2|/;
2238         }
2239         $ctr++;
2240         last if $ctr > 10;
2241     }
2242
2243     ### remove all spaces between hex codes (they simply waste space
2244     ### on the command line, and they aren't part of the string to
2245     ### search in network traffic anyway).
2246     $ipt_str = &consolidate_hex_spaces($ipt_str);
2247
2248     ### handles length of hex blocks
2249     my $content_len = &get_content_len($ipt_str);
2250     if ($content_len >= $ipt_max_str_len
2251             or $content_len >= $config{'MAX_STRING_LEN'}) {
2252         return 0, "pattern too long: $snort_str", \%pattern;
2253     }
2254
2255     $pattern{'ipt_pattern'} = $ipt_str;
2256     $pattern{'length'}      = $content_len;
2257
2258     return 1, $log_str, \%pattern;
2259 }
2260
2261 sub define_offsets() {
2262     my ($patterns_ar, $avg_hdr_len, $opt, $val) = @_;
2263
2264     my $current_index = $#$patterns_ar;
2265
2266     ### the option should not be defined - if so, there is a duplicate
2267     ### Snort keyword in the signature
2268     if (defined $patterns_ar->[$current_index]->{$opt}) {
2269         return 0, "duplicate keyword: $opt";
2270     }
2271
2272     ### store the value that was in the original Snort rule
2273     $patterns_ar->[$current_index]->{"${opt}_snort_orig"} = $val;
2274
2275     ### store the value and account for average header length if
2276     ### necessary
2277     if ($ipt_has_string_payload_offset_opt) {
2278         $patterns_ar->[$current_index]->{$opt} = $val;
2279     } else {
2280         $patterns_ar->[$current_index]->{$opt}
2281             = $val + $avg_hdr_len + $MAC_HDR_LEN;
2282     }
2283
2284     return 1, '';
2285 }
2286
2287 sub update_offsets_relative_matches() {
2288     my $patterns_ar = shift;
2289
2290     for (my $i=0; $i <= $#$patterns_ar; $i++) {
2291
2292         my $pat_hr = $patterns_ar->[$i];
2293         my $snort_offset   = -1;
2294         my $snort_depth    = -1;
2295         my $snort_distance = -1;
2296         my $snort_within   = -1;
2297
2298         $snort_offset = $pat_hr->{'offset_snort_orig'}
2299             if defined $pat_hr->{'offset_snort_orig'};
2300         $snort_depth = $pat_hr->{'depth_snort_orig'}
2301             if defined $pat_hr->{'depth_snort_orig'};
2302         $snort_distance = $pat_hr->{'distance_snort_orig'}
2303             if defined $pat_hr->{'distance_snort_orig'};
2304         $snort_within = $pat_hr->{'within_snort_orig'}
2305             if defined $pat_hr->{'within_snort_orig'};
2306
2307         if ($snort_depth > -1) {
2308             ### if there is also an offset, then the depth begins after
2309             ### the offset starts
2310             if ($snort_offset > -1) {
2311                 $pat_hr->{'depth'} += $snort_offset;
2312             } elsif ($snort_distance > -1) {
2313                 $pat_hr->{'depth'} += $snort_distance;
2314             }
2315             if ($snort_depth < $pat_hr->{'length'}) {
2316                 return 0, "depth: $snort_depth less than " .
2317                     "pattern length: $pat_hr->{'length'}";
2318             }
2319         }
2320
2321         if ($snort_distance > -1) {
2322             ### see if we need to increase the distance based
2323             ### on the length of the previous pattern match and offset
2324             if ($i > 0) {
2325                 my $prev_pat_hr = $patterns_ar->[$i-1];
2326                 $pat_hr->{'distance'} += $prev_pat_hr->{'length'};
2327                 if (defined $prev_pat_hr->{'offset_snort_orig'}) {
2328                     $pat_hr->{'distance'} += $prev_pat_hr->{'offset_snort_orig'};
2329                 }
2330             }
2331         }
2332
2333         if ($snort_within > -1) {
2334             if ($snort_offset > -1) {
2335                 $pat_hr->{'within'} += $snort_offset;
2336             } elsif ($snort_distance > -1) {
2337                 $pat_hr->{'within'} += $snort_distance;
2338             }
2339             if ($i > 0) {
2340                 my $prev_pat_hr = $patterns_ar->[$i-1];
2341                 $pat_hr->{'within'} += $prev_pat_hr->{'length'};
2342                 if (defined $prev_pat_hr->{'offset_snort_orig'}) {
2343                     $pat_hr->{'within'} += $prev_pat_hr->{'offset_snort_orig'};
2344                 }
2345             }
2346         }
2347     }
2348     return 1, '';
2349 }
2350
2351 sub get_avg_hdr_len() {
2352     my $hdr_hr = shift;
2353
2354     my $avg_hdr_len = $config{'AVG_IP_HEADER_LEN'};
2355     if (defined $hdr_hr->{'proto'}) {
2356         if ($hdr_hr->{'proto'} =~ /udp/i) {
2357             $avg_hdr_len += $UDP_HDR_LEN;  ### udp header is 8 bytes
2358         } elsif ($hdr_hr->{'proto'} =~ /icmp/i) {
2359             $avg_hdr_len += $ICMP_HDR_LEN;  ### icmp header is 8 bytes
2360         } else {
2361             ### default to TCP
2362             $avg_hdr_len += $config{'AVG_TCP_HEADER_LEN'};
2363         }
2364     } else {
2365         ### don't know what the average transport layer (if there
2366         ### is one) length will be; add 10 bytes just to be safe
2367         $avg_hdr_len += 10;
2368     }
2369     return $avg_hdr_len;
2370 }
2371
2372 ### handles length of hex blocks
2373 sub get_content_len() {
2374     my $str = shift;
2375     my $len = 0;
2376     my $hex_mode = 0;
2377     my @chars = split //, $str;
2378     for (my $i=0; $i<=$#chars; $i++) {
2379         if ($chars[$i] eq '|') {
2380             $hex_mode == 0 ? ($hex_mode = 1) : ($hex_mode = 0);
2381             next;
2382         }
2383         if ($hex_mode) {
2384             next if $chars[$i] eq ' ';
2385             $len++;
2386             $i++;
2387         } else {
2388             $len++;
2389         }
2390     }
2391     return $len;
2392 }
2393
2394 sub consolidate_hex_spaces() {
2395     my $str = shift;
2396     my $new_str = '';
2397     my $hex_mode = 0;
2398     my @chars = split //, $str;
2399     for my $char (@chars) {
2400         if ($char eq '|') {
2401             $hex_mode == 0 ? ($hex_mode = 1) : ($hex_mode = 0);
2402         }
2403         if ($hex_mode) {
2404             next if $char eq ' ';
2405         }
2406         $new_str .= $char;
2407     }
2408     return $new_str;
2409 }
2410
2411 sub translate_perl_trigger() {
2412     my $str = shift;
2413     my $trigger_str = '';
2414     my $hex_mode = 0;
2415     my $append_hex_str = '';
2416     my $append_non_hex_str = '';
2417
2418     my @chars = split //, $str;
2419
2420     for my $char (@chars) {
2421         if ($char eq '|') {
2422             if ($hex_mode) {
2423                 if ($append_hex_str) {
2424                     while ($append_hex_str =~ /(.{2})/g) {
2425                         $trigger_str .= "\\x$1";
2426                     }
2427                     $append_hex_str = '';
2428                 }
2429                 $hex_mode = 0;
2430             } else {
2431                 if ($append_non_hex_str) {
2432                     $trigger_str .= qq|$append_non_hex_str|;
2433                     $append_non_hex_str = '';
2434                 }
2435                 $hex_mode = 1;
2436             }
2437             next;
2438         }
2439
2440         if ($hex_mode) {
2441             $append_hex_str .= $char;
2442         } else {
2443             $append_non_hex_str .= $char;
2444         }
2445     }
2446
2447     if ($append_hex_str) {
2448         while ($append_hex_str =~ /(.{2})/g) {
2449             $trigger_str .= "\\x$1";
2450         }
2451     }
2452     $trigger_str .= qq|$append_non_hex_str| if $append_non_hex_str;
2453
2454     return $trigger_str;
2455 }
2456
2457 sub ipt_add_rule() {
2458     my ($hdr_hr, $opts_hr, $orig_snort_rule, $rule_base,
2459         $target_str, $comment, $add_snort_comment,
2460         $perl_trigger_str, $add_perl_trigger, $chain, $fwsnort_chain) = @_;
2461
2462     my $action_rule = '';
2463     if ($hdr_hr->{'proto'} eq 'tcp') {
2464         if ($hdr_hr->{'action'} eq 'pass') {
2465             $action_rule = "$rule_base -j ACCEPT";
2466         } else {
2467             if (defined $opts_hr->{'resp'}
2468                     and $opts_hr->{'resp'} =~ /rst/i) {
2469                 ### iptables can only send tcp resets to the connection
2470                 ### client, so we can't support rst_rcv, but we should
2471                 ### try to tear the connection down anyway.
2472                 $action_rule = "$rule_base -j REJECT " .
2473                     "--reject-with tcp-reset";
2474             } elsif ($ipt_drop) {
2475                 $action_rule = "$rule_base -j DROP";
2476             } elsif ($ipt_reject) {
2477                 $action_rule = "$rule_base -j REJECT " .
2478                     "--reject-with tcp-reset";
2479             }
2480         }
2481     } elsif ($hdr_hr->{'proto'} eq 'udp') {
2482         if ($hdr_hr->{'action'} eq 'pass') {
2483             $action_rule = "$rule_base -j ACCEPT";
2484         } else {
2485             if (defined $opts_hr->{'resp'}
2486                     and $opts_hr->{'resp'} =~ /icmp/i) {
2487                 if ($opts_hr->{'resp'} =~ /all/i) {  ### icmp_all
2488                     $action_rule = "$rule_base -j REJECT " .
2489                         "--reject-with icmp-port-unreachable";
2490                 } elsif ($opts_hr->{'resp'} =~ /net/i) {  ### icmp_net
2491                     $action_rule = "$rule_base -j REJECT " .
2492                         "--reject-with icmp-net-unreachable";
2493                 } elsif ($opts_hr->{'resp'} =~ /host/i) {  ### icmp_host
2494                     $action_rule = "$rule_base -j REJECT " .
2495                         "--reject-with icmp-host-unreachable";
2496                 } elsif ($opts_hr->{'resp'} =~ /port/i) {  ### icmp_port
2497                     $action_rule = "$rule_base -j REJECT " .
2498                         "--reject-with icmp-port-unreachable";
2499                 }
2500             } elsif ($ipt_drop) {
2501                 $action_rule = "$rule_base -j DROP";
2502             } elsif ($ipt_reject) {
2503                 $action_rule = "$rule_base -j REJECT " .
2504                     "--reject-with icmp-port-unreachable";
2505             }
2506         }
2507     } else {
2508         if ($hdr_hr->{'action'} eq 'pass') {
2509             $action_rule = "$rule_base -j ACCEPT";
2510         } else {
2511             $action_rule = "$rule_base -j DROP";
2512         }
2513     }
2514     my $ipt_rule = $rule_base . $target_str;
2515
2516     push @ipt_script_lines, "\n### $orig_snort_rule" if $add_snort_comment;
2517     if ($include_perl_triggers and $add_perl_trigger) {
2518         push @ipt_script_lines, "### $perl_trigger_str";
2519     }
2520     if ($verbose) {
2521         push @ipt_script_lines, qq|\$ECHO "[+] rule $ipt_rule_ctr"|;
2522     }
2523
2524     ### save format handling
2525     my $save_format_ipt_rule    = $ipt_rule . " \n";
2526     my $save_format_action_rule = $action_rule . " \n";
2527
2528     $save_format_ipt_rule    =~ s|\$${ipt_var_str}\s+\-A|-A|;
2529     $save_format_action_rule =~ s|\$${ipt_var_str}\s+\-A|-A|;
2530
2531     if ($hdr_hr->{'action'} ne 'pass') {
2532         if ($queue_mode or $nfqueue_mode) {
2533
2534             push @ipt_script_lines, $ipt_rule;
2535             push @{$save_format_rules{$fwsnort_chain}}, $save_format_ipt_rule;
2536
2537         } else {
2538
2539             push @ipt_script_lines, $ipt_rule unless $no_ipt_log;
2540             push @{$save_format_rules{$fwsnort_chain}}, $save_format_ipt_rule
2541                 unless $no_ipt_log;
2542         }
2543     }
2544
2545     if ($action_rule and ($ipt_drop or $ipt_reject or
2546             $hdr_hr->{'action'} eq 'pass' or defined $opts_hr->{'resp'})) {
2547
2548         push @ipt_script_lines, $action_rule;
2549         push @{$save_format_rules{$fwsnort_chain}}, $save_format_action_rule;
2550     }
2551
2552     $ipt_rule_ctr++;
2553     return;
2554 }
2555
2556 sub save_format_append_rules() {
2557
2558      for my $chain (sort keys %ipt_save_existing_chains) {
2559
2560         next unless &is_fwsnort_chain($chain, $MATCH_EQUIV);
2561
2562         ### make sure that whitelist/blacklist and established jump rules
2563         ### are added at the beginning of each chain in save format
2564         &save_format_add_prereqs($chain);
2565
2566         for my $rule (@{$save_format_rules{$chain}}) {
2567
2568             push @fwsnort_save_lines, $rule;
2569         }
2570     }
2571
2572     ### now append any last lines from the iptables-save output that
2573     ### had nothing to do with fwsnort (other custom chains, etc.)
2574     for (my $i = $ipt_save_index; $i <= $#ipt_save_lines; $i++) {
2575         next if &is_fwsnort_chain($ipt_save_lines[$i], $MATCH_SUBSTR);
2576         push @fwsnort_save_lines, $ipt_save_lines[$i];
2577     }
2578
2579     return;
2580 }
2581
2582 sub save_format_add_prereqs() {
2583     my $chain = shift;
2584
2585     return if defined $save_format_prereqs{$chain};
2586
2587     ### add whitelist
2588     if (defined $save_format_whitelist{$chain}) {
2589         for my $whitelist_rule (@{$save_format_whitelist{$chain}}) {
2590             push @fwsnort_save_lines, "$whitelist_rule \n";
2591         }
2592     }
2593
2594     ### add blacklist
2595     if (defined $save_format_blacklist{$chain}) {
2596         for my $blacklist_rule (@{$save_format_blacklist{$chain}}) {
2597             push @fwsnort_save_lines, "$blacklist_rule \n";
2598         }
2599     }
2600
2601     ### add jump rules into the connection tracking fwsnort chains
2602     if (defined $save_format_conntrack_jumps{$chain}) {
2603         for my $jump_rule (@{$save_format_conntrack_jumps{$chain}}) {
2604             push @fwsnort_save_lines, "$jump_rule \n";
2605         }
2606     }
2607
2608     $save_format_prereqs{$chain} = '';
2609
2610     return;
2611 }
2612
2613 sub ipt_whitelist() {
2614     my @whitelist_addrs = ();
2615
2616     for my $whitelist_line (@{$config{'WHITELIST'}}) {
2617         for my $addr (@{&expand_addresses($whitelist_line)}) {
2618             push @whitelist_addrs, $addr;
2619         }
2620     }
2621
2622     return unless $#whitelist_addrs >= 0;
2623
2624     push @ipt_script_lines, "\n###\n############ Add IP/network " .
2625         "WHITELIST rules. ############\n###";
2626
2627     for my $addr (@whitelist_addrs) {
2628         for my $chain (keys %process_chains) {
2629             next unless $process_chains{$chain};
2630
2631             if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
2632                 my $rule_str = qq|-A $config{"FWSNORT_$chain"} | .
2633                     "-s $addr -j RETURN";
2634                 push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2635
2636                 push @{$save_format_whitelist{$config{"FWSNORT_$chain"}}},
2637                     $rule_str;
2638             }
2639             if ($chain eq 'OUTPUT' or $chain eq 'FORWARD') {
2640                 my $rule_str = qq|-A $config{"FWSNORT_$chain"} | .
2641                     "-d $addr -j RETURN";
2642                 push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2643
2644                 push @{$save_format_whitelist{$config{"FWSNORT_$chain"}}},
2645                     $rule_str;
2646             }
2647         }
2648     }
2649     return;
2650 }
2651
2652 sub ipt_blacklist() {
2653
2654     my $printed_intro = 0;
2655
2656     for my $blacklist_line (@{$config{'BLACKLIST'}}) {
2657
2658         my @blacklist_addrs = ();
2659         my $target = 'DROP';  ### default
2660
2661         if ($blacklist_line =~ /\s+REJECT/) {
2662             $target = 'REJECT';
2663         }
2664
2665         for my $addr (@{&expand_addresses($blacklist_line)}) {
2666             push @blacklist_addrs, $addr;
2667         }
2668
2669         return unless $#blacklist_addrs >= 0;
2670
2671         unless ($printed_intro) {
2672             push @ipt_script_lines, "\n###\n############ Add IP/network " .
2673                 "BLACKLIST rules. ############\n###";
2674             $printed_intro = 1;
2675         }
2676
2677         for my $addr (@blacklist_addrs) {
2678             for my $chain (keys %process_chains) {
2679                 next unless $process_chains{$chain};
2680
2681                 if ($target eq 'DROP') {
2682                     if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
2683
2684                         my $rule_str = qq|-A $config{"FWSNORT_$chain"} | .
2685                             "-s $addr -j DROP";
2686
2687                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2688                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2689                             $rule_str;
2690                     }
2691
2692                     if ($chain eq 'OUTPUT' or $chain eq 'FORWARD') {
2693
2694                         my $rule_str = qq|-A $config{"FWSNORT_$chain"} | .
2695                             "-d $addr -j DROP";
2696                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2697                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2698                             $rule_str;
2699                     }
2700                 } else {
2701                     if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
2702                         my $rule_str = qq|-A $config{"FWSNORT_$chain"} -s $addr | .
2703                             "-p tcp -j REJECT --reject-with tcp-reset";
2704
2705                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2706                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2707                             $rule_str;
2708
2709                         $rule_str = qq|-A $config{"FWSNORT_$chain"} -s $addr | .
2710                             "-p udp -j REJECT --reject-with icmp-port-unreachable";
2711
2712                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2713                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2714                             $rule_str;
2715
2716                         $rule_str = qq|-A $config{"FWSNORT_$chain"} -s $addr | .
2717                             "-p icmp -j REJECT --reject-with icmp-host-unreachable";
2718
2719                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2720                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2721                             $rule_str;
2722                     }
2723                     if ($chain eq 'OUTPUT' or $chain eq 'FORWARD') {
2724                         my $rule_str = qq|-A $config{"FWSNORT_$chain"} -d $addr | .
2725                             "-p tcp -j REJECT --reject-with tcp-reset";
2726
2727                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2728                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2729                             $rule_str;
2730
2731                         $rule_str = qq|-A $config{"FWSNORT_$chain"} -d $addr | .
2732                             "-p udp -j REJECT --reject-with icmp-port-unreachable";
2733
2734                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2735                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2736                             $rule_str;
2737
2738                         $rule_str = qq|-A $config{"FWSNORT_$chain"} -d $addr | .
2739                             "-p icmp -j REJECT --reject-with icmp-host-unreachable";
2740
2741                         push @ipt_script_lines, "\$${ipt_var_str} $rule_str";
2742                         push @{$save_format_blacklist{$config{"FWSNORT_$chain"}}},
2743                             $rule_str;
2744                     }
2745                 }
2746             }
2747         }
2748     }
2749     return;
2750 }
2751
2752 sub ipt_add_chains() {
2753
2754     ### save format
2755     my %ipt_save_built_in_chains = ();
2756     my $look_for_chains = 0;
2757     for (@ipt_save_lines) {
2758         unless ($look_for_chains) {
2759             push @fwsnort_save_lines, $_;
2760         }
2761         if (/^\*filter/) {
2762             $look_for_chains = 1;
2763         } elsif ($look_for_chains and $_ =~ /^:(\S+)/) {
2764             my $chain = $1;
2765             if ($chain eq 'INPUT'
2766                     or $chain eq 'OUTPUT'
2767                     or $chain eq 'FORWARD') {
2768                 $ipt_save_built_in_chains{$chain} = $_;
2769             } else {
2770                 ### don't preserve any old fwsnort chains, but preserve
2771                 ### any other existing custom chains
2772                 unless (&is_fwsnort_chain($chain, $MATCH_EQUIV)) {
2773                     $ipt_save_existing_chains{$chain} = $_;
2774                 }
2775             }
2776         } elsif ($look_for_chains and $_ !~ /^:(\S+)/) {
2777             last;
2778         }
2779         $ipt_save_index++;
2780     }
2781
2782     ### save format - add the built-in chains first
2783     for my $chain (qw(INPUT FORWARD OUTPUT)) {
2784         ### should always be defined unless we're not running as root
2785         next unless defined $ipt_save_built_in_chains{$chain};
2786         push @fwsnort_save_lines, $ipt_save_built_in_chains{$chain};
2787     }
2788
2789     ### add the fwsnort chains
2790     push @ipt_script_lines, "\n###\n############ Create " .
2791         "fwsnort $ipt_str chains. ############\n###";
2792
2793     for my $built_in_chain (qw(INPUT FORWARD OUTPUT)) {
2794
2795         ### see if any of the "FWSNORT_<built-in-chain>" chains need to be
2796         ### excluded
2797         next unless $process_chains{$built_in_chain};
2798
2799         for my $chain ($config{"FWSNORT_$built_in_chain"},
2800                 $config{"FWSNORT_${built_in_chain}_ESTAB"}) {
2801             if ($no_ipt_conntrack) {
2802                 next if $chain eq
2803                     $config{"FWSNORT_${built_in_chain}_ESTAB"};
2804             }
2805             push @ipt_script_lines,
2806                 "\$${ipt_var_str} -N $chain 2> /dev/null",
2807                 "\$${ipt_var_str} -F $chain\n";
2808
2809             ### save format
2810             $ipt_save_existing_chains{$chain} = ":$chain - [0:0]\n";
2811         }
2812     }
2813
2814     ### save format - add the custom chains
2815     for my $chain (sort keys %ipt_save_existing_chains) {
2816         push @fwsnort_save_lines, $ipt_save_existing_chains{$chain};
2817     }
2818
2819     ### save format - add in the jump rules from the
2820     ### built-in chains here
2821     &save_format_add_jump_rules();
2822
2823     ### add in any rules from custom chains that alphabetically come
2824     ### before the first fwsnort chain
2825     &save_format_add_early_custom_chains();
2826
2827     return;
2828 }
2829
2830 sub save_format_add_jump_rules() {
2831
2832     ### add the jump rule for each built-in chain
2833     for my $built_in_chain (qw(INPUT FORWARD OUTPUT)) {
2834
2835         next unless defined $process_chains{$built_in_chain}
2836             and $process_chains{$built_in_chain};
2837
2838         ### get the current $chain rules (if any), and then see where to add
2839         ### the fwsnort jump rule
2840
2841         my @existing_chain_rules = ();
2842
2843         for (my $i = $ipt_save_index; $i < $#ipt_save_lines; $i++) {
2844
2845             ### delete any existing fwsnort jump rules
2846             if ($ipt_save_lines[$i] =~ /^\-A\s$built_in_chain\s/) {
2847                 if ($ipt_save_lines[$i] !~ /\-j\sFWSNORT_/) {
2848                     push @existing_chain_rules, $ipt_save_lines[$i];
2849                 }
2850             } else {
2851                 last;
2852             }
2853             $ipt_save_index++;
2854         }
2855
2856         my $ctr = 1;
2857         my $added_jump_rule = 0;
2858         for my $existing_rule (@existing_chain_rules) {
2859             if ($ctr == $config{"FWSNORT_${built_in_chain}_JUMP"}) {
2860                 &save_format_add_chain_jump_rule($built_in_chain);
2861                 $added_jump_rule = 1;
2862             }
2863
2864             push @fwsnort_save_lines, $existing_rule;
2865             $ctr++;
2866         }
2867
2868         ### the chain may have been empty
2869         unless ($added_jump_rule) {
2870             &save_format_add_chain_jump_rule($built_in_chain);
2871         }
2872     }
2873
2874     return;
2875 }
2876
2877 sub save_format_add_chain_jump_rule() {
2878     my $built_in_chain = shift;
2879     my $fwsnort_chain = "FWSNORT_${built_in_chain}";
2880     if (%restrict_interfaces) {
2881         for my $intf (keys %restrict_interfaces) {
2882             if ($built_in_chain eq 'INPUT' or $built_in_chain eq 'FORWARD') {
2883                 push @fwsnort_save_lines, "-A $built_in_chain -i $intf " .
2884                     "-j $fwsnort_chain \n";
2885             } elsif ($built_in_chain eq 'OUTPUT') {
2886                 push @fwsnort_save_lines, "-A $built_in_chain -o $intf " .
2887                     "-j $fwsnort_chain \n";
2888             }
2889         }
2890     } else {
2891         if ($no_exclude_loopback) {
2892             push @fwsnort_save_lines, "-A $built_in_chain " .
2893                 "-j $fwsnort_chain \n";
2894         } else {
2895             if ($built_in_chain eq 'INPUT' or $built_in_chain eq 'FORWARD') {
2896                 push @fwsnort_save_lines, "-A $built_in_chain ! -i lo " .
2897                     "-j $fwsnort_chain \n";
2898             } elsif ($built_in_chain eq 'OUTPUT') {
2899                 push @fwsnort_save_lines, "-A $built_in_chain ! -o lo " .
2900                     "-j $fwsnort_chain \n";
2901             }
2902         }
2903     }
2904
2905     return;
2906 }
2907
2908 sub save_format_add_early_custom_chains() {
2909
2910     for my $chain (sort keys %ipt_save_existing_chains) {
2911         last if &is_fwsnort_chain($chain, $MATCH_EQUIV);
2912
2913         for (my $i = $ipt_save_index; $i <= $#ipt_save_lines; $i++) {
2914             last if &is_fwsnort_chain($ipt_save_lines[$i], $MATCH_SUBSTR);
2915             push @fwsnort_save_lines, $ipt_save_lines[$i];
2916             $ipt_save_index++;
2917         }
2918     }
2919
2920     return;
2921 }
2922
2923 sub is_fwsnort_chain() {
2924     my ($str, $match_style) = @_;
2925     my $rv = 0;
2926
2927     for my $fwsnort_chain ($config{'FWSNORT_INPUT'},
2928             $config{'FWSNORT_INPUT_ESTAB'},
2929             $config{'FWSNORT_FORWARD'},
2930             $config{'FWSNORT_FORWARD_ESTAB'},
2931             $config{'FWSNORT_OUTPUT'},
2932             $config{'FWSNORT_OUTPUT_ESTAB'}) {
2933
2934         if ($match_style eq $MATCH_SUBSTR) {
2935             if ($str =~ /$fwsnort_chain/) {
2936                 $rv = 1;
2937                 last;
2938             }
2939         } elsif ($match_style eq $MATCH_EQUIV) {
2940             if ($str eq $fwsnort_chain) {
2941                 $rv = 1;
2942                 last;
2943             }
2944         }
2945     }
2946
2947     return $rv;
2948 }
2949
2950 sub ipt_add_conntrack_jumps() {
2951     ### jump ESTABLISHED tcp traffic to each of the _ESTAB
2952     ### chains
2953     push @ipt_script_lines, "\n###\n############ Inspect $conntrack_state " .
2954         "tcp connections. ############\n###";
2955
2956     for my $chain (keys %process_chains) {
2957         next unless $process_chains{$chain};
2958
2959         my $rule_str = '';
2960
2961         if ($have_conntrack) {
2962             $rule_str = qq|-A $config{"FWSNORT_$chain"} -p tcp -m conntrack | .
2963                 qq|--ctstate $conntrack_state -j | .
2964                 qq|$config{"FWSNORT_${chain}_ESTAB"}|;
2965         } elsif ($have_state) {
2966             $rule_str = qq|-A $config{"FWSNORT_$chain"} -p tcp -m state | .
2967                 qq|--state $conntrack_state -j | .
2968                 qq|$config{"FWSNORT_${chain}_ESTAB"}|;
2969         }
2970
2971         push @ipt_script_lines, qq|\$${ipt_var_str} $rule_str|;
2972         push @{$save_format_conntrack_jumps{$config{"FWSNORT_$chain"}}},
2973             $rule_str;
2974     }
2975     return;
2976 }
2977
2978 sub ipt_jump_chain() {
2979     push @ipt_script_lines, "\n###\n############ Jump traffic " .
2980         "to the fwsnort chains. ############\n###";
2981     if (%restrict_interfaces) {
2982         for my $intf (keys %restrict_interfaces) {
2983             for my $chain (keys %process_chains) {
2984                 next unless $process_chains{$chain};
2985
2986                 if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
2987                     ### delete any existing jump rule so that fwsnort.sh can
2988                     ### be executed many times in a row without adding several
2989                     ### jump rules
2990                     push @ipt_script_lines, "\$${ipt_var_str} -D $chain " .
2991                         qq|-i $intf -j $config{"FWSNORT_$chain"}| .
2992                         ' 2> /dev/null';
2993
2994                     ### now add the jump rule
2995                     push @ipt_script_lines, "\$${ipt_var_str} -I $chain " .
2996                         qq|$config{"FWSNORT_${chain}_JUMP"} -i | .
2997                         qq|$intf -j $config{"FWSNORT_$chain"}|;
2998                 } elsif ($chain eq 'OUTPUT') {
2999
3000                     push @ipt_script_lines, "\$${ipt_var_str} -D $chain " .
3001                         qq|-o $intf -j $config{'FWSNORT_OUTPUT'}| .
3002                         ' 2> /dev/null';
3003
3004                     push @ipt_script_lines, "\$${ipt_var_str} -I $chain " .
3005                         qq|$config{'FWSNORT_OUTPUT_JUMP'} -o | .
3006                         qq|$intf -j $config{'FWSNORT_OUTPUT'}|;
3007                 }
3008             }
3009         }
3010     } else {
3011         for my $chain (keys %process_chains) {
3012             next unless $process_chains{$chain};
3013
3014             if ($no_exclude_loopback) {
3015
3016                 push @ipt_script_lines, "\$${ipt_var_str} -D $chain " .
3017                     qq|-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"} | .
3022                     qq|-j $config{"FWSNORT_$chain"}|;
3023             } else {
3024                 if ($chain eq 'INPUT' or $chain eq 'FORWARD') {
3025
3026                     push @ipt_script_lines, "\$${ipt_var_str} -D $chain " .
3027                         qq|! -i 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"} ! -i lo | .
3032                         qq|-j $config{"FWSNORT_$chain"}|;
3033
3034                 } elsif ($chain eq 'OUTPUT') {
3035
3036                     push @ipt_script_lines, "\$${ipt_var_str} -D $chain " .
3037                         qq|! -o lo -j $config{"FWSNORT_$chain"}| .
3038                         ' 2> /dev/null';
3039
3040                     push @ipt_script_lines, "\$${ipt_var_str} -I $chain " .
3041                         qq|$config{"FWSNORT_${chain}_JUMP"} ! -o lo | .
3042                         qq|-j $config{"FWSNORT_$chain"}|;
3043                 }
3044             }
3045         }
3046     }
3047     return;
3048 }
3049
3050 sub hdr_lines() {
3051     return
3052         "#!$cmds{'sh'}\n#", '#'x76,
3053         "#\n# File:  $config{'FWSNORT_SCRIPT'}",
3054         "#\n# Purpose:  This script was auto-" .
3055         "generated by fwsnort, and implements",
3056         "#           an $ipt_str ruleset based upon " .
3057         "Snort rules.  For more",
3058         "#           information see the fwsnort man " .
3059         "page or the documentation",
3060         "#           available at " .
3061         "http://www.cipherdyne.org/fwsnort/",
3062         "#\n# Generated with:     fwsnort @argv_cp",
3063         "# Generated on host:  " . hostname(),
3064         "# Time stamp:         " . localtime(),
3065         "#\n# Author:  Michael Rash <mbr\@cipherdyne.org>",
3066         "#\n# Version: $version",
3067         "#", '#'x76, "#\n";
3068 }
3069
3070 sub ipt_hdr() {
3071     push @ipt_script_lines, &hdr_lines();
3072     push @ipt_save_script_lines, $ipt_script_lines[$#ipt_script_lines];
3073
3074     ### add paths to system binaries (iptables included)
3075     &ipt_config_section();
3076     return;
3077 }
3078
3079 sub ipt_config_section() {
3080     ### build the config section of the iptables script
3081     push @ipt_script_lines,
3082         '#==================== config ====================',
3083         "ECHO=$cmds{'echo'}",
3084         "${ipt_var_str}=$ipt_bin",
3085         "#================== end config ==================\n";
3086     push @ipt_save_script_lines, $ipt_script_lines[$#ipt_script_lines];
3087     return;
3088 }
3089
3090 sub ipt_type() {
3091     my $type = shift;
3092     push @ipt_script_lines, "\n###\n############ ${type}.rules #######" .
3093         "#####\n###", "\$ECHO \"[+] Adding $type rules:\"";
3094     return;
3095 }
3096
3097 sub check_type() {
3098     for my $type_hr (\%include_types, \%exclude_types) {
3099         for my $type (keys %$type_hr) {
3100             my $found = 0;
3101             my @valid_types = ();
3102             for my $dir (split /\,/, $config{'RULES_DIR'}) {
3103                 if (-e "$dir/${type}.rules") {
3104                     $found = 1;
3105                 } else {
3106                     opendir D, $dir or die "[*] Could not open $dir: $!";
3107                     for my $file (readdir D) {
3108                         if ($file =~ /(\S+)\.rules/) {
3109                             push @valid_types, $1;
3110                         }
3111                     }
3112                 }
3113             }
3114             unless ($found) {
3115                 print "[-] \"$type\" is not a valid type.\n",
3116                 "    Choose from the following available signature types:\n";
3117                 for my $type (sort @valid_types) {
3118                     print "        $type\n";
3119                 }
3120                 die "[-] Exiting.";
3121             }
3122         }
3123     }
3124     return;
3125 }
3126
3127 sub import_config() {
3128     open C, "< $fwsnort_conf" or die "[*] Could not open $fwsnort_conf: $!";
3129     my @lines = <C>;
3130     close C;
3131     my $l_ctr = 0;
3132     for my $line (@lines) {
3133         $l_ctr++;
3134         chomp $line;
3135         next if $line =~ /^\s*#/;
3136         next unless $line =~ /\S/;
3137         if ($line =~ /^\s*(\S+)Cmd\s+(\S+);/) {  ### e.g. "iptablesCmd"
3138             $cmds{$1} = $2;
3139         } elsif ($line =~ /^\s*(\S+)\s+(.*?);/) {
3140             my $var = $1;
3141             my $val = $2;
3142             die "[*] $fwsnort_conf: Variable \"$var\" is set to\n",
3143                 "    _CHANGEME_ at line $l_ctr.  Edit $fwsnort_conf.\n"
3144                 if $val eq '_CHANGEME_';
3145             if (defined $multi_line_vars{$var}) {
3146                 push @{$config{$var}}, $val;
3147             } else {
3148                 ### may have already been defined in existing snort.conf
3149                 ### file if --snort-conf was given.
3150                 $config{$var} = $val unless defined $config{$var};
3151             }
3152         }
3153     }
3154
3155     &expand_vars();
3156
3157     return;
3158 }
3159
3160 sub ipt_list() {
3161     for my $chain (
3162         $config{'FWSNORT_INPUT'},
3163         $config{'FWSNORT_INPUT_ESTAB'},
3164         $config{'FWSNORT_OUTPUT'},
3165         $config{'FWSNORT_OUTPUT_ESTAB'},
3166         $config{'FWSNORT_FORWARD'},
3167         $config{'FWSNORT_FORWARD_ESTAB'}
3168     ) {
3169         my $cmd = "$ipt_bin -v -n -L $chain";
3170         my $exists = (system "$cmd > /dev/null 2>&1") >> 8;
3171         if ($exists == 0) {
3172             print "[+] Listing $chain chain...\n";
3173             system $cmd;
3174             print "\n";
3175         } else {
3176             print "[-] Chain $chain does not exist...\n";
3177         }
3178     }
3179     exit 0;
3180 }
3181
3182 sub ipt_flush() {
3183     for my $chain (
3184         $config{'FWSNORT_INPUT'},
3185         $config{'FWSNORT_INPUT_ESTAB'},
3186         $config{'FWSNORT_OUTPUT'},
3187         $config{'FWSNORT_OUTPUT_ESTAB'},
3188         $config{'FWSNORT_FORWARD'},
3189         $config{'FWSNORT_FORWARD_ESTAB'}
3190     ) {
3191         my $exists = (system "$ipt_bin -n -L " .
3192             "$chain > /dev/null 2>&1") >> 8;
3193         if ($exists == 0) {
3194             print "[+] Flushing $chain chain...\n";
3195             system "$ipt_bin -F $chain";
3196             if ($ipt_del_chains) {
3197                 ### must remove any jump rules from the built-in
3198                 ### chains
3199                 &del_jump_rule($chain);
3200
3201                 print "    Deleting $chain chain...\n";
3202                 system "$ipt_bin -X $chain";
3203             }
3204         } else {
3205             print "[-] Chain $chain does not exist...\n";
3206         }
3207     }
3208     exit 0;
3209 }
3210
3211 sub cache_ipt_save_policy() {
3212
3213     return unless $is_root;
3214
3215     open IPT, "$save_bin -t filter |" or die "[*] Could not execute $save_bin";
3216     while (<IPT>) {
3217         push @ipt_save_lines, $_;
3218     }
3219     close IPT;
3220
3221     ### also write out the current iptables policy so that we can
3222     ### revert to it if necessary (iptables does a good job of not committing
3223     ### a policy via iptables-save if there is a problem with a rule though).
3224     &archive($config{'IPT_BACKUP_SAVE_FILE'});
3225     open F, "> $config{'IPT_BACKUP_SAVE_FILE'}" or die "[*] Could not " .
3226         "open $config{'IPT_BACKUP_SAVE_FILE'}: $!";
3227     print F for @ipt_save_lines;
3228     close F;
3229
3230     ### remove the last two lines (the 'COMMIT' and '# Completed ...' lines
3231     ### so they can be added later).
3232     $ipt_save_completed_line = $ipt_save_lines[$#ipt_save_lines];
3233     pop @ipt_save_lines;
3234     pop @ipt_save_lines;
3235
3236     return;
3237 }
3238
3239 sub del_jump_rule() {
3240     my $chain = shift;
3241
3242     my $ipt = new IPTables::Parse 'iptables' => $cmds{'iptables'}
3243         or die "[*] Could not acquire IPTables::Parse object: $!";
3244
3245     for my $built_in_chain (qw(INPUT OUTPUT FORWARD)) {
3246         my $rules_ar = $ipt->chain_rules('filter', $built_in_chain, $ipt_file);
3247
3248         for (my $i=0; $i <= $#$rules_ar; $i++) {
3249             my $rule_num = $i+1;
3250             if ($rules_ar->[$i]->{'target'} eq $chain) {
3251                 system "$ipt_bin -D $built_in_chain $rule_num";
3252                 last;
3253             }
3254         }
3255     }
3256
3257     return;
3258 }
3259
3260 sub fwsnort_init() {
3261
3262     ### set umask to -rw-------
3263     umask 0077;
3264
3265     ### turn off buffering
3266     $| = 1;
3267
3268     &set_non_root_values() unless $is_root;
3269
3270     ### read in configuration info from the config file
3271     &import_config();
3272
3273     ### make sure the commands are where the
3274     ### config file says they are
3275     &chk_commands();
3276
3277     ### make sure all of the required variables are defined
3278     ### in the config file
3279     &required_vars();
3280
3281     $non_host    = $NON_HOST;
3282     $ipt_bin     = $cmds{'iptables'};
3283     $restore_bin = $cmds{'iptables-restore'};
3284     $save_bin    = $cmds{'iptables-save'};
3285
3286     if ($enable_ip6tables) {
3287         for my $opt (qw(itype icode ttl tos ipopts)) {
3288             $snort_opts{'unsupported'}{$opt}
3289                 = $snort_opts{'filter'}{$opt};
3290             delete $snort_opts{'filter'}{$opt};
3291         }
3292         $non_host    = $NON_IP6_HOST;
3293         $save_str    = 'ip6tables-save';
3294         $ipt_str     = 'ip6tables';
3295         $ipt_bin     = $cmds{'ip6tables'};
3296         $restore_bin = $cmds{'ip6tables-restore'};
3297         $save_bin    = $cmds{'ip6tables-save'};
3298     }
3299
3300     unless ($is_root) {
3301         $no_ipt_test = 1;
3302     }
3303
3304     if ($ipt_exec) {
3305         die "[*] You need to be root for --ipt-apply" unless $is_root;
3306         if (-e $config{'FWSNORT_SAVE_EXEC_FILE'}) {
3307             print "[+] Executing $config{'FWSNORT_SAVE_EXEC_FILE'}\n";
3308             system $config{'FWSNORT_SAVE_EXEC_FILE'};
3309             exit 0;
3310         } else {
3311             die "[*] $config{'FWSNORT_SAVE_EXEC_FILE'} does not exist.";
3312         }
3313     }
3314
3315     if ($enable_ip6tables) {
3316         ### switch to ip6tables
3317         $ipt_var_str = 'IP6TABLES';
3318     }
3319
3320     $process_chains{'INPUT'}   = 0 if $no_ipt_input;
3321     $process_chains{'FORWARD'} = 0 if $no_ipt_forward;
3322     $process_chains{'OUTPUT'}  = 0 if $no_ipt_output;
3323
3324     ### import HOME_NET, etc. from existing Snort config file.
3325     &import_snort_conf() if $snort_conf_file;
3326
3327     if ($rules_types) {
3328         my @types = split /\,/, $rules_types;
3329         for my $type (@types) {
3330             $include_types{$type} = '';
3331         }
3332     }
3333     if ($exclude_types) {
3334         my @types = split /\,/, $exclude_types;
3335         for my $type (@types) {
3336             $exclude_types{$type} = '';
3337         }
3338     }
3339     if ($include_sids) {
3340         ### disable iptables policy parsing if we are translating a
3341         ### specific set of Snort sids.
3342         $ipt_sync = 0;
3343
3344         my @sids = split /\,/, $include_sids;
3345         for my $sid (@sids) {
3346             $include_sids{$sid} = '';
3347         }
3348     }
3349     if ($exclude_sids) {
3350         my @sids = split /\,/, $exclude_sids;
3351         for my $sid (@sids) {
3352             $exclude_sids{$sid} = '';
3353         }
3354     }
3355     if ($ipt_restrict_intf) {
3356         my @interfaces = split /\,/, $ipt_restrict_intf;
3357         for my $intf (@interfaces) {
3358             $restrict_interfaces{$intf} = '';
3359         }
3360     }
3361
3362     if ($include_re) {
3363         if ($include_re_caseless) {
3364             $include_re = qr|$include_re|i;
3365         } else {
3366             $include_re = qr|$include_re|;
3367         }
3368     }
3369     if ($exclude_re) {
3370         if ($exclude_re_caseless) {
3371             $exclude_re = qr|$exclude_re|i;
3372         } else {
3373             $exclude_re = qr|$exclude_re|;
3374         }
3375     }
3376
3377     ### flush all fwsnort chains.
3378     &ipt_flush() if $ipt_flush or $ipt_del_chains;
3379
3380     ### list all fwsnort chains.
3381     &ipt_list() if $ipt_list;
3382
3383     ### download latest snort rules from snort.org
3384     &update_rules() if $update_rules;
3385
3386     ### make sure some directories exist, etc.
3387     &setup();
3388
3389     ### get kernel version (this is mainly used to know whether
3390     ### the "--algo bm" argument is required for the string match
3391     ### extension in the 2.6.14 (and later) kernels.  Also, the
3392     ### string match extension as of 2.6.14 supports the Snort
3393     ### offset and depth keywords via --from and --to
3394     &get_kernel_ver();
3395
3396     ### may have been specified on the command line
3397     $home_net = $config{'HOME_NET'} unless $home_net;
3398     $ext_net  = $config{'EXTERNAL_NET'} unless $ext_net;
3399
3400     &get_local_addrs() unless $no_addr_check;
3401
3402     if ($strict) {
3403         ### make the snort options parser very strict
3404         for my $opt (qw(uricontent pcre
3405                 distance within http_uri http_method urilen)) {
3406             $snort_opts{'unsupported'}{$opt}
3407                 = $snort_opts{'filter'}{$opt};
3408             delete $snort_opts{'filter'}{$opt};
3409         }
3410         my @ignore = (qw(nocase));
3411
3412         if ($kernel_ver eq '2.4') {
3413             push @ignore, 'offset', 'depth';
3414         }
3415         for my $opt (@ignore) {
3416             next unless defined $snort_opts{'ignore'}{$opt};
3417             $snort_opts{'unsupported'}{$opt}
3418                 = $snort_opts{'ignore'}{$opt};
3419             delete $snort_opts{'ignore'}{$opt};
3420         }
3421     }
3422     if ($no_pcre) {
3423         ### skip trying to translate basic PCRE's
3424         $snort_opts{'unsupported'}{'pcre'}
3425             = $snort_opts{'filter'}{'pcre'};
3426         delete $snort_opts{'filter'}{'pcre'};
3427     }
3428
3429     if ($no_fast_pattern_order) {
3430         $snort_opts{'ignore'}{'fast_pattern'}
3431             = $snort_opts{'filter'}{'fast_pattern'}{'regex'};
3432         delete $snort_opts{'filter'}{'fast_pattern'};
3433     }
3434     return;
3435 }
3436
3437 sub get_kernel_ver() {
3438     die "[*] uname command: $cmds{'uname'} is not executable."
3439         unless -x $cmds{'uname'};
3440     open U, "$cmds{'uname'} -a |" or die "[*] Could not run ",
3441         "$cmds{'uname'} -a";
3442     my $out = <U>;
3443     close U;
3444     ### Linux orthanc 2.6.12.5 #2 Tue Sep 27 22:43:02 EDT 2005 i686 \
3445     ### Pentium III (Coppermine) GenuineIntel GNU/Linux
3446     if&nbs