#!/usr/bin/perl -w # # genfw - Generate a firewall script from some rules # and a Red Hat box's network configuration # # Copyright (C) 2001 Steven Pritchard # This program is free software; you can redistribute it # and/or modify it under the same terms as Perl itself. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # # $Id: genfw,v 1.21 2002/03/06 15:19:42 steve Exp $ #### # # TODO: # # * FIXME - Maximum prefix length 29 for --log-prefix # * FIXME - Chain names have a maximum length (31 characters?) too # * FIXME - We should do something useful with inserted or appended # rules in tables other than filter and nat. # * FIXME - It would probably be good to set the default policy for # chains in other tables (like nat) to DROP and then add # explicit rules for accepting packets. # * FIXME - We get warnings when things (like NETWORK and BROADCAST) # aren't defined in ifcfg-*. # * FIXME - We should be dropping reserved addresses. # * FIXME - We should be dropping traffic to/from port 0. # * FIXME - Users should be able to add arbitrary chains. # #### use strict; use FileHandle; use DirHandle; use Socket; use Getopt::Std; eval { use Data::Dumper; } if ($ENV{'DEBUG'}); sub reject_auth($$); sub check_allowed($$); sub allow($$@); sub label($); sub icmpfilter($$); sub established($$); sub drop($$); sub accept($$); sub filter_from($$); sub filter_to($$$); sub generate_rules(%); sub parseconfig($$); sub debug(@); sub bold($); sub generate_rules(%); sub comment_block(@); sub comment_long(@); sub comment(@); sub begin_script(); sub set_policy($$$); sub iptables(@); sub new_chain($); sub insert($); sub append($); sub action($$); sub clear_all(); use vars qw(%interface %conf $config_dir %opt); getopts('id', \%opt); if (defined($ENV{'DEBUG'}) || $opt{'d'}) { $config_dir="."; $|=1; my $oldfh=select(STDERR); $|=1; select($oldfh); } else { $config_dir="/etc/sysconfig"; } # Default $conf{'log'}=[qw(-m limit -j LOG)]; my $fh=new FileHandle "<$config_dir/rules" or die "Couldn't open $config_dir/rules: $!\n"; # *FIXME* - need to handle when the last line ends in '\' my $last; while (<$fh>) { chomp; s/(? $type}; if (@parts) { $interface{$name}->{'flags'}=[@parts]; debug "interface $name flags \"@{$interface{$name}->{'flags'}}\""; } } elsif (defined($parts[1]) and ($parts[1] eq "logging")) { if ($parts[0] eq "no") { $conf{'log'}=[]; } elsif ($parts[0] eq "limit") { $conf{'log'}=[qw(-m limit -j LOG)]; } elsif ($parts[0] eq "full") { $conf{'log'}=[qw(-j LOG)]; } else { warn "I don't know what you mean by '@parts[0,1]' (line $.).\n"; next; } } elsif ($parts[0]=~/^(append|insert)$/ and defined($parts[1]) and defined($parts[2])) { # This is a totally bogus test. #if ($parts[1]!~ # /^((filter:)?(INPUT|OUTPUT|FORWARD)|nat:(PRE|POST)ROUTING)$/) #{ # warn "Ignoring $parts[0] on unknown chain $parts[1] " # . "(rules line $.).\n"; # next; #} $parts[0]=~s/^filter://; push(@{$conf{$parts[0]}->{$parts[1]}}, [@parts[2..$#parts]]); } else { warn "Skipping bogus line ($.):\n\t$_\n"; next; } debug; } for my $name (keys(%interface)) { if (grep(/^ignore$/, @{$interface{$name}->{'flags'}})) { debug "Skipping interface $name"; debug; next; } my $dir=new DirHandle "$config_dir/network-scripts" or die "Failed to open $config_dir/network-scripts: $!\n"; my @configs=("$config_dir/network-scripts/ifcfg-$name"); for my $file (grep(/^ifcfg-$name:\d+$/, $dir->read)) { push(@configs, "$config_dir/network-scripts/$file"); } for my $configfile (@configs) { debug "configfile: $configfile"; my %cfg=parseconfig($configfile, $name); next if (!%cfg); push(@{$interface{$name}->{'cfg'}}, \%cfg); for my $cfg (@{$interface{$name}->{'cfg'}}) { debug "interface $name"; debug " address " . $cfg->{'ipaddr'}; debug " netmask " . $cfg->{'netmask'}; debug " network " . $cfg->{'network'}; debug " broadcast " . $cfg->{'broadcast'}; } } debug; } eval { debug Dumper(\%interface); } if ($ENV{'DEBUG'}); generate_rules(%interface); #### # # Subs # sub reject_auth($$) { my ($in, $out)=@_; iptables "-A", label($in) . "-" . label($out), qw(-p tcp --dport 113 -j REJECT --reject-with tcp-reset); iptables "-A", label($in) . "-" . label($out), qw(-p udp --dport 113 -j REJECT --reject-with icmp-port-unreachable); } sub check_allowed($$) { my ($in, $out)=@_; debug "checking for allowed traffic to interface $out"; my @allow=grep /^allow=/, @{$interface{$out}->{'flags'}}; if (@allow) { my @open_port; for my $item (@allow) { debug "parsing '$item'"; $item=~s/^allow=//; next if (!$item); for my $ports (split /,/, $item) { # *FIXME* - probably should verify that source # & destination are valid-looking addresses my ($port, $proto, $from, $to, $interface)= $ports=~/^(.*?) # port (or protocol) (?:\/(.*?))? # protocol (can be omitted) (?::(.*?))? # source address (ditto) (?::(.*?))? # destination address (ditto) (?::(.*?))? # source interface $/x; next if ($interface and $interface ne $in); if (!$proto) { if ($port=~/^\d+$/) { warn "ambiguous allow '$port', ignoring...\n"; next; } my $found=0; for $proto ("tcp", "udp") { if (getservbyname($port, $proto)) { push(@open_port, [$port, $proto, $from, $to]); $found++; } } if (!$found) { if (getprotobyname($port)) { push(@open_port, [undef, $port, $from, $to]); } else { warn "ambiguous allow '$port', ignoring...\n"; next; } } } else { if ($port!~/^\d+$/ and !getservbyname($port, $proto)) { warn "invalid port/proto '$port/$proto', ignoring...\n"; next; } push(@open_port, [$port, $proto, $from, $to]); } } } for my $port (@open_port) { debug "allowing proto ", $port->[1], defined($port->[0]) ? ", port " . $port->[0] : "", defined($port->[2]) ? ", from " . $port->[2] : "", defined($port->[3]) ? ", to " . $port->[3] : ""; allow($in, $out, @{$port}); } } } sub allow($$@) { my ($in, $out, $port, $proto, $from, $to)=@_; iptables "-A", label($in) . "-" . label($out), "-p", $proto, $port ? ("--dport", $port) : (), $from ? ("-s", $from) : (), $to ? ("-d", $to) : (), qw(-j ACCEPT); } sub label($) { my ($name)=@_; if (my ($label)=grep /^label=/, @{$interface{$name}->{'flags'}}) { $label=~s/^label=(.*)$/$1/; if ($label) { return $label; } else { return $name; } } else { return $name; } } sub generate_rules(%) { my %interface=@_; begin_script; comment_long "Set default policy to DROP."; set_policy("DROP", "DROP", "DROP"); comment_block "Clear all current rules."; clear_all; comment_block "User-defined chains."; comment_long "Allow established connections."; new_chain("established"); iptables qw(-A established -m state --state), "ESTABLISHED,RELATED", qw(-j ACCEPT); comment_long "Filter ICMP traffic."; new_chain("icmp-filter"); iptables qw(-A icmp-filter -p icmp -j ACCEPT); for my $in (keys(%interface)) { next if (grep(/^ignore$/, @{$interface{$in}->{'flags'}})); for my $out (keys(%interface)) { next if ($in eq $out or grep(/^ignore$/, @{$interface{$out}->{'flags'}})); my $label=label($in) . "-" . label($out); comment_block(label($in) . " -> " . label($out)); new_chain($label); debug "insert($label)"; insert($label); if ($interface{$in}->{'type'} eq "out" and $interface{$out}->{'type'} eq "out") { drop($in, $out); } else { # Filter traffic to the network address. filter_to($in, $out, "network"); # Filter traffic to the broadcast address. filter_to($in, $out, "broadcast"); if (($interface{$in}->{'type'} eq "int" and grep /^trusted$/, @{$interface{$in}->{'flags'}}) or ($interface{$in}->{'type'} eq "dmz" and $interface{$out}->{'type'} eq "out" and grep /^trusted$/, @{$interface{$in}->{'flags'}})) { debug "append($label)"; append($label); &main::accept($in, $out); } else { check_allowed($in, $out); debug "append($label)"; append($label); icmpfilter($in, $out); established($in, $out); reject_auth($in, $out); drop($in, $out); } } } } for my $in (keys(%interface)) { next if (grep(/^ignore$/, @{$interface{$in}->{'flags'}})); comment_block("Filter traffic from " . label($in) . "."); new_chain(label($in)); debug "insert(" . label($in) . ")"; insert(label($in)); # *FIXME* - need to handle aliased interfaces on the same # subnet correctly (without adding additional rules) filter_from($in, "network"); filter_from($in, "broadcast"); for my $out (keys(%interface)) { next if (grep(/^ignore$/, @{$interface{$out}->{'flags'}})); if ($in eq $out) { iptables("-A", label($in), "-o", $out, "-j", "ACCEPT"); } else { iptables("-A", label($in), "-o", $out, "-j", label($in) . "-" . label($out)); } } debug "append(" . label($in) . ")"; append(label($in)); } comment_block("INPUT chain."); debug "insert(INPUT)"; insert("INPUT"); iptables qw(-A INPUT -j established); iptables qw(-A INPUT -i lo -j ACCEPT); for my $iface (keys(%interface)) { next if (grep /^ignore$/, @{$interface{$iface}->{'flags'}}); if (($interface{$iface}->{'type'} eq "int") && grep /^trusted$/, @{$interface{$iface}->{'flags'}}) { iptables("-A", "INPUT", "-i", $iface, "-j", "ACCEPT"); } } debug "append(INPUT)"; append("INPUT"); iptables qw(-A INPUT -p tcp --dport 113 -j REJECT --reject-with tcp-reset); iptables qw(-A INPUT -p udp --dport 113 -j REJECT), qw(--reject-with icmp-port-unreachable); iptables qw(-A INPUT -j icmp-filter); iptables qw(-A INPUT), @{$conf{'log'}}, "--log-prefix", "INPUT fall-through: " if (@{$conf{'log'}}); iptables qw(-A INPUT -j DROP); comment_block("OUTPUT chain."); debug "insert(OUTPUT)"; insert("OUTPUT"); debug "append(OUTPUT)"; append("OUTPUT"); iptables qw(-A OUTPUT -j ACCEPT); comment_block("FORWARD chain."); debug "insert(FORWARD)"; insert("FORWARD"); for my $in (keys(%interface)) { next if (grep /^ignore$/, @{$interface{$in}->{'flags'}}); iptables qw(-A FORWARD -i), $in, "-j", label($in); } debug "append(FORWARD)"; append("FORWARD"); iptables qw(-A FORWARD), @{$conf{'log'}}, "--log-prefix", "FORWARD fall-through: " if (@{$conf{'log'}}); iptables qw(-A FORWARD -j DROP); comment_block "POSTROUTING chain (nat table)"; debug "insert(nat:POSTROUTING)"; insert("nat:POSTROUTING"); for my $in (keys(%interface)) { next if (grep /^ignore$/, @{$interface{$in}->{'flags'}}); debug "checking interface $in"; if (grep /^nat$/, @{$interface{$in}->{'flags'}}) { debug "need to nat interface $in"; for my $out (keys(%interface)) { next if ($in eq $out or $interface{$out}->{'type'} ne "out" or grep /^ignore$/, @{$interface{$out}->{'flags'}}); debug "found interface $out to nat out"; for my $iface ($in, $out) { if (!defined($interface{$iface}->{'cfg'}) or !@{$interface{$iface}->{'cfg'}}) { warn "I have no configuration for interface $iface!\n"; return; } } # I can't see where there is any point to this warning. #if (@{$interface{$out}->{'cfg'}}>1) #{ # warn "interface $out has multiple configurations, " . # "using first address found.\n"; #} for my $cfg (@{$interface{$in}->{'cfg'}}) { # *FIXME* - need to handle aliased interfaces on the same # subnet correctly (without adding additional rules) iptables qw(-t nat -A POSTROUTING -o), $out, "-s", $cfg->{'network'} . "/" . $cfg->{'netmask'}, qw(-j SNAT --to), $interface{$out}->{'cfg'}->[0]->{'ipaddr'}; } } } } debug "append(nat:POSTROUTING)"; append("nat:POSTROUTING"); comment_block "PREROUTING chain (nat table)"; debug "insert(nat:PREROUTING)"; insert("nat:PREROUTING"); for my $in (keys(%interface)) { debug "PREROUTING: checking $in..."; next if ($interface{$in}->{'type'} ne "out" or grep /^ignore$/, @{$interface{$in}->{'flags'}}); for my $out (keys(%interface)) { debug "PREROUTING: checking $out -> $in..."; next if ($in eq $out or $interface{$out}->{'type'} eq "out" or grep /^ignore$/, @{$interface{$out}->{'flags'}} or !grep /^nat$/, @{$interface{$out}->{'flags'}}); debug "filtering traffic to $out from $in..."; for my $cfg (@{$interface{$out}->{'cfg'}}) { # *FIXME* - need to handle aliased interfaces on the same # subnet correctly (without adding additional rules) my @rule=(qw(-t nat -A PREROUTING -i), $in, "-d", $cfg->{'network'} . "/" . $cfg->{'netmask'}); iptables @rule, @{$conf{'log'}}, "--log-prefix", label($in) . " -> " . label($out) . " - bad dest: " if (@{$conf{'log'}}); iptables @rule, qw(-j DROP); } } } debug "append(nat:PREROUTING)"; append("nat:PREROUTING"); } sub icmpfilter($$) { my ($in, $out)=@_; iptables "-A", label($in) . "-" . label($out), qw(-j icmp-filter); } sub established($$) { my ($in, $out)=@_; iptables "-A", label($in) . "-" . label($out), qw(-j established); } sub drop($$) { my ($in, $out)=@_; iptables "-A", label($in) . "-" . label($out), @{$conf{'log'}}, "--log-prefix", label($in) . " -> " . label($out) . ": " if (@{$conf{'log'}}); iptables "-A", label($in) . "-" . label($out), qw(-j DROP); } sub accept($$) { my ($in, $out)=@_; iptables "-A", label($in) . "-" . label($out), qw(-j ACCEPT); } sub filter_from($$) { my ($in, $item)=@_; debug "$in, filtering from $item"; if (!defined($interface{$in}->{'cfg'}) or !@{$interface{$in}->{'cfg'}}) { warn "I have no configuration for interface $in!\n"; return; } comment "Filter traffic from the $item address(es)."; for my $cfg (@{$interface{$in}->{'cfg'}}) { my @rule=("-A", label($in), "-d", $cfg->{$item}); iptables @rule, @{$conf{'log'}}, "--log-prefix", label($in) . " - from $item: " if (@{$conf{'log'}}); iptables @rule, qw(-j DROP); } } sub filter_to($$$) { my ($in, $out, $item)=@_; debug "$in -> $out, filtering to $item"; if (!defined($interface{$out}->{'cfg'}) or !@{$interface{$out}->{'cfg'}}) { warn "I have no configuration for interface $out!\n"; return; } comment "Filter traffic to the $item address(es)."; for my $cfg (@{$interface{$out}->{'cfg'}}) { my @rule=("-A", label($in) . "-" . label($out), "-d", $cfg->{$item}); iptables @rule, @{$conf{'log'}}, "--log-prefix", label($in) . "-" . label($out) . " - to $item: " if (@{$conf{'log'}}); iptables @rule, qw(-j DROP); } } sub parseconfig($$) { my ($configfile, $name)=@_; my %cfg; if (my $config=new FileHandle "<$configfile") { while (<$config>) { chomp; s/(?> 8) if (system(@command) != 0); } else { my @bits=@_; for my $part (@bits) { if ($part=~/[^\w\-\.\/]/) { $part=~s/'/'\''/g; $part="'$part'"; } push @command, $part; } print "@command\n"; } } sub new_chain($) { iptables("-N", $_[0]); } sub insert($) { action('insert', $_[0]); } sub append($) { action('append', $_[0]); } sub action($$) { my ($action, $label)=@_; my $table=undef; my $chain; if ($label=~/^([^:]+):(.*)$/) { $table=$1; $chain=$2; } else { $chain=$label; } debug "$action in $chain" . (defined $table ? ", table '$table'" : ""); if (defined($conf{$action}) and defined($conf{$action}->{$label})) { my @rules=@{$conf{$action}->{$label}}; @rules=reverse @rules if ($action eq 'insert'); for my $rule (@rules) { debug "Adding rule '@{$rule}'..."; iptables $table ? ("-t", $table) : (), "-A", $chain, @{$rule}; } } } sub clear_all() { iptables "-F"; iptables "-X"; iptables qw(-t nat -F); iptables qw(-t nat -X); iptables qw(-t mangle -F); iptables qw(-t mangle -X); iptables "-Z"; } __END__ =head1 NAME genfw - Generate a firewall script =head1 SYNOPSIS B =head1 DESCRIPTION This script generates a firewall script based on a F file and the network configuration of a system. =head1 OPTIONS The F file should contain one line for each interface on the system. Like most standard Unix configuration files, C<\> is used to continue lines and C<#> is used to begin comments. =head2 Directives =over 4 =item C I An "internal" interface tends to be able to connect out, but nothing can connect in to it. =item C I Equivalent to C. =item C I A "dmz" interface tends allow connections from the outside world, but can't connect to "internal" interfaces, and can sometimes connect out to the world. =item C I An "outside" interface usually can't connect to "internal" interfaces but can connect to some things on "dmz" interfaces. Also, traffic is not allowed between "outside" interfaces. =item C I Equivalent to C. =item C I Equivalent to C. =item C [I:]I I This appends I to a I in a specified I
. If I
(and the colon (:) after it) is omitted, the filter table is assumed. For example, the following would redirect outgoing connections on port 80 to port 3128 (for transparent proxying with squid): append nat:PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to 3128 =item C Modifies generated rules so that dropped packets are not logged. =item C Dropped packets are logged, but rate-limited. This adds C<-m limit> to all C<-j LOG> lines. This is the default. =item C Modifies generated rules so that all dropped packets are logged. =back =head2 Flags Any of the interface definition lines can include any number of the following whitespace-separated options: =over 4 =item C Don't generate rules for this interface. =item CICI[C<:>I[C<:>I[C<:>I]]]>[C<,>...] Allow specific traffic to this interface. This option can take many including any of the following: =over 8 =item CI Checks for I/tcp and I/udp in F. Rules are generated for each one that is defined. An example might be C to allow traffic to UDP or TCP port 53. =item CI Checks for I in F. A rule is generated if the protocol is defined. For example, C. =item CICI Allows traffic to the specified I on the specified I. I can be numeric. (I may also be numeric, although this is untested at the moment.) Examples might be C or C. Both are equivalent. =back Any number of protocols, ports, etc. can be included after an C in a comma-separated list. Any of the above can also be followed by a colon-separated list including the source address, destination address, and source interface. Any of the three can be left blank. For example, C would allow ssh access from eth0, but nowhere else. =item CI