4 # ferm, a firewall setup program that makes firewall rules easy!
6 # Copyright (C) 2001-2006 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);
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 my %p1 = parse_table
(<<EOT);
62 i=interface o=outerface
65 reject-with icmp-type icmpv6-type
66 to-destination to-ports to
68 tcp-option mss set-mss
70 ulog-nlgroup ulog-prefix ulog-cprange ulog-qthreshold
73 ctstate ctproto ctorigsrc ctorigdst ctreplsrc ctrepldst ctstatus ctexpire
79 iplimit-above iplimit-mask
84 every counter start packet
85 uid-owner gid-owner pid-owner sid-owner cmd-owner
86 physdev-in physdev-out
88 psd-weight-threshold psd-delay-threshold
89 psd-lo-ports-weight psd-hi-ports-weight
93 timestart timestop days datestart datestop
101 ttl-set ttl-dec ttl-inc
103 dir pol reqid spi proto mode tunnel-src tunnel-dst
108 set-dscp set-dscp-class
111 my %p1c = parse_table
(<<EOT);
113 source-ports destination-ports ports
118 my %p1multi = parse_table
(<<EOT);
122 my %p2c = parse_table
(<<EOT);
127 my @pre_negated = qw(fragment connbytes connlimit-above
129 set rcheck update remove seconds hitcount
133 my @mix_negated = qw(
140 protocol saddr daddr interface outerface sport dport
141 ahspi condition connrate
142 ctorigsrc ctorigdst ctreplsrc ctrepldst
144 icmp-type icmpv6-type
145 mac-source source-ports destination-ports ports
146 genre realm chunk-types
150 dccp-types dccp-option
153 sub is_netfilter_core_target
{
155 die unless defined $target and length $target;
157 return $target =~ /(?:ACCEPT|DROP|RETURN|QUEUE|NOP)$/;
160 sub is_netfilter_module_target
{
162 die unless defined $target and length $target;
164 return $target =~ /(?
:BALANCE
|CLASSIFY
|CLUSTERIP
|CONNMARK
165 |DNAT
|DSCP
|ECN
|LOG
|MARK
|MASQUERADE
166 |MIRROR
|NETMAP
|REDIRECT
|REJECT
|ROUTE
167 |SNAT
|TCPMSS
|TOS
|TRACE
|TTL
|ULOG
174 return $_ unless /[^-\w.:]/s;
180 return ferm_escape
($a) unless ref $a;
181 return ferm_escape
($a->[0]) if @
$a == 1;
182 return '(' . join(' ', @
$a) . ')';
186 # write a line of tokens, with indent handling
188 # don't add space before semicolon
189 my $comma = $_[-1] eq ';' ?
pop : '';
190 # begins with closing curly braces -> decrease indent
191 $indent -= 4 if $_[0] =~ /^}/;
196 # ends with opening curly braces -> increase indent
197 $indent += 4 if $_[-1] =~ /{$/;
200 sub module_match_count
{
201 my ($module, $rules) = @_;
204 last unless $_->{mod
}{$module};
211 my ($prefix, $rule) = @_;
212 return unless exists $rule->{match
};
213 while (my ($key, $value) = each %$prefix) {
214 return unless exists $rule->{match
}{$key}
215 and Dumper
($rule->{match
}{$key}) eq Dumper
($value);
220 sub prefix_match_count
{
221 my ($prefix, $rules) = @_;
224 last unless prefix_matches
($prefix, $_);
230 sub is_merging_array_member
{
232 return defined $value &&
234 ref $value eq 'ARRAY');
238 my ($key, $rule1, $rule2) = @_;
239 return unless is_merging_array_member
($rule1->{match
}{$key});
240 return unless is_merging_array_member
($rule2->{match
}{$key});
243 $r1{match
} = {%{$r1{match
}}};
244 $r2{match
} = {%{$r2{match
}}};
245 delete $r1{match
}{$key};
246 delete $r2{match
}{$key};
247 return Dumper
(\
%r1) eq Dumper
(\
%r2);
250 sub array_match_count
{
251 my ($key, $first, $rules) = @_;
254 last unless array_matches
($key, $first, $_);
263 # try to combine rules with arrays:
264 # saddr 1.2.3.4 proto tcp ACCEPT;
265 # saddr 5.6.7.8 proto tcp ACCEPT;
267 # saddr (1.2.3.4 5.6.7.8) proto tcp ACCEPT;
270 if (exists $rule->{match
}) {
273 my %match_copy = %{$rule->{match
}};
274 while (my ($key, $value) = each %match_copy) {
275 next unless defined $value;
276 next if ref $value and ref $value eq 'HASH';
277 my $match_count2 = array_match_count
($key, $rule, \
@_);
278 if ($match_count2 > $match_count) {
280 $match_count = $match_count2;
284 if ($match_count > 0) {
286 my $value = $_->{match
}{$match_key};
287 ref $value ? @
$value : $value;
288 } ($rule, splice(@_, 0, $match_count));
289 $rule->{match
}{$match_key} = \
@values;
302 # try to find a common prefix for modules
303 # mod state state INVALID DROP;
304 # mod state state (ESTABLISHED RELATED) ACCEPT;
307 # state INVALID DROP;
308 # state (ESTABLISHED RELATED) ACCEPT;
312 if (exists $rule->{mod
}) {
315 foreach my $module (keys %{$rule->{mod
}}) {
316 my $match_count2 = module_match_count
($module, \
@_);
317 if ($match_count2 > $match_count) {
318 $match_module = $module;
319 $match_count = $match_count2;
322 if ($match_count > 0) {
324 delete $_->{mod
}{$match_module};
326 } ($rule, splice(@_, 0, $match_count));
327 push @result, { mod
=> { $match_module => 1 },
328 block
=> [ optimize
(@block) ],
341 # try to find a common prefix and put rules in a block:
342 # saddr 1.2.3.4 proto tcp dport ssh ACCEPT;
343 # saddr 5.6.7.8 proto tcp dport ssh DROP;
345 # proto tcp dport ssh {
346 # saddr 1.2.3.4 ACCEPT;
347 # saddr 5.6.7.8 DROP;
351 if (exists $rule->{match
}) {
354 while (my ($key, $value) = each %{$rule->{match
}}) {
355 my $prefix2 = { $key => $value };
356 my $match_count2 = prefix_match_count
($prefix2, \
@_);
357 if ($match_count2 > $match_count) {
359 $match_count = $match_count2;
362 if ($match_count > 0) {
364 foreach my $key (keys %prefix) {
365 delete $_->{match
}{$key};
367 #delete @_->{match}{keys %prefix};
369 } ($rule, splice(@_, 0, $match_count));
370 push @result, { match
=> \
%prefix,
371 block
=> [ optimize
(@block) ]
381 # combine simple closures:
382 # proto tcp { dport http { LOG; ACCEPT; } }
384 # proto tcp dport http { LOG; ACCEPT; }
385 foreach my $rule (@result) {
386 next unless exists $rule->{block
} && @
{$rule->{block
}} == 1;
388 # a block with only one item can be merged
389 my $inner = $rule->{block
}[0];
390 delete $rule->{block
};
395 foreach (qw(match jump target)) {
396 next unless exists $inner->{$_};
397 while (my ($key, $value) = each %{$inner->{$_}}) {
398 $rule->{$_}{$key} = $value;
403 # inherit everything else
404 while (my ($key, $value) = each %$inner) {
405 $rule->{$key} = $value;
413 my ($line, $key, $value) = @_;
415 if (ref($value) and ref($value) eq 'HASH' and
416 $value->{wrap
} eq 'pre-negation') {
418 $value = $value->{value
};
423 if (ref($value) and ref($value) eq 'HASH' and
424 $value->{wrap
} eq 'negation') {
426 $value = $value->{value
};
429 if (ref($value) and ref($value) eq 'HASH' and
430 $value->{wrap
} eq 'multi') {
431 foreach (@
{$value->{values}}) {
432 push @
$line, format_array
($_);
434 } elsif (defined $value) {
435 push @
$line, format_array
($value);
440 # optimize and write a list of rules
442 my @r = @_ ?
@_ : @rules;
444 foreach my $rule (@r) {
446 # assemble the line, match stuff first, then target parameters
447 foreach my $mod (keys %{$rule->{mod
} || {}}) {
448 push @line, 'mod', $mod;
451 if ($rule->{match
}{proto
}) {
452 flush_option
(\
@line, 'proto', $rule->{match
}{proto
});
453 delete $rule->{match
}{proto
};
456 foreach (qw(match jump target)) {
457 unless (exists $rule->{$_}) {
458 push @line, 'NOP' if $_ eq 'jump';
462 while (my ($key, $value) = each %{$rule->{$_}}) {
463 flush_option
(\
@line, $key, $value);
467 if (exists $rule->{block
}) {
468 # this rule begins a block created in &optimize
469 write_line
(@line, '{');
470 flush
(@
{$rule->{block
}});
474 write_line
(@line, ';');
483 while (s/^\s*"([^"]+)"//s || s/^\s*(!)// || s/^\s*(\S+)//s) {
489 sub wrap_pre_negated
{
490 my ($option, $negated_ref, $value) = @_;
491 return $value unless $negated_ref && $$negated_ref;
494 if (grep { $_ eq $option } @pre_negated) {
495 $wrap = 'pre-negation';
496 } elsif (grep { $_ eq $option } @mix_negated) {
500 die "option '$option' in line $. cannot be pre-negated\n"
501 unless defined $wrap;
505 return { wrap
=> $wrap,
511 my ($option, $tokens) = @_;
512 die "not enough arguments for option '$option' in line $."
517 sub fetch_token_comma
{
518 [ split(',', fetch_token
(@_)) ]
521 sub fetch_two_tokens_comma
{
522 return { wrap
=> 'multi',
523 values => [ fetch_token_comma
(@_),
524 fetch_token_comma
(@_) ],
529 my ($option, $tokens, $fetch) = (shift, shift, shift);
531 my $negated = @
$tokens > 0 && $tokens->[0] eq '!' && shift @
$tokens;
533 die "option '$option' in line $. cannot be negated\n"
534 if $negated and not grep { $_ eq $option } @negated;
536 my $value = &$fetch($option, $tokens, @_);
538 $value = { wrap
=> 'negation',
547 my ($line, $option, $pre_negated, $tokens) = @_;
549 my $cur = $line->{cur
};
550 die unless defined $cur;
552 if ($option eq 'p') {
553 my $keyword = 'protocol';
554 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token
);
555 # protocol implicitly loads the module
556 unless (ref $param) {
557 my $mod = $param eq 'ipv6-icmp' ?
'icmpv6' : $param;
558 delete $line->{mod
}{$mod};
560 $cur->{proto
} = $param;
561 } elsif ($option eq 'm') {
563 my $param = shift @
$tokens;
564 # we don't need this module if the protocol with the
565 # same name is already specified
566 $line->{mod
}{$param} = 1
567 unless exists $cur->{proto
} and
568 ($cur->{proto
} eq $param or
569 $cur->{proto
} eq 'ipv6-icmp' and $param eq 'icmpv6');
570 } elsif (exists $p0{$option}) {
571 my $keyword = $p0{$option};
572 $cur->{$keyword} = wrap_pre_negated
($keyword, \
$pre_negated, undef);
573 } elsif (exists $p1{$option}) {
574 my $keyword = $p1{$option};
575 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token
);
576 $cur->{$keyword} = wrap_pre_negated
($keyword, \
$pre_negated, $param);
577 } elsif (exists $p1c{$option}) {
578 my $keyword = $p1c{$option};
579 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token_comma
);
580 $cur->{$keyword} = wrap_pre_negated
($keyword, \
$pre_negated, $param);
581 } elsif (exists $p1multi{$option}) {
582 my $keyword = $p1multi{$option};
583 my $param = wrap_negated
($keyword, $tokens, \
&fetch_token
);
584 $cur->{$keyword} ||= [];
585 push @
{$cur->{$keyword}},
586 wrap_pre_negated
($option, \
$pre_negated, $param);
587 } elsif (exists $p2c{$option}) {
588 my $keyword = $p2c{$option};
589 my $param = wrap_negated
($keyword, $tokens, \
&fetch_two_tokens_comma
);
590 $cur->{$keyword} = wrap_pre_negated
($keyword, \
$pre_negated, $param);
591 } elsif ($option eq 'j') {
593 my $target = shift @
$tokens;
594 # store the target in $line->{jump}
595 $cur = $line->{jump
} = {};
596 unless (is_netfilter_core_target
($target) ||
597 is_netfilter_module_target
($target)) {
598 $cur->{goto} = $target;
600 $cur->{$target} = undef;
602 # what now follows is target parameters; set $cur
604 $line->{cur
} = $line->{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";
630 write_line
qw(domain ip {);
635 } elsif (/^\*(\w+)$/) {
637 write_line
('}') if defined $table;
639 write_line
('table', $table, '{');
640 } elsif (/^:(\S+)\s+-\s+/) {
642 die unless defined $table;
643 write_line
("chain $1;");
644 } elsif (/^:(\S+)\s+(\w+)\s+/) {
646 die unless defined $table;
647 write_line
('chain', $1, 'policy', $2, ';');
648 } elsif (s/^-A (\S+)\s+//) {
650 unless (defined $chain) {
653 write_line
('chain', $chain, '{');
654 } elsif ($1 ne $chain) {
658 write_line
('chain', $chain, '{');
661 my @tokens = tokenize
($_);
664 # separate 'match' parameters from 'targe't parameters; $cur
665 # points to the current position
666 $line{cur
} = $line{match
} = {};
668 local $_ = shift @tokens;
669 if (/^-(\w)$/ || /^--(\S+)$/) {
670 parse_option
(\
%line, $1, undef, \
@tokens);
671 } elsif ($_ eq '!') {
674 /^-(\w)$/ || /^--(\S+)$/
675 or die "option expected in line $.\n";
676 parse_option
(\
%line, $1, 1, \
@tokens);
678 print STDERR
"warning: unknown token '$_' in line $.\n";
683 } elsif ($_ =~ /^COMMIT/) {
686 if (defined $chain) {
691 print STDERR
"line $. was not understood, ignoring it\n";
696 write_line
'}' if defined $chain;
697 write_line
'}' if defined $table;
701 die unless $indent == 0;
707 import-ferm - import existing firewall rules into ferm
711 B<import-ferm> > ferm.conf
713 iptables-save | B<import-ferm> > ferm.conf
715 B<import-ferm> I<inputfile> > ferm.conf
719 This script helps you with porting an existing IPv4 firewall
720 configuration to ferm. It reads a file generated with
721 B<iptables-save>, and tries to suggest a ferm configuration file.
723 If no input file was specified on the command line, B<import-ferm>
724 runs F<iptables-save>.