1 # Functions for managing an ipfw firewall.
2 # Works on a file as generated by ipfw list and read by ipfw /path/name,
3 # rather than a script.
4 # XXX some thing are not supported by ipfw1
6 BEGIN { push(@INC, ".."); };
9 if (&foreign_check("net")) {
10 &foreign_require("net", "net-lib.pl");
11 if (defined(&net::get_rc_conf)) {
17 $ipfw_file = "$module_config_directory/ipfw.rules";
18 if ($config{'save_file'}) {
19 $ipfw_file = $config{'save_file'};
21 elsif ($has_net_lib) {
22 # Use entry in rc.conf, if set
23 local %rc = &net::get_rc_conf();
24 if ($rc{'firewall_type'} =~ /^\//) {
25 $ipfw_file = $rc{'firewall_type'};
29 @actions = ( "allow", "deny", "reject", "reset", "skipto", "fwd", "check-state",
30 "count", "divert", "pipe", "queue", "tee", "unreach" );
32 @unreaches = ( "net", "host", "protocol", "port", "needfrag", "srcfail",
33 "net-unknown", "host-unknown", "isolated", "net-prohib",
34 "host-prohib", "tosnet", "toshost", "filter-prohib",
35 "host-precedence", "precedence-cutoff" );
37 @options = ( "bridged", "established", "frag", "in", "out",
38 "keep-state", "setup" );
40 @one_options = ( "gid", "uid", "icmptypes", "recv", "xmit",
43 @two_options = ( "limit", "mac" );
45 @multi_options = ( "dst-port", "src-port" );
47 @icmptypes = ( "echo-reply", undef, undef, "destination-unreachable",
48 "source-quench", "redirect", undef, undef, "echo-request",
49 "router-advertisement", "router-solicitation", "ttl-exceeded",
50 "ip-header-bad", "timestamp-request", "timestamp-reply",
51 "information-request", "information-reply",
52 "address-mask-request", "address-mask-reply" );
54 @tcpflags = ( "fin", "syn", "rst", "psh", "ack", "urg" );
56 # Get the detected ipfw version
57 if (open(VERSION, "$module_config_directory/version")) {
58 chop($ipfw_version = <VERSION>);
62 # get_config([file], [&output])
63 # Returns a list of rules from the firewall file
66 local $file = $_[0] || $ipfw_file;
67 local $fmt = &get_ipfw_format();
69 # When getting from command, there is never an 'add'
77 ${$_[1]} .= $_ if ($_[1]);
79 if ($fmt == 1 && !/^add\s+/ && !/^#/) {
80 # Expecting 'add' suffixes, but found some other directive
81 local $rule = { 'index' => scalar(@rv),
82 'line' => $lnum-scalar(@cmts),
89 elsif (/^(add\s+)?(\d+)\s+(.*)/) {
91 local @cmts = split(/\n/, $cmt);
92 local $rule = { 'index' => scalar(@rv),
93 'line' => $lnum-scalar(@cmts),
100 local @w = &split_quoted_string($3);
101 $rule->{'cmd'} =~ s/\s+$//;
103 # Parse counts, if given
104 if ($w[0] =~ /^\d+$/) {
105 $rule->{'count1'} = shift(@w);
106 $rule->{'count2'} = shift(@w);
109 # parse the set number
110 if ($w[0] eq "set") {
112 $rule->{'set'} = shift(@w);
115 # parse the probability of match
116 if ($w[0] eq "prob") {
118 $rule->{'prob'} = shift(@w);
122 $rule->{'action'} = shift(@w);
123 if ($rule->{'action'} =~ /divert|fwd|forward|pipe|queue|skipto|tee|unreach/) {
125 $rule->{'aarg'} = shift(@w);
128 # Parse the log section
129 if ($w[0] eq "log") {
132 if ($w[0] eq "logamount") {
134 $rule->{'logamount'} = shift(@w);
140 if ($w[0] eq "{" || $w[0] eq "(") {
141 $rule->{'proto'} = &words_to_orblock(\@w);
144 $rule->{'proto'} = shift(@w);
145 $hasproto++ if ($rule->{'proto'} ne "ip" &&
146 $rule->{'proto'} ne "any");
149 # Parse the source and destination sections
151 foreach $s ("from", "to") {
152 local $sn = shift(@w);
156 if ($w[0] eq "not") {
157 $rule->{$s."_not"} = 1;
160 if ($w[0] eq "{" || $w[0] eq "(") {
161 $rule->{$s} = &words_to_orblock(\@w);
164 $rule->{$s} = shift(@w);
168 local $pr = $rule->{'proto'};
169 if ($w[0] eq "not" && @w > 1 &&
170 ($w[1] =~ /^\d+$/ || $w[1] =~ /,/ ||
172 defined(getservbyname($w[1], $rule->{'proto'})))) {
174 $rule->{$s."_ports_not"} = 1;
176 if ($w[0] =~ /^\d+$/ || $w[0] =~ /,/ ||
177 ($w[0] =~ /^(\S+)\-(\S+)$/ &&
178 &valid_port($1, $pr) &&
179 &valid_port($2, $pr)) ||
180 &valid_port($w[0], $pr)) {
181 $rule->{$s."_ports"} = shift(@w);
186 if ($w[0] eq "{" || $w[0] eq "(") {
187 # XXX can be an or-block!
188 $rule->{'options'} = &words_to_orblock(\@w);
193 local $o = lc(shift(@w));
194 $o = "icmptypes" if ($o eq "icmptype");
199 if (&indexof($o, @options) >= 0) {
202 $rule->{$o."_not"} = $nextnot;
204 elsif (&indexof($o, @one_options) >= 0) {
205 # Option with one value
206 $rule->{$o} = shift(@w);
207 $rule->{$o."_not"} = $nextnot;
209 elsif (&indexof($o, @two_options) >= 0) {
210 $rule->{$o} = [ shift(@w), shift(@w) ];
211 $rule->{$o."_not"} = $nextnot;
213 elsif (&indexof($o, @multi_options) >= 0) {
215 while(@w && $w[0] =~ /^\d+$/) {
216 push(@{$rule->{$o}}, shift(@w));
218 $rule->{$o."_not"} = $nextnot;
222 push(@{$rule->{'unknown'}}, "not") if ($nextnot);
223 push(@{$rule->{'unknown'}}, $o);
232 elsif (/^#\s*(.*)/) {
233 # A comment, which applies to the next rule
234 $cmt .= "\n" if ($cmt);
242 # valid_port(text, protocol)
245 return 1 if ($_[0] =~ /^\d+$/);
246 return 1 if (defined(getservbyname($_[0], $_[1])));
250 # save_config(&rules)
251 # Updates the firewall file with a list of rules
254 open(LIST, ">$ipfw_file");
255 foreach $r (@{$_[0]}) {
256 local @lines = &rule_lines($r);
258 foreach $l (@lines) {
265 # rule_lines(&rule, [no-comment], [no-add])
266 # Returns the lines of text to make up a rule
269 local ($rule, $nocmt, $noadd) = @_;
270 local @cmts = $nocmt ? ( ) : map { "# $_" } split(/\n/, $rule->{'cmt'});
271 local $fmt = &get_ipfw_format();
272 if ($rule->{'other'}) {
273 # Some other line (non-add) that never changes
274 return (@cmts, $rule->{'text'});
276 elsif (defined($rule->{'text'})) {
277 # A rule line that has not changed
278 if ($fmt && !$rule->{'cmd'}) {
279 $rule->{'cmd'} = 'add';
281 return (@cmts, ($rule->{'cmd'} ? $rule->{'cmd'}." " : "").
282 (defined($rule->{'num'}) ? $rule->{'num'}." " : "").
289 # Add the basic rule parameters
290 if ($fmt == 1 && !$noadd) {
293 push(@w, $rule->{'num'});
294 push(@w, "set", $rule->{'set'}) if (defined($rule->{'set'}));
295 push(@w, "prob", $rule->{'prob'}) if (defined($rule->{'prob'}));
296 push(@w, $rule->{'action'});
297 push(@w, $rule->{'aarg'}) if (defined($rule->{'aarg'}));
298 if ($rule->{'log'}) {
300 push(@w, "logamount", $rule->{'logamount'})
301 if (defined($rule->{'logamount'}));
303 push(@w, &orblock_to_words($rule->{'proto'}));
305 # Add the from and to sections
307 foreach $s ("from", "to") {
309 push(@w, "not") if ($rule->{$s."_not"});
310 push(@w, &orblock_to_words($rule->{$s}));
311 if (defined($rule->{$s."_ports"})) {
312 push(@w, "not") if ($rule->{$s."_ports_not"});
313 push(@w, $rule->{$s."_ports"});
318 if (ref($rule->{'options'})) {
319 push(@w, &orblock_to_words($rule->{'options'}));
323 foreach $o (@options) {
325 push(@w, "not") if ($rule->{$o."_not"});
329 foreach $o (@one_options) {
330 if (defined($rule->{$o})) {
331 push(@w, "not") if ($rule->{$o."_not"});
333 push(@w, $rule->{$o});
336 foreach $o (@two_options, @multi_options) {
337 if (defined($rule->{$o})) {
338 push(@w, "not") if ($rule->{$o."_not"});
340 push(@w, @{$rule->{$o}});
343 push(@w, @{$rule->{'unknown'}});
346 # Create the resulting rule string
347 local @w = map { $_ =~ /\(|\)/ ? "\"$_\"" : $_ } @w;
348 return (@cmts, join(" ", @w));
356 if ($r->{'proto'} ne 'all' && $r->{'proto'} ne 'ip') {
357 push(@rv, &text($r->{'proto_not'} ? 'desc_proto_not' : 'desc_proto',
358 "<b>".uc($r->{'proto'})."</b>"));
360 if ($r->{'from'} ne 'any') {
361 push(@rv, &text($r->{'from_not'} ? 'desc_from_not' : 'desc_from',
362 $r->{'from'} eq 'me' ? $text{'desc_me'} : "<b>$r->{'from'}</b>"));
364 if ($r->{'from_ports'} ne '') {
365 push(@rv, &text($r->{'from_ports_not'} ? 'desc_from_ports_not'
367 "<b>$r->{'from_ports'}</b>"));
369 if ($r->{'to'} ne 'any') {
370 push(@rv, &text($r->{'to_not'} ? 'desc_to_not' : 'desc_to',
371 $r->{'to'} eq 'me' ? $text{'desc_me'} : "<b>$r->{'to'}</b>"));
373 if ($r->{'to_ports'} ne '') {
374 push(@rv, &text($r->{'to_ports_not'} ? 'desc_to_ports_not'
376 "<b>$r->{'to_ports'}</b>"));
378 push(@rv, $text{'desc_in'}) if ($r->{'in'});
379 push(@rv, $text{'desc_out'}) if ($r->{'out'});
381 foreach $o (@options) {
382 if ($r->{$o} && $r->{$o."_not"}) {
383 push(@rv, $text{'desc_'.$o.'_not'});
386 push(@rv, $text{'desc_'.$o});
389 foreach $o (@one_options) {
391 if ($o eq "icmptypes") {
392 $v = join(",", map { $icmptypes[$_] || $_ }
395 if ($r->{$o} && $r->{$o."_not"}) {
396 push(@rv, &text('desc_'.$o.'_not', "<b>$v</b>"));
399 push(@rv, &text('desc_'.$o, "<b>$v</b>"));
403 if ($r->{'mac'}->[0] eq "any") {
404 push(@rv, &text('desc_mac1', "<b>$r->{'mac'}->[1]</b>"));
406 elsif ($r->{'mac'}->[1] eq "any") {
407 push(@rv, &text('desc_mac2', "<b>$r->{'mac'}->[0]</b>"));
410 push(@rv, &text('desc_mac', "<b>$r->{'mac'}->[0]</b>",
411 "<b>$r->{'mac'}->[1]</b>"));
415 $limit = &text('desc_limit', $text{'desc_'.$r->{'limit'}->[0]}, $r->{'limit'}->[1]);
417 if ($r->{'dst-port'}) {
418 push(@rv, &text('desc_dstport', join(", ", @{$r->{'dst-port'}})));
420 if ($r->{'src-port'}) {
421 push(@rv, &text('desc_srcport', join(", ", @{$r->{'src-port'}})));
423 return @rv ? &text($_[1] ? 'desc_where' : 'desc_if',
424 join(" $text{'desc_and'} ", @rv)).$limit
425 : $text{$_[1] ? 'desc_all' : 'desc_always'}.$limit;
428 # words_to_orblock(&words)
431 local $st = shift(@{$_[0]});
432 while($_[0]->[0] ne $st) {
433 push(@or, shift(@{$_[0]}));
439 # orblock_to_words(&block)
443 return ( "{", @{$_[0]}, "}" );
451 # Returns the proper name for some action
454 return $_[0] =~ /accept|pass|permit/ ? "allow" :
455 $_[0] =~ /drop/ ? "deny" :
456 $_[0] =~ /forward/ ? "fwd" : $_[0];
461 local @stdprotos = ( 'tcp', 'udp', 'icmp' );
463 open(PROTOS, "/etc/protocols");
467 push(@otherprotos, $1) if (/^(\S+)\s+(\d+)/);
470 @otherprotos = sort { lc($a) cmp lc($b) } @otherprotos;
471 return &unique(@stdprotos, @otherprotos);
474 # apply_rules([&rules])
475 # Apply the supplied firewall rules
479 $conf ||= &get_config();
480 local $dir = &get_current_dir();
482 local $fmt = &get_ipfw_format();
483 &system_logged("$config{'ipfw'} -f flush >/dev/null 2>&1");
485 # Apply each rule in turn
487 foreach $r (@$conf) {
488 if (!$r->{'other'} &&
489 $r->{'num'} != 65535) { # skip auto-added final rule
490 local ($line) = &rule_lines($r, 1, 1);
491 local $cmd = "$config{'ipfw'} add $line";
492 $out = &backquote_logged("$cmd 2>&1 </dev/null");
493 return "<tt>$cmd</tt> failed : <tt>$out</tt>" if ($?);
498 # The ipfw command can apply the whole file
499 local $out = &backquote_logged(
500 "$config{'ipfw'} ".quotemeta($ipfw_file)." 2>&1 </dev/null");
501 return "<tt>$config{'ipfw'} $ipfw_file</tt> failed : <tt>$out</tt>"
509 # Returns the system to an 'accept all' state
515 &system_logged("$config{'ipfw'} -f flush >/dev/null 2>&1");
516 &system_logged("$config{'ipfw'} add allow ip from any to any >/dev/null 2>&1");
521 # interface_choice(name, value, noignored)
526 return &net::interface_choice($_[0], $_[1],
527 $_[2] ? undef : "<$text{'edit_ignored'}>");
530 return "<input name=$_[0] size=6 value='$_[1]'>";
534 sub create_firewall_init
536 &foreign_require("init", "init-lib.pl");
537 &foreign_require("cron", "cron-lib.pl");
538 &cron::create_wrapper("$module_config_directory/start.pl",
539 $module_name, "start.pl");
540 &cron::create_wrapper("$module_config_directory/stop.pl",
541 $module_name, "stop.pl");
542 &init::enable_at_boot($module_name,
544 "$module_config_directory/start.pl",
545 "$module_config_directory/stop.pl");
548 # list_cluster_servers()
549 # Returns a list of servers on which the firewall is managed
550 sub list_cluster_servers
552 &foreign_require("servers", "servers-lib.pl");
553 local %ids = map { $_, 1 } split(/\s+/, $config{'servers'});
554 return grep { $ids{$_->{'id'}} } &servers::list_servers();
557 # add_cluster_server(&server)
558 sub add_cluster_server
560 local @sids = split(/\s+/, $config{'servers'});
561 $config{'servers'} = join(" ", @sids, $_[0]->{'id'});
562 &save_module_config();
565 # delete_cluster_server(&server)
566 sub delete_cluster_server
568 local @sids = split(/\s+/, $config{'servers'});
569 $config{'servers'} = join(" ", grep { $_ != $_[0]->{'id'} } @sids);
570 &save_module_config();
573 # server_name(&server)
576 return $_[0]->{'desc'} ? $_[0]->{'desc'} : $_[0]->{'host'};
579 # copy_to_cluster([force])
580 # Copy all firewall rules from this server to those in the cluster
583 return if (!$config{'servers'}); # no servers defined
584 return if (!$_[0] && $config{'cluster_mode'}); # only push out when applying
586 foreach $s (&list_cluster_servers()) {
587 &remote_foreign_require($s, "ipfw", "ipfw-lib.pl");
588 local $rfile = &remote_eval($s, "ipfw", "\$ipfw_file");
589 &remote_write($s, $ipfw_file, $rfile);
593 # apply_cluster_configuration()
594 # Activate the current configuration on all servers in the cluster
595 sub apply_cluster_configuration
597 return undef if (!$config{'servers'});
598 if ($config{'cluster_mode'}) {
602 foreach $s (&list_cluster_servers()) {
603 &remote_foreign_require($s, "ipfw", "ipfw-lib.pl");
604 local $err = &remote_foreign_call($s, "ipfw", "apply_rules");
606 return &text('apply_remote', $s->{'host'}, $err);
613 # Returns 1 if enabled at boot via an init script, 2 if enabled via rc.conf,
614 # -1 if a different file is enabled at boot, 0 otherwise
617 &foreign_require("init", "init-lib.pl");
618 local $atboot = &init::action_status($module_name);
622 if ($has_net_lib && defined(&net::get_rc_conf)) {
623 local %rc = &net::get_rc_conf();
624 if ($rc{'firewall_enable'} ne 'YES') {
628 elsif ($rc{'firewall_type'} eq $ipfw_file) {
631 elsif ($rc{'firewall_type'}) {
632 # A *different* file is enabled
640 # Make sure ipfw gets started at boot. Uses rc.conf if possible
643 return 0 if (&check_boot()); # Already on
644 if ($has_net_lib && defined(&net::get_rc_conf) && &get_ipfw_format() == 1) {
646 local %rc = &net::get_rc_conf();
647 &lock_file("/etc/rc.conf");
648 &net::save_rc_conf('firewall_type', $ipfw_file);
649 &net::save_rc_conf('firewall_enable', 'YES');
650 &net::save_rc_conf('firewall_quiet', 'YES');
651 &unlock_file("/etc/rc.conf");
656 &create_firewall_init();
663 local $mode = &check_boot();
664 return 0 if ($mode <= 0);
666 # Turn off init script
667 &init::disable_at_boot($module_name);
670 # Take out rc.conf entry
671 &lock_file("/etc/rc.conf");
672 &net::save_rc_conf('firewall_enable', 'NO');
673 &unlock_file("/etc/rc.conf");
679 # Works out the IPFW file format we should use. Returns 1 for with 'add' at the
680 # start, vs 0 for without.
683 if (defined($get_ipfw_format_cache)) {
684 return $get_ipfw_format_cache;
687 if (open(FILE, $ipfw_file)) {
688 # Check existing format
695 elsif (/\S/ && !/^\#/) {
696 # Add or other directive line
703 if (!defined($fmt)) {
704 if (-r "/etc/rc.conf") {
705 # FreeBSD - use it's format
709 # Assume numeric format
713 $get_ipfw_format_cache = $fmt;