4 # ferm, a firewall setup program that makes firewall rules easy!
6 # Copyright (C) 2001-2007 Auke Kok, Max Kellermann
8 # Comments, questions, greetings and additions to this program
9 # may be sent to <ferm@foo-projects.org>
12 # This tool allows you to import an existing firewall configuration
16 # This program is free software; you can redistribute it and/or modify
17 # it under the terms of the GNU General Public License as published by
18 # the Free Software Foundation; either version 2 of the License, or
19 # (at your option) any later version.
21 # This program is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 # GNU General Public License for more details.
26 # You should have received a copy of the GNU General Public License
27 # along with this program; if not, write to the Free Software
28 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
37 use vars
qw($indent $table $chain @rules $domain $next_domain);
41 /(.+)=(.+)/ ? ( $1 => $2 ) : ( $_ => $_ )
42 } split(/\s+/s, shift);
45 my %p0 = parse_table(<<EOT);
49 ecn-tcp-cwr ecn-tcp-ece
50 physdev-is-in physdev-is-out physdev-is-bridged
51 set rcheck update remove rttl rsource rdest
52 save-mark restore-mark
53 log-tcp-sequence log-tcp-options log-ip-options log-uid
56 fragres fragfirst fragmore fraglast
61 rt-0-res rt-0-not-strict
65 ssrr lsrr no-srr rr ts ra any-opt
69 my %p1 = parse_table
(<<EOT);
71 i=interface o=outerface
72 in-interface=interface out-interface=outerface
74 source=saddr destination=daddr
76 reject-with icmp-type icmpv6-type
79 tcp-option mss set-mss
81 ulog-nlgroup ulog-prefix ulog-cprange ulog-qthreshold
84 ctstate ctproto ctorigsrc ctorigdst ctreplsrc ctrepldst ctstatus ctexpire
90 iplimit-above iplimit-mask
95 every counter start packet
96 uid-owner gid-owner pid-owner sid-owner cmd-owner
97 physdev-in physdev-out
99 psd-weight-threshold psd-delay-threshold
100 psd-lo-ports-weight psd-hi-ports-weight
103 name seconds hitcount
104 timestart timestop days datestart datestop
108 set-mark set-xmark and-mark or-mark xor-mark mask
112 ttl-set ttl-dec ttl-inc
114 dir pol reqid spi proto mode tunnel-src tunnel-dst
119 set-dscp set-dscp-class
122 lower-limit upper-limit
125 rt-type rt-segsleft rt-len
128 hashlimit hashlimit-burst hashlimit-mode hashlimit-name
129 hashlimit-htable-size hashlimit-htable-max
130 hashlimit-htable-expire hashlimit-htable-gcinterval
131 connlimit-above connlimit-mask
132 connbytes connbytes-dir connbytes-mode
136 nflog-group nflog-prefix nflog-range nflog-threshold
137 hashmode clustermac total-nodes local-node hash-init
138 algo from string hex-string
139 and-tos or-tos xor-tos
142 my %p1c = parse_table
(<<EOT);
144 source-ports destination-ports ports
152 my %p1multi = parse_table
(<<EOT);
158 my %p2c = parse_table
(<<EOT);
164 my @pre_negated = qw(fragment connbytes connlimit-above
166 set rcheck update remove seconds hitcount
168 physdev-is-in physdev-is-out physdev-is-bridged
171 my @mix_negated = qw(
179 protocol saddr daddr interface outerface sport dport
180 ahspi ahlen condition connrate
181 ctorigsrc ctorigdst ctreplsrc ctrepldst
183 icmp-type icmpv6-type
184 mac-source source-ports destination-ports ports
185 genre realm chunk-types
189 dccp-types dccp-option
194 physdev-in physdev-out
195 rt-type rt-segsleft rt-len
199 sub is_netfilter_core_target
{
201 die unless defined $target and length $target;
203 return $target =~ /(?:ACCEPT|DROP|RETURN|QUEUE|NOP)$/;
206 sub is_netfilter_module_target
{
208 die unless defined $target and length $target;
210 return $target =~ /(?
:BALANCE
|CLASSIFY
|CLUSTERIP
|CONNMARK
211 |DNAT
|DSCP
|ECN
|LOG
|MARK
|MASQUERADE
212 |MIRROR
|NETMAP
|REDIRECT
|REJECT
|ROUTE
213 |SNAT
|TCPMSS
|TOS
|TRACE
|TTL
|ULOG
220 return $_ unless /[^-\w.:]/s;
226 return ferm_escape
($a) unless ref $a;
227 return ferm_escape
($a->[0]) if @
$a == 1;
228 return '(' . join(' ', map { ferm_escape
($_) } @
$a) . ')';
232 # write a line of tokens, with indent handling
234 # don't add space before semicolon
235 my $comma = $_[-1] eq ';' ?
pop : '';
236 # begins with closing curly braces -> decrease indent
237 $indent -= 4 if $_[0] =~ /^}/;
242 # ends with opening curly braces -> increase indent
243 $indent += 4 if $_[-1] =~ /{$/;
246 sub module_match_count
{
247 my ($module, $rules) = @_;
250 last unless $_->{mod
}{$module};
258 return @
{$b->{match
}} > 0 &&
259 (Dumper
($a->{match
}[0]) eq Dumper
($b->{match
}[0]));
262 sub prefix_match_count
{
263 my ($prefix, $rules) = @_;
266 last unless prefix_matches
($prefix, $_);
272 sub is_merging_array_member
{
274 return defined $value &&
276 ref $value eq 'ARRAY');
279 sub array_matches
($$) {
280 my ($rule1, $rule2) = @_;
281 return unless is_merging_array_member
($rule1->{match
}[0][1]);
282 return unless is_merging_array_member
($rule2->{match
}[0][1]);
283 return unless @
{$rule2->{match
}} > 0;
284 return unless $rule1->{match
}[0][0] eq $rule2->{match
}[0][0];
287 $r1{match
} = [ @
{$r1{match
}} ];
288 $r2{match
} = [ @
{$r2{match
}} ];
291 return Dumper
(\
%r1) eq Dumper
(\
%r2);
294 sub array_match_count
($\@
) {
295 my ($first, $rules) = @_;
296 return 0 unless @
{$first->{match
}} > 0;
299 last unless array_matches
($first, $_);
308 # try to combine rules with arrays:
309 # saddr 1.2.3.4 proto tcp ACCEPT;
310 # saddr 5.6.7.8 proto tcp ACCEPT;
312 # saddr (1.2.3.4 5.6.7.8) proto tcp ACCEPT;
315 my $match_count = array_match_count
($rule, @_);
317 if ($match_count > 0) {
318 my $option = $rule->{match
}[0][0];
319 my @matching = ( $rule, splice(@_, 0, $match_count) );
321 (ref $_ and ref $_ eq 'ARRAY') ? @
$_ : $_
326 $rule->{match
}[0][1] = \
@params;
335 # try to find a common prefix and put rules in a block:
336 # saddr 1.2.3.4 proto tcp dport ssh ACCEPT;
337 # saddr 5.6.7.8 proto tcp dport ssh DROP;
339 # proto tcp dport ssh {
340 # saddr 1.2.3.4 ACCEPT;
341 # saddr 5.6.7.8 DROP;
345 if (@
{$rule->{match
}} > 0) {
346 my $match_count = prefix_match_count
($rule, \
@_);
348 if ($match_count > 0) {
349 my $match = $rule->{match
}[0];
350 my @matching = ( $rule, splice(@_, 0, $match_count) );
351 map { shift @
{$_->{match
}} } @matching;
353 push @result, { match
=> [ $match ],
354 block
=> [ optimize
(@matching) ],
364 # combine simple closures:
365 # proto tcp { dport http { LOG; ACCEPT; } }
367 # proto tcp dport http { LOG; ACCEPT; }
368 foreach my $rule (@result) {
369 next unless exists $rule->{block
} && @
{$rule->{block
}} == 1;
371 # a block with only one item can be merged
372 my $inner = $rule->{block
}[0];
373 delete $rule->{block
};
376 push @
{$rule->{match
}}, @
{$inner->{match
}};
377 $rule->{jump
} = $inner->{jump
}
378 if exists $inner->{jump
};
379 $rule->{goto} = $inner->{goto}
380 if exists $inner->{goto};
381 $rule->{block
} = $inner->{block
}
382 if exists $inner->{block
};
383 push @
{$rule->{target
} ||= []}, @
{$inner->{target
}}
384 if exists $inner->{target
};
391 my ($line, $key, $value) = @_;
393 if (ref($value) and ref($value) eq 'HASH' and
394 $value->{wrap
} eq 'pre-negation') {
396 $value = $value->{value
};
401 if (ref($value) and ref($value) eq 'HASH' and
402 $value->{wrap
} eq 'negation') {
404 $value = $value->{value
};
407 if (ref($value) and ref($value) eq 'HASH' and
408 $value->{wrap
} eq 'multi') {
409 foreach (@
{$value->{values}}) {
410 push @
$line, format_array
($_);
412 } elsif (defined $value) {
413 push @
$line, format_array
($value);
418 # optimize and write a list of rules
420 my @r = @_ ?
@_ : @rules;
422 foreach my $rule (@r) {
424 # assemble the line, match stuff first, then target parameters
425 if (exists $rule->{match
}) {
426 foreach (@
{$rule->{match
}}) {
427 flush_option
(\
@line, @
$_);
431 if (exists $rule->{jump
}) {
432 if (is_netfilter_core_target
($rule->{jump
}) ||
433 is_netfilter_module_target
($rule->{jump
})) {
434 push @line, $rule->{jump
};
436 flush_option
(\
@line, 'jump', $rule->{jump
});
438 } elsif (exists $rule->{goto}) {
439 flush_option
(\
@line, 'realgoto', $rule->{goto});
440 } elsif (not exists $rule->{block
}) {
444 if (exists $rule->{target
}) {
445 foreach (@
{$rule->{target
}}) {
446 flush_option
(\
@line, @
$_);
450 if (exists $rule->{block
}) {
451 # this rule begins a block created in &optimize
452 write_line
(@line, '{');
453 flush
(@
{$rule->{block
}});
457 write_line
(@line, ';');
465 write_line
'}' if defined $chain;
466 write_line
'}' if defined $table;
467 write_line
'}' if defined $domain;
477 while (s/^\s*"([^"]+)"//s || s/^\s*(!)// || s/^\s*(\S+)//s) {
483 sub wrap_pre_negated
{
484 my ($option, $negated_ref, $value) = @_;
485 return $value unless $negated_ref && $$negated_ref;
488 if (grep { $_ eq $option } @pre_negated) {
489 $wrap = 'pre-negation';
490 } elsif (grep { $_ eq $option } @mix_negated) {
494 die "option '$option' in line $. cannot be pre-negated\n"
495 unless defined $wrap;
499 return { wrap
=> $wrap,
505 my ($option, $tokens) = @_;
506 die "not enough arguments for option '$option' in line $."
511 sub fetch_token_comma
{
512 [ split(',', fetch_token
(@_)) ]
515 sub fetch_two_tokens_comma
{
516 return { wrap
=> 'multi',
517 values => [ fetch_token_comma
(@_),
518 fetch_token_comma
(@_) ],
523 my ($option, $tokens, $fetch) = (shift, shift, shift);
525 my $negated = @
$tokens > 0 && $tokens->[0] eq '!' && shift @
$tokens;
527 die "option '$option' in line $. cannot be negated\n"
528 if $negated and not grep { $_ eq $option } @negated;
530 my $value = &$fetch($option, $tokens, @_);
532 $value = { wrap
=> 'negation',
541 my ($line, $option, $pre_negated, $tokens) = @_;
543 my $cur = $line->{cur
};
544 die unless defined $cur;
546 $option = 'destination-ports' if $option eq 'dports';
547 $option = 'source-ports' if $option eq 'sports';
549 if ($option eq 'protocol') {
550 my $keyword = 'protocol';
551 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token
);
552 # protocol implicitly loads the module
553 unless (ref $param) {
554 my $mod = $param eq 'ipv6-icmp' ?
'icmp6' : $param;
555 delete $line->{mod
}{$mod};
557 $line->{proto
} = $param;
558 push @
$cur, [ 'proto', $param ];
559 } elsif ($option eq 'm') {
561 my $param = shift @
$tokens;
562 $line->{mod
}{$param} = 1;
563 # we don't need this module if the protocol with the
564 # same name is already specified
565 push @
$cur, [ 'mod', $param ]
566 unless exists $line->{proto
} and
567 ($line->{proto
} eq $param or
568 $line->{proto
} =~ /^(ipv6-icmp|icmpv6)$/s and $param eq 'icmp6');
569 } elsif ($option eq 'set' and exists $line->{mod
}{set
}) {
570 push @
$cur, [ $option, fetch_two_tokens_comma
($option, $tokens) ];
571 } elsif (exists $p0{$option}) {
572 my $keyword = $p0{$option};
573 push @
$cur, [ $keyword, wrap_pre_negated
($keyword, \
$pre_negated, undef) ];
574 } elsif (exists $p1{$option}) {
575 my $keyword = $p1{$option};
576 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token
);
577 my $value = wrap_pre_negated
($keyword, \
$pre_negated, $param);
578 unless ($keyword =~ /^[sd]addr$/ && $value eq '::/0') {
579 push @
$cur, [ $keyword, $value ];
581 } elsif (exists $p1c{$option}) {
582 my $keyword = $p1c{$option};
583 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token_comma
);
584 push @
$cur, [ $keyword, wrap_pre_negated
($keyword, \
$pre_negated, $param) ];
585 } elsif (exists $p1multi{$option}) {
586 my $keyword = $p1multi{$option};
588 my @v = grep { $_->[0] eq $keyword } @
$cur;
591 push @
$cur, [ $keyword, $values ];
595 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token
);
596 push @
$values, wrap_pre_negated
($option, \
$pre_negated, $param);
597 } elsif (exists $p2c{$option}) {
598 my $keyword = $p2c{$option};
599 my $param = wrap_negated
($keyword, $tokens, \
&fetch_two_tokens_comma
);
600 push @
$cur, [ $keyword, wrap_pre_negated
($keyword, \
$pre_negated, $param) ];
601 } elsif ($option eq 'j') {
603 my $target = shift @
$tokens;
604 # store the target in $line->{jump}
605 $line->{jump
} = $target;
606 # what now follows is target parameters; set $cur
608 $line->{cur
} = $line->{target
} = [];
609 } elsif ($option eq 'g') {
611 my $target = shift @
$tokens;
612 # store the target in $line->{jump}
613 $line->{goto} = $target;
615 die "option '$option' in line $. not understood\n";
618 die "option '$option' in line $. cannot be negated\n"
622 if (grep { $_ eq '-h' || $_ eq '--help' } @ARGV) {
624 Pod
::Usage
::pod2usage
(-exitstatus
=> 0,
628 if (@ARGV == 0 && -t STDIN
) {
629 open STDIN
, "/sbin/iptables-save|"
630 or die "Failed run to /sbin/iptables-save: $!";
631 } elsif (grep { /^-./ } @ARGV) {
633 Pod
::Usage
::pod2usage
(-exitstatus
=> 1,
637 print "# ferm rules generated by import-ferm\n";
638 print "# http://ferm.foo-projects.org/\n";
640 $next_domain = $ENV{FERM_DOMAIN
} || 'ip';
648 $next_domain = $1 if /^#.*\b(ip|ip6)tables(?:-save)\b/;
649 } elsif (/^\*(\w+)$/) {
652 if (keys %policies > 0) {
653 while (my ($chain, $policy) = each %policies) {
654 write_line
('chain', $chain, 'policy', $policy, ';');
659 unless (defined $domain and $domain eq $next_domain) {
661 $domain = $next_domain;
662 write_line
'domain', $domain, '{';
665 write_line
('}') if defined $table;
667 write_line
('table', $table, '{');
668 } elsif (/^:(\S+)\s+-\s+/) {
670 die unless defined $table;
671 write_line
("chain $1;");
672 } elsif (/^:(\S+)\s+(\w+)\s+/) {
674 die unless defined $table;
676 } elsif (s/^-A (\S+)\s+//) {
678 unless (defined $chain) {
681 write_line
('chain', $chain, '{');
682 } elsif ($1 ne $chain) {
686 write_line
('chain', $chain, '{');
689 if (exists $policies{$chain}) {
690 write_line
('policy', $policies{$chain}, ';');
691 delete $policies{$chain};
694 my @tokens = tokenize
($_);
697 # separate 'match' parameters from 'targe't parameters; $cur
698 # points to the current position
699 $line{cur
} = $line{match
} = [];
701 local $_ = shift @tokens;
702 if (/^-(\w)$/ || /^--(\S+)$/) {
703 parse_option
(\
%line, $1, undef, \
@tokens);
704 } elsif ($_ eq '!') {
707 /^-(\w)$/ || /^--(\S+)$/
708 or die "option expected in line $.\n";
709 parse_option
(\
%line, $1, 1, \
@tokens);
711 print STDERR
"warning: unknown token '$_' in line $.\n";
716 } elsif ($_ =~ /^COMMIT/) {
719 if (defined $chain) {
724 print STDERR
"line $. was not understood, ignoring it\n";
728 if (keys %policies > 0) {
729 while (my ($chain, $policy) = each %policies) {
730 write_line
('chain', $chain, 'policy', $policy, ';');
734 flush_domain
if defined $domain;
736 die unless $indent == 0;
742 import-ferm - import existing firewall rules into ferm
746 B<import-ferm> > ferm.conf
748 iptables-save | B<import-ferm> > ferm.conf
750 B<import-ferm> I<inputfile> > ferm.conf
754 This script helps you with porting an existing IPv4 firewall
755 configuration to ferm. It reads a file generated with
756 B<iptables-save>, and tries to suggest a ferm configuration file.
758 If no input file was specified on the command line, B<import-ferm>
759 runs F<iptables-save>.
763 iptables-save older than 1.3 is unable to write valid saves - this is
764 not a bug in B<import-ferm>.