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
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
66 my %p1 = parse_table
(<<EOT);
68 i=interface o=outerface
69 in-interface=interface out-interface=outerface
71 source=saddr destination=daddr
73 reject-with icmp-type icmpv6-type
74 to-destination to-ports to
76 tcp-option mss set-mss
78 ulog-nlgroup ulog-prefix ulog-cprange ulog-qthreshold
81 ctstate ctproto ctorigsrc ctorigdst ctreplsrc ctrepldst ctstatus ctexpire
87 iplimit-above iplimit-mask
92 every counter start packet
93 uid-owner gid-owner pid-owner sid-owner cmd-owner
94 physdev-in physdev-out
96 psd-weight-threshold psd-delay-threshold
97 psd-lo-ports-weight psd-hi-ports-weight
100 name seconds hitcount
101 timestart timestop days datestart datestop
109 ttl-set ttl-dec ttl-inc
111 dir pol reqid spi proto mode tunnel-src tunnel-dst
116 set-dscp set-dscp-class
119 lower-limit upper-limit
122 rt-type rt-segsleft rt-len
125 hashlimit hashlimit-burst hashlimit-mode hashlimit-name
126 hashlimit-htable-size hashlimit-htable-max
127 hashlimit-htable-expire hashlimit-htable-gcinterval
128 connlimit-above connlimit-mask
129 connbytes connbytes-dir connbytes-mode
133 nflog-group nflog-prefix nflog-range nflog-threshold
136 my %p1c = parse_table
(<<EOT);
138 source-ports destination-ports ports
145 my %p1multi = parse_table
(<<EOT);
149 my %p2c = parse_table
(<<EOT);
155 my @pre_negated = qw(fragment connbytes connlimit-above
157 set rcheck update remove seconds hitcount
159 physdev-is-in physdev-is-out physdev-is-bridged
162 my @mix_negated = qw(
170 protocol saddr daddr interface outerface sport dport
171 ahspi ahlen condition connrate
172 ctorigsrc ctorigdst ctreplsrc ctrepldst
174 icmp-type icmpv6-type
175 mac-source source-ports destination-ports ports
176 genre realm chunk-types
180 dccp-types dccp-option
185 physdev-in physdev-out
186 rt-type rt-segsleft rt-len
190 sub is_netfilter_core_target
{
192 die unless defined $target and length $target;
194 return $target =~ /(?:ACCEPT|DROP|RETURN|QUEUE|NOP)$/;
197 sub is_netfilter_module_target
{
199 die unless defined $target and length $target;
201 return $target =~ /(?
:BALANCE
|CLASSIFY
|CLUSTERIP
|CONNMARK
202 |DNAT
|DSCP
|ECN
|LOG
|MARK
|MASQUERADE
203 |MIRROR
|NETMAP
|REDIRECT
|REJECT
|ROUTE
204 |SNAT
|TCPMSS
|TOS
|TRACE
|TTL
|ULOG
211 return $_ unless /[^-\w.:]/s;
217 return ferm_escape
($a) unless ref $a;
218 return ferm_escape
($a->[0]) if @
$a == 1;
219 return '(' . join(' ', @
$a) . ')';
223 # write a line of tokens, with indent handling
225 # don't add space before semicolon
226 my $comma = $_[-1] eq ';' ?
pop : '';
227 # begins with closing curly braces -> decrease indent
228 $indent -= 4 if $_[0] =~ /^}/;
233 # ends with opening curly braces -> increase indent
234 $indent += 4 if $_[-1] =~ /{$/;
237 sub module_match_count
{
238 my ($module, $rules) = @_;
241 last unless $_->{mod
}{$module};
249 return @
{$b->{match
}} > 0 &&
250 (Dumper
($a->{match
}[0]) eq Dumper
($b->{match
}[0]));
253 sub prefix_match_count
{
254 my ($prefix, $rules) = @_;
257 last unless prefix_matches
($prefix, $_);
263 sub is_merging_array_member
{
265 return defined $value &&
267 ref $value eq 'ARRAY');
270 sub array_matches
($$) {
271 my ($rule1, $rule2) = @_;
272 return unless is_merging_array_member
($rule1->{match
}[0][1]);
273 return unless is_merging_array_member
($rule2->{match
}[0][1]);
274 return unless @
{$rule2->{match
}} > 0;
275 return unless $rule1->{match
}[0][0] eq $rule2->{match
}[0][0];
278 $r1{match
} = [ @
{$r1{match
}} ];
279 $r2{match
} = [ @
{$r2{match
}} ];
282 return Dumper
(\
%r1) eq Dumper
(\
%r2);
285 sub array_match_count
($\@
) {
286 my ($first, $rules) = @_;
287 return 0 unless @
{$first->{match
}} > 0;
290 last unless array_matches
($first, $_);
299 # try to combine rules with arrays:
300 # saddr 1.2.3.4 proto tcp ACCEPT;
301 # saddr 5.6.7.8 proto tcp ACCEPT;
303 # saddr (1.2.3.4 5.6.7.8) proto tcp ACCEPT;
306 my $match_count = array_match_count
($rule, @_);
308 if ($match_count > 0) {
309 my $option = $rule->{match
}[0][0];
310 my @matching = ( $rule, splice(@_, 0, $match_count) );
312 (ref $_ and ref $_ eq 'ARRAY') ? @
$_ : $_
317 $rule->{match
}[0][1] = \
@params;
326 # try to find a common prefix and put rules in a block:
327 # saddr 1.2.3.4 proto tcp dport ssh ACCEPT;
328 # saddr 5.6.7.8 proto tcp dport ssh DROP;
330 # proto tcp dport ssh {
331 # saddr 1.2.3.4 ACCEPT;
332 # saddr 5.6.7.8 DROP;
336 if (@
{$rule->{match
}} > 0) {
337 my $match_count = prefix_match_count
($rule, \
@_);
339 if ($match_count > 0) {
340 my $match = $rule->{match
}[0];
341 my @matching = ( $rule, splice(@_, 0, $match_count) );
342 map { shift @
{$_->{match
}} } @matching;
344 push @result, { match
=> [ $match ],
345 block
=> [ optimize
(@matching) ],
355 # combine simple closures:
356 # proto tcp { dport http { LOG; ACCEPT; } }
358 # proto tcp dport http { LOG; ACCEPT; }
359 foreach my $rule (@result) {
360 next unless exists $rule->{block
} && @
{$rule->{block
}} == 1;
362 # a block with only one item can be merged
363 my $inner = $rule->{block
}[0];
364 delete $rule->{block
};
367 push @
{$rule->{match
}}, @
{$inner->{match
}};
368 $rule->{jump
} = $inner->{jump
}
369 if exists $inner->{jump
};
370 $rule->{goto} = $inner->{goto}
371 if exists $inner->{goto};
372 $rule->{block
} = $inner->{block
}
373 if exists $inner->{block
};
374 push @
{$rule->{target
} ||= []}, @
{$inner->{target
}}
375 if exists $inner->{target
};
382 my ($line, $key, $value) = @_;
384 if (ref($value) and ref($value) eq 'HASH' and
385 $value->{wrap
} eq 'pre-negation') {
387 $value = $value->{value
};
392 if (ref($value) and ref($value) eq 'HASH' and
393 $value->{wrap
} eq 'negation') {
395 $value = $value->{value
};
398 if (ref($value) and ref($value) eq 'HASH' and
399 $value->{wrap
} eq 'multi') {
400 foreach (@
{$value->{values}}) {
401 push @
$line, format_array
($_);
403 } elsif (defined $value) {
404 push @
$line, format_array
($value);
409 # optimize and write a list of rules
411 my @r = @_ ?
@_ : @rules;
413 foreach my $rule (@r) {
415 # assemble the line, match stuff first, then target parameters
416 if (exists $rule->{match
}) {
417 foreach (@
{$rule->{match
}}) {
418 flush_option
(\
@line, @
$_);
422 if (exists $rule->{jump
}) {
423 if (is_netfilter_core_target
($rule->{jump
}) ||
424 is_netfilter_module_target
($rule->{jump
})) {
425 push @line, $rule->{jump
};
427 flush_option
(\
@line, 'jump', $rule->{jump
});
429 } elsif (exists $rule->{goto}) {
430 flush_option
(\
@line, 'realgoto', $rule->{goto});
431 } elsif (not exists $rule->{block
}) {
435 if (exists $rule->{target
}) {
436 foreach (@
{$rule->{target
}}) {
437 flush_option
(\
@line, @
$_);
441 if (exists $rule->{block
}) {
442 # this rule begins a block created in &optimize
443 write_line
(@line, '{');
444 flush
(@
{$rule->{block
}});
448 write_line
(@line, ';');
456 write_line
'}' if defined $chain;
457 write_line
'}' if defined $table;
458 write_line
'}' if defined $domain;
468 while (s/^\s*"([^"]+)"//s || s/^\s*(!)// || s/^\s*(\S+)//s) {
474 sub wrap_pre_negated
{
475 my ($option, $negated_ref, $value) = @_;
476 return $value unless $negated_ref && $$negated_ref;
479 if (grep { $_ eq $option } @pre_negated) {
480 $wrap = 'pre-negation';
481 } elsif (grep { $_ eq $option } @mix_negated) {
485 die "option '$option' in line $. cannot be pre-negated\n"
486 unless defined $wrap;
490 return { wrap
=> $wrap,
496 my ($option, $tokens) = @_;
497 die "not enough arguments for option '$option' in line $."
502 sub fetch_token_comma
{
503 [ split(',', fetch_token
(@_)) ]
506 sub fetch_two_tokens_comma
{
507 return { wrap
=> 'multi',
508 values => [ fetch_token_comma
(@_),
509 fetch_token_comma
(@_) ],
514 my ($option, $tokens, $fetch) = (shift, shift, shift);
516 my $negated = @
$tokens > 0 && $tokens->[0] eq '!' && shift @
$tokens;
518 die "option '$option' in line $. cannot be negated\n"
519 if $negated and not grep { $_ eq $option } @negated;
521 my $value = &$fetch($option, $tokens, @_);
523 $value = { wrap
=> 'negation',
532 my ($line, $option, $pre_negated, $tokens) = @_;
534 my $cur = $line->{cur
};
535 die unless defined $cur;
537 $option = 'destination-ports' if $option eq 'dports';
538 $option = 'source-ports' if $option eq 'sports';
540 if ($option eq 'protocol') {
541 my $keyword = 'protocol';
542 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token
);
543 # protocol implicitly loads the module
544 unless (ref $param) {
545 my $mod = $param eq 'ipv6-icmp' ?
'icmp6' : $param;
546 delete $line->{mod
}{$mod};
548 $line->{proto
} = $param;
549 push @
$cur, [ 'proto', $param ];
550 } elsif ($option eq 'm') {
552 my $param = shift @
$tokens;
553 $line->{mod
}{$param} = 1;
554 # we don't need this module if the protocol with the
555 # same name is already specified
556 push @
$cur, [ 'mod', $param ]
557 unless exists $line->{proto
} and
558 ($line->{proto
} eq $param or
559 $line->{proto
} =~ /^(ipv6-icmp|icmpv6)$/s and $param eq 'icmp6');
560 } elsif ($option eq 'set' and exists $line->{mod
}{set
}) {
561 push @
$cur, [ $option, fetch_two_tokens_comma
($option, $tokens) ];
562 } elsif (exists $p0{$option}) {
563 my $keyword = $p0{$option};
564 push @
$cur, [ $keyword, wrap_pre_negated
($keyword, \
$pre_negated, undef) ];
565 } elsif (exists $p1{$option}) {
566 my $keyword = $p1{$option};
567 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token
);
568 my $value = wrap_pre_negated
($keyword, \
$pre_negated, $param);
569 unless ($keyword =~ /^[sd]addr$/ && $value eq '::/0') {
570 push @
$cur, [ $keyword, $value ];
572 } elsif (exists $p1c{$option}) {
573 my $keyword = $p1c{$option};
574 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token_comma
);
575 push @
$cur, [ $keyword, wrap_pre_negated
($keyword, \
$pre_negated, $param) ];
576 } elsif (exists $p1multi{$option}) {
577 my $keyword = $p1multi{$option};
579 my @v = grep { $_->[0] eq $keyword } @
$cur;
582 push @
$cur, [ $keyword, $values ];
586 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token
);
587 push @
$values, wrap_pre_negated
($option, \
$pre_negated, $param);
588 } elsif (exists $p2c{$option}) {
589 my $keyword = $p2c{$option};
590 my $param = wrap_negated
($keyword, $tokens, \
&fetch_two_tokens_comma
);
591 push @
$cur, [ $keyword, wrap_pre_negated
($keyword, \
$pre_negated, $param) ];
592 } elsif ($option eq 'j') {
594 my $target = shift @
$tokens;
595 # store the target in $line->{jump}
596 $line->{jump
} = $target;
597 # what now follows is target parameters; set $cur
599 $line->{cur
} = $line->{target
} = [];
600 } elsif ($option eq 'g') {
602 my $target = shift @
$tokens;
603 # store the target in $line->{jump}
604 $line->{goto} = $target;
606 die "option '$option' in line $. not understood\n";
609 die "option '$option' in line $. cannot be negated\n"
613 if (grep { $_ eq '-h' || $_ eq '--help' } @ARGV) {
615 Pod
::Usage
::pod2usage
(-exitstatus
=> 0,
619 if (@ARGV == 0 && -t STDIN
) {
620 open STDIN
, "/sbin/iptables-save|"
621 or die "Failed run to /sbin/iptables-save: $!";
622 } elsif (grep { /^-./ } @ARGV) {
624 Pod
::Usage
::pod2usage
(-exitstatus
=> 1,
628 print "# ferm rules generated by import-ferm\n";
629 print "# http://ferm.foo-projects.org/\n";
631 $next_domain = $ENV{FERM_DOMAIN
} || 'ip';
639 $next_domain = $1 if /^#.*\b(ip|ip6)tables(?:-save)\b/;
640 } elsif (/^\*(\w+)$/) {
643 if (keys %policies > 0) {
644 while (my ($chain, $policy) = each %policies) {
645 write_line
('chain', $chain, 'policy', $policy, ';');
650 unless (defined $domain and $domain eq $next_domain) {
652 $domain = $next_domain;
653 write_line
'domain', $domain, '{';
656 write_line
('}') if defined $table;
658 write_line
('table', $table, '{');
659 } elsif (/^:(\S+)\s+-\s+/) {
661 die unless defined $table;
662 write_line
("chain $1;");
663 } elsif (/^:(\S+)\s+(\w+)\s+/) {
665 die unless defined $table;
667 } elsif (s/^-A (\S+)\s+//) {
669 unless (defined $chain) {
672 write_line
('chain', $chain, '{');
673 } elsif ($1 ne $chain) {
677 write_line
('chain', $chain, '{');
680 if (exists $policies{$chain}) {
681 write_line
('policy', $policies{$chain}, ';');
682 delete $policies{$chain};
685 my @tokens = tokenize
($_);
688 # separate 'match' parameters from 'targe't parameters; $cur
689 # points to the current position
690 $line{cur
} = $line{match
} = [];
692 local $_ = shift @tokens;
693 if (/^-(\w)$/ || /^--(\S+)$/) {
694 parse_option
(\
%line, $1, undef, \
@tokens);
695 } elsif ($_ eq '!') {
698 /^-(\w)$/ || /^--(\S+)$/
699 or die "option expected in line $.\n";
700 parse_option
(\
%line, $1, 1, \
@tokens);
702 print STDERR
"warning: unknown token '$_' in line $.\n";
707 } elsif ($_ =~ /^COMMIT/) {
710 if (defined $chain) {
715 print STDERR
"line $. was not understood, ignoring it\n";
719 if (keys %policies > 0) {
720 while (my ($chain, $policy) = each %policies) {
721 write_line
('chain', $chain, 'policy', $policy, ';');
725 flush_domain
if defined $domain;
727 die unless $indent == 0;
733 import-ferm - import existing firewall rules into ferm
737 B<import-ferm> > ferm.conf
739 iptables-save | B<import-ferm> > ferm.conf
741 B<import-ferm> I<inputfile> > ferm.conf
745 This script helps you with porting an existing IPv4 firewall
746 configuration to ferm. It reads a file generated with
747 B<iptables-save>, and tries to suggest a ferm configuration file.
749 If no input file was specified on the command line, B<import-ferm>
750 runs F<iptables-save>.
754 iptables-save older than 1.3 is unable to write valid saves - this is
755 not a bug in B<import-ferm>.