File Coverage

blib/lib/Fwctl.pm
Criterion Covered Total %
statement 19 20 95.0
branch 2 4 50.0
condition n/a
subroutine 5 5 100.0
pod n/a
total 26 29 89.6


line stmt bran cond sub pod time code
1             #
2             # Fwctl.pm - Module to control the linux kernel firewall with high level rules.
3             #
4             # This file is part of Fwctl.
5             #
6             # Author: Francis J. Lacoste
7             #
8             # Copyright (c) 1999,2000 iNsu Innovations Inc.
9             #
10             # This program is free software; you can redistribute it and/or modify
11             # it under the terms of the GNU General Public License as published by
12             # the Free Software Foundation; either version 2 of the License, or
13             # (at your option) any later version.
14             #
15              
16             package Fwctl;
17              
18 1     1   938 use strict;
  1         2  
  1         37  
19 1     1   5 use vars qw( $VERSION $PORTFW $IPCHAINS );
  1         2  
  1         54  
20 1     1   5 use Carp;
  1         6  
  1         108  
21              
22             BEGIN {
23 1     1   2 $VERSION = '0.28';
24              
25 1     1   604 eval { use IPChains::PortFW; new IPChains::PortFW; };
  1         3  
  1         116  
  1         2  
  1         4  
26 1 50       3 $PORTFW = 1 unless $@;
27              
28             # Look for ipchains
29 1         14 ($IPCHAINS) = grep { -x "$_/ipchains" } split /:/, "/sbin:/bin:/usr/sbin:/usr/bin:$ENV{PATH}";
  11         141  
30 1 50       1105 die ( "Couldn't find ipchains in PATH ($ENV{PATH})\n" ) unless $IPCHAINS;
31 0           $IPCHAINS .= "/ipchains";
32             }
33              
34             # Preloaded methods go here.
35             use Net::IPv4Addr 0.10 qw(:all);
36             use Getopt::Long;
37             use Fcntl qw( :flock );
38             use IPChains;
39              
40              
41             # Constants
42             use constant INTERFACES_FILE => '/etc/fwctl/interfaces';
43             use constant ALIASES_FILE => '/etc/fwctl/aliases';
44             use constant RULES_FILE => '/etc/fwctl/rules';
45              
46             use constant SERVICES_DIR => [ "/etc/fwctl/services" ];
47             use constant ACCOUNTING_FILE => '/var/log/fwctl_acct';
48              
49              
50             my @REQUIRED_METHODS = qw( valid_options block_rules account_rules accept_rules );
51             my %ACTIONS = map { $_ => 1; } qw( ACCEPT DENY REJECT ACCOUNT );
52             my @STANDARD_OPTIONS = ( "masq!", "log!", "copy!", "src=s", "dst=s",
53             "name=s", "account", "mark=i", "portfw:s" );
54              
55             sub new {
56             my $proto = shift;
57             my $class = ref( $proto) || $proto;
58              
59             my $self = bless {
60             interfaces_file => INTERFACES_FILE,
61             aliases_file => ALIASES_FILE,
62             rules_file => RULES_FILE,
63             services_dir => SERVICES_DIR,
64             accounting_file => ACCOUNTING_FILE,
65             interfaces => {},
66             aliases => {},
67             services => {},
68             rules => [],
69             account => 0,
70             copy => 1,
71             log => 1,
72             mark => 0,
73             default => 'DENY',
74             @_, # Overrides any default with arguments
75             }, $class;
76              
77             # Add services dir to @INC
78             eval( join( " ", "use lib qw(", @{ $self->{services_dir} },");" ) );
79             die __PACKAGE__, ": error while adding services dir to \@INC: $@" if $@;
80              
81             carp __PACKAGE__, "default must be one of ACCEPT, REJECT or DENY"
82             unless $self->{default} =~ /^ACCEPT|REJECT|DENY$/;
83              
84             carp __PACKAGE__, "mark not an integer" unless $self->{mark} =~ /^\d+$/;
85              
86             warn __PACKAGE__, "default policy is not REJECT or DENY"
87             if $self->{default} eq "ACCEPT";
88              
89             # Read all configuration files
90             $self->read_interfaces();
91             $self->read_aliases();
92             $self->read_rules();
93              
94             # Return ourselve
95             $self;
96             };
97              
98             # Get or sets the interfaces.
99             sub interfaces {
100             my $self = shift;
101              
102             if (@_) {
103             # Must get an array of interface references
104             $self->{interfaces} = { map { $_->{name} => $_ } @_ };
105             $self->{routes} = undef;
106             }
107              
108             # Returns an array of references
109             values %{$self->{interfaces}};
110             }
111              
112             sub routes {
113             my $self = $_[0];
114              
115             unless ($self->{routes}) {
116             my @routes = ();
117              
118             foreach my $if ( $self->interfaces ) {
119             # Don't include the ANY interface.
120             next if $if->{name} eq "ANY";
121              
122             # Host route to the interface.
123             unless ( $if->{netmask} == 32 ) {
124             push @routes, [ $if, { network => $if->{ip},
125             netmask => 32 } ];
126             }
127              
128             # Directly connected net.
129             push @routes, [ $if, $if ];
130              
131             # Other connected nets.
132             push @routes, map { [$if, $_ ] } @{$if->{other_nets}};
133             }
134              
135             # Sort from the most specific to the least specific.
136             $self->{routes} = [ sort { $b->[1]{netmask} <=> $a->[1]{netmask} } @routes ];
137             }
138              
139             @{ $self->{routes} };
140             }
141              
142             # Get or set an interface by name
143             sub interface {
144             my ($self, $name) = @_;
145              
146             if ( @_ == 3 ) {
147             $self->{interfaces}{$name} = $_[2];
148             $self->{routes} = undef;
149             }
150              
151             $self->{interfaces}{$name};
152             }
153              
154             # Get or set an alias
155             sub alias {
156             my $self = shift;
157             my $name = shift;
158              
159             $self->{aliases}{$name} = shift if @_;
160              
161             $self->{aliases}{$name};
162             }
163              
164             # Expand an alias recursively into interface and parsed IP.
165             sub expand {
166             my ( $self, $string, $recurs_lvl ) = @_;
167             $recurs_lvl ||= 0;
168             $recurs_lvl++;
169             die __PACKAGE__, ": too much alias recursion\n" if $recurs_lvl > 15;
170             my @expansion = ();
171             for my $s (split /\s+/, $string ) {
172             if ( $self->alias($s) ) {
173             push @expansion, $self->expand( $self->alias($s), $recurs_lvl );
174             } else {
175             if ( $s eq "INTERNET" ) {
176             push @expansion, [ "0.0.0.0/0", $self->interface( 'EXT' ) ];
177             } elsif ( $s eq "ANY" ) {
178             push @expansion, [ "0.0.0.0/0", $self->interface( 'ANY' ) ];
179             } else {
180             my ( $ipv4, $if ) = $s =~ m!([0-9./]+)(?:\((\w+)\))?!;
181             if ( defined $if ) {
182             $if = $self->interface( $if );
183             die "invalid interface spec in alias expansion: $if\n" unless $if;
184             }
185             eval {
186             $ipv4 = ipv4_parse( $ipv4 );
187             };
188             die "invalid ip address : $ipv4\n" if $@;
189             $if = $self->find_interface( $ipv4 ) unless defined $if;
190             push @expansion, [$ipv4, $if];
191             }
192             }
193             }
194             return @expansion;
195             }
196              
197             # Get a service handler. The service handler is loaded
198             # dynamically if it is not already defined.
199             sub service {
200             my ($self,$name) = @_;
201              
202             unless ($self->{services}{$name} ) {
203             # Load because it is not loaded
204              
205             $self->{services}{$name} =
206             eval "use Fwctl::Services::$name; new Fwctl::Services::$name;";
207              
208             if ($@) {
209             # No service defined as module.
210             # Try to cook a generic TCP one.
211             my $port = getservbyname $name, 'tcp';
212             unless ($port) {
213             my $new_serv = $name;
214             $new_serv =~ s/_/-/g;
215             $port = getservbyname $new_serv, 'tcp';
216             }
217             # If no port could be find, then warn
218             unless ($port) {
219             warn __PACKAGE__, ": error while loading service $name: $@\n";
220             return undef;
221             }
222             $self->{services}{$name} =
223             eval "package Fwctl::Services::$name;" . q{
224              
225             use vars qw(@ISA);
226             use Fwctl::Services::tcp_service;
227              
228             BEGIN { @ISA = qw(Fwctl::Services::tcp_service); }
229              
230             sub new {
231             my $proto = shift;
232             my $class = ref($proto) || $proto;
233             my $self = $class->SUPER::new(@_);
234             $self->{port} = $port;
235             bless $self,$class;
236             }
237              
238             } . "new Fwctl::Services::$name;";
239              
240             if ($@) {
241             warn __PACKAGE__, ": error while defining tcp_service $name: $@";
242             return undef;
243             }
244             }
245             }
246              
247             $self->{services}{$name};
248             };
249              
250             sub accounting_file {
251             my $self = shift;
252             if (@_) {
253             $self->{accounting_file} = shift;
254             }
255             $self->{accounting_file};
256             }
257              
258             # Get the accounting rules only
259             sub account_rules {
260             my $self = shift;
261              
262             grep { $_->{action} eq "ACCOUNT" } @{$self->{rules}};
263             }
264              
265             # Get a reference to the array containing all
266             # the firewall rules
267             sub rules {
268             my $self = shift;
269              
270             $self->{rules};
271             }
272              
273             # Given an host, find the alias to which this one is
274             # related. We return the most specific one. Least specific is
275             # INTERNET.
276             sub find_host_alias {
277             my ( $self, $ip ) = @_;
278              
279             # Try to find the alias as an IP
280             $ip =~ s/\.0+(\d)/\.$1/g; # Normalize .001 -> .1 .000 .0
281             while ( my ( $alias, $expansion ) = each %{$self->{aliases}} ) {
282             my @aliases = split /\s+/, $expansion;
283             # We don't need to recurse since recursive alias are necesserarly
284             # less specific
285             foreach my $a ( @aliases ) {
286             $a =~ s/\.0+(\d)/\.$1/g; # Canonicalize
287             $a =~ s/\(\w+\)//g; # Remove interface spec
288             return $alias if $a eq $ip;
289             }
290             }
291              
292             # Try to find as included in a subnet.
293             while ( my ( $alias, $expansion ) = each %{$self->{aliases}} ) {
294             # Skip ANY_ alias
295             next if index ( $alias, "ANY") == 0;
296             my @aliases = split /\s+/, $expansion;
297              
298             foreach my $a ( @aliases ) {
299             $a =~ s/\(\w+\)//g; # Remove interface spec
300             # Try not to compare aliase
301             next unless $a =~ m!^[\d/.]+$!;
302             return $alias if ipv4_in_network( $a, $ip );
303             }
304             }
305              
306             # Default
307             return 'INTERNET';
308             }
309              
310             # Given an IP, network or whatever, find the target
311             # interface.
312             sub find_interface {
313             my ($self,$ip) = @_;
314              
315             # Magic interfaces
316             return $self->interface('ANY') if $ip =~ /ANY/i;
317             return $self->interface('EXT') if $ip =~ /INTERNET/i;
318              
319             # Check each interface to see if the ip
320             # is part of a network
321             foreach my $route ( $self->routes ) {
322             my ( $if, $net ) = @$route;
323              
324             # Check if they are on the same network
325             return $if if ipv4_in_network( $net->{network},
326             $net->{netmask},
327             $ip
328             );
329              
330             }
331              
332             # Default is Internet
333             return $self->interface('EXT');
334             }
335              
336             # This breaks in regards to virtual interface
337             sub find_interface_by_dev {
338             my ( $self, $dev ) = @_;
339              
340             foreach my $if ( $self->interfaces ) {
341             return $if->{name} if $if->{interface} eq $dev;
342             }
343              
344             # Not found
345             return undef;
346             }
347              
348             sub reset_fw {
349             my $self = shift;
350              
351             # Dump old account
352             $self->dump_acct;
353              
354             # Sets default policies
355             my $fwctl = IPChains->new( Rule => $self->{default} );
356             $fwctl->set_policy( "input" );
357             $fwctl->set_policy( "forward" );
358             $fwctl->set_policy( "output" );
359              
360             # Setting the default policy, prevents
361             # a vulnerability window when we reset the
362             # firewall.
363             #
364             # Some usually not blocked packets, will be
365             # blocked. Some usually logged packets won't be.
366              
367             # Flush standard chains
368             $fwctl->clopts;
369             $fwctl->flush( "input" );
370             $fwctl->flush( "output" );
371             $fwctl->flush( "forward" );
372              
373             # Flush the user chains
374             for ($fwctl->list_chains) {
375             $fwctl->flush( $_ );
376             }
377              
378             # ... and delete them
379             for ($fwctl->list_chains) {
380             $fwctl->del_chain( $_ );
381             }
382              
383             # If portfw is available flush it
384             if ( $PORTFW ) {
385             IPChains::PortFW->new()->flush;
386             }
387              
388             # Create accounting chains...
389             for (qw(in fwd out )) {
390             $fwctl->new_chain( "acct-$_" );
391             }
392              
393             # ... and add them to the firewall
394             $fwctl->attribute( Rule => "acct-in" );
395             $fwctl->append( "input" );
396             $fwctl->attribute( Rule => "acct-fwd" );
397             $fwctl->append( "forward" );
398             $fwctl->attribute( Rule => "acct-out" );
399             $fwctl->append( "output" );
400              
401             # Create the protocol optimizing chains...
402             $fwctl->clopts;
403             for my $proto (qw( tcp udp icmp all syn ack oth )) {
404             for my $dir ( qw( -in -fwd -out ) ) {
405             $fwctl->new_chain( $proto . $dir );
406             }
407             }
408              
409             # ... add them to the firewall
410             for my $proto (qw(icmp tcp udp)) {
411             $fwctl->attribute( Prot => $proto );
412             $fwctl->attribute( Rule => "$proto-in" );
413             $fwctl->append( "input" );
414             }
415             for my $proto (qw(icmp tcp udp)) {
416             $fwctl->attribute( Prot => $proto );
417             $fwctl->attribute( Rule => "$proto-fwd" );
418             $fwctl->append( "forward" );
419             }
420             for my $proto (qw(icmp tcp udp)) {
421             $fwctl->attribute( Prot => $proto );
422             $fwctl->attribute( Rule => "$proto-out" );
423             $fwctl->append( "output" );
424             }
425              
426             # Add other and all
427             $fwctl->attribute( Prot => undef );
428             for my $proto ( qw(oth all) ) {
429             $fwctl->attribute( Rule => "$proto-in" );
430             $fwctl->append( "input" );
431              
432             $fwctl->attribute( Rule => "$proto-fwd" );
433             $fwctl->append( "forward" );
434              
435             $fwctl->attribute( Rule => "$proto-out" );
436             $fwctl->append( "output" );
437             }
438              
439             # The all optimisation may cause some
440             # non intuitive behavior regarding the
441             # final outcome of a packet from the
442             # order of the rules. In clear, all
443             # ALL rules with be evaluated after
444             # other rules with more specific
445             # protocol.
446              
447             # TCP with SYN or ACK
448             $fwctl->attribute( Prot => 'tcp');
449             for my $dir (qw(in out fwd)) {
450             # SYN
451             $fwctl->attribute( Rule => "syn-$dir");
452             $fwctl->attribute( SYN => 1 );
453             $fwctl->append( "tcp-$dir" );
454             # ACK
455             $fwctl->attribute( Rule => "ack-$dir");
456             $fwctl->attribute( SYN => '!' );
457             $fwctl->append( "tcp-$dir" );
458             }
459              
460             $self->init_acct;
461             }
462              
463             sub init_acct {
464             my $self = shift;
465              
466             # Create rules accounting chains
467             my $fwctl = IPChains->new;
468             my $acct_chain = IPChains->new;
469             for ($self->account_rules) {
470             $fwctl->new_chain( $_->{account_chain} );
471             $acct_chain->append( $_->{account_chain});
472             }
473              
474             my $file = $self->{accounting_file};
475             open ACCT_FILE, ">>$file"
476             or die __PACKAGE__, ": can't open accounting file ", $file,
477             ": $!\n";
478             my $success = 0;
479             for (0..10) {
480             $success = flock ACCT_FILE,LOCK_EX;
481             last if $success;
482             sleep 3;
483             }
484             die __PACKAGE__, ": couldn't obtain lock on accounting file ",
485             $file, ": $!\n" unless $success;
486              
487             my $timestamp = time;
488             for my $rule ($self->account_rules) {
489             print ACCT_FILE join( " ", $timestamp, $rule->{account_chain}, 0, 0,
490             $rule->{options}{name} ), "\n";
491             }
492             flock ACCT_FILE, LOCK_UN;
493             close ACCT_FILE;
494             }
495              
496             sub dump_acct {
497             my $self = shift;
498              
499             my $timestamp = time;
500              
501             my %chains = ();
502             open IPCHAINS, "$IPCHAINS -Z -L -v -x -n|"
503             or die __PACKAGE__, ": couldn't fork: $!\n";
504             while () {
505             my $chain = undef;
506             if ( ($chain) = /^Chain (acct\d{4})/) {
507             #Start of an accounting chain
508             ; # Discard header
509             my $acct = ;
510             my ($pkts,$bytes) = $acct =~ /^\s*(\d+)\s*(\d+)/;
511             $chains{$chain} = [ $pkts, $bytes ];
512             }
513             }
514             close IPCHAINS
515             or die __PACKAGE__, ": error in ipchains: $?\n";
516              
517             my $file = $self->{accounting_file};
518             open ACCT_FILE, ">>" . $file
519             or die __PACKAGE__, ": can't open accounting file ", $file,
520             ": $!\n";
521             my $success = 0;
522             for (0..10) {
523             $success = flock ACCT_FILE,LOCK_EX;
524             last if $success;
525             sleep 3;
526             }
527             die __PACKAGE__, ": couldn't obtain lock on accounting file ",
528             $file, ": $!\n" unless $success;
529              
530             for my $rule ($self->account_rules) {
531             if ( $chains{$rule->{account_chain} } ) {
532             print ACCT_FILE join( " ", $timestamp, $rule->{account_chain},
533             @{$chains{$rule->{account_chain}}},
534             $rule->{options}{name}), "\n";
535             }
536             }
537             flock ACCT_FILE, LOCK_UN;
538             close ACCT_FILE;
539             }
540              
541             # Configure the firewall
542             sub configure {
543             my $self = shift;
544              
545             $self->reset_fw;
546              
547             # Add user rules
548             RULE:
549             foreach my $rule ( @{$self->{rules} } ) {
550             my $action = $rule->{action};
551             my $service = $self->service( $rule->{service} );
552             my $options = $rule->{options};
553             SRC:
554             foreach my $src_spec ( @{$rule->{src}} ) {
555             DST:
556             foreach my $dst_spec ( @{$rule->{dst}} ) {
557             my ( $src, $src_if ) = @$src_spec;
558             my ( $dst, $dst_if ) = @$dst_spec;
559              
560             SWITCH:
561             for ($action) {
562             /DENY|REJECT/ && do {
563             $service->block_rules( $action, $src, $src_if,
564             $dst, $dst_if, $options );
565             last SWITCH;
566             };
567             /ACCEPT/ && do {
568             $service->accept_rules( $action, $src, $src_if,
569             $dst, $dst_if, $options );
570             last SWITCH;
571             };
572             /ACCOUNT/ && do {
573             $service->account_rules( $rule->{account_chain},
574             $src, $src_if,
575             $dst, $dst_if, $options );
576             last SWITCH;
577             };
578             die __PACKAGE__, ": unknown action $action\n" ;
579             } #SWITCH
580             } #DST
581             } #SRC
582             } #RULE
583              
584             # Then add the logging rules
585             my $fwctl = IPChains->new();
586             $fwctl->attribute( Rule => $self->{default} );
587             if ( $self->{log} ) {
588             $fwctl->attribute( Log => 1 );
589             }
590             if ( $self->{copy} ) {
591             $fwctl->attribute( Output => 1 );
592             }
593             if ( $self->{mark}) {
594             $fwctl->attribute( Mark => $self->{mark} );
595             }
596             $fwctl->append( "input" );
597             $fwctl->append( "forward" );
598             $fwctl->append( "output" );
599             }
600              
601             sub stop {
602             my $self = shift;
603              
604             $self->reset_fw;
605              
606             # Enable looback
607             my $loopback = IPChains->new( Rule => 'ACCEPT', Interface => "lo" );
608             $loopback->insert( "input" );
609             $loopback->insert( "output" );
610             }
611              
612             sub really_flush_chains {
613              
614             # Sets default policies
615             my $fwctl = IPChains->new( Rule => 'ACCEPT' );
616             $fwctl->set_policy( "input" );
617             $fwctl->set_policy( "forward" );
618             $fwctl->set_policy( "output" );
619              
620             # Flush standard chains
621             $fwctl->clopts;
622             $fwctl->flush( "input" );
623             $fwctl->flush( "output" );
624             $fwctl->flush( "forward" );
625              
626             # Flush the user chains
627             for ($fwctl->list_chains) {
628             $fwctl->flush( $_ );
629             }
630              
631             # ... and delete them
632             for ($fwctl->list_chains) {
633             $fwctl->del_chain( $_ );
634             }
635             }
636              
637             sub flush_chains {
638             my $self = shift;
639              
640             # Dump old account
641             $self->dump_acct;
642             $self->really_flush_chains;
643              
644             }
645              
646             # Read the interface specifications
647             sub read_interfaces {
648             my $self = shift;
649             my $file = $self->{interfaces_file};
650              
651             # The loopback device
652             $self->interface( 'LOCAL', { name => 'LOCAL',
653             interface => 'lo',
654             ip => '127.0.0.1',
655             network => '127.0.0.0',
656             broadcast => '127.255.255.255',
657             netmask => '8',
658             });
659              
660             # The ANY device
661             $self->interface( 'ANY', {
662             name => 'ANY',
663             interface => "",
664             ip => "0.0.0.0/0",
665             network => "0.0.0.0",
666             broadcast => "255.255.255.255",
667             netmask => "0",
668             });
669              
670             open ( INTERFACES, $file )
671             or die "fwctl: can't open file $file\n";
672              
673             while () {
674             next if /^\s*#/; # Skip comments
675             next if /^\s*$/; # Skip blank lines
676             chomp;
677              
678             my ($name,$if,$rest) =
679             m@(\w+)\s+([\w+]+)\s*([^#]+)?@;
680             die <
681             fwctl: invalid interface specification at line $. of file $file
682             ERROR
683             # Canonicalize interface -> remove aliases.
684             $if =~ s/:.+//g;
685              
686             my @networks = ();
687             foreach (split /\s+/, $rest) {
688             eval {
689             my ($ip,$msklen) = ipv4_parse( $_ );
690             my ($network) = (ipv4_network($ip,$msklen))[0];
691             push @networks, {
692             ip => $ip,
693             network => $network,
694             netmask => $msklen,
695             broadcast => ipv4_broadcast($network, $msklen),
696             };
697             };
698             warn __PACKAGE__, ": bad interface specification at line $. of file $file: $@\n"
699             if $@;
700             }
701              
702             my $spec = shift @networks;
703             $self->interface($name, {
704             name => $name,
705             interface => $if,
706             %$spec,
707             other_nets => \@networks,
708             });
709             }
710             close INTERFACES;
711             die "fwctl: no EXT interface defined."
712             unless defined $self->interface('EXT');
713             }
714              
715             # Read in the aliases
716             sub read_aliases {
717             my $self = shift;
718             # Defined common aliases for each of the interface
719             foreach my $if ( $self->interfaces ) {
720             my $name = uc $if->{name};
721             my $ip_alias = $name . "_IP";
722             my $net_alias = $name . "_NET";
723             my $rem_alias = $name . "_REM_NETS";
724             my $nets_alias = $name . "_NETS";
725             my $bcast_alias = $name . "_BCAST";
726             my $net = $if->{network} . "/" . $if->{netmask} . "(" . $name . ")";
727             my $nets = [ $net ];
728             foreach my $n ( @{$if->{other_nets} }) {
729             push @$nets, $n->{network} . "/" . $n->{netmask} . "(" . $name . ")";
730             }
731             $self->alias($net_alias, $net );
732             $self->alias($ip_alias, $if->{ip} . "(" . $name . ")" );
733             $self->alias($nets_alias, join( " ", @$nets ) );
734             $self->alias($rem_alias, join( " ", @{$nets}[ 1 .. $#$nets ] ) );
735             $self->alias( $bcast_alias, $if->{broadcast}, "(" . $name . ")" );
736             }
737              
738             # Read in the additional aliases
739             my $file = $self->{aliases_file};
740             open ( ALIASES, $file )
741             or die "fwctl: can't open file $file: $!\n";
742             while () {
743             next if /^\s*#/; # Skip comments
744             next if /^\s*$/; # Skip blank lines
745             chomp;
746              
747             my ( $alias, $exp ) = /^\s*(\w+)\s*[=:]+\s*([^#]+)/;
748             die "fwctl: invalid alias at line $. of file $file\n"
749             unless $alias and $exp;
750             $self->alias( $alias, $exp);
751             }
752             close ALIASES;
753             }
754              
755             # Read in the firewall rules
756             sub read_rules {
757             my $self = shift;
758             my $file = $self->{rules_file};
759             my $error = 0;
760             open ( RULES, $file ) or die "fwctl: can't open file $file: $!\n";
761             RULE:
762             while () {
763             next if /^\s*#/; # Skip comments
764             next if /^\s*$/; # Skip blank lines
765             chomp;
766              
767             # When loop is sucessful it is decrement. Must be 0 when the loop quit.
768             $error++;
769             my ($action,$service,@opts) = split;
770              
771             # Validate rule
772             unless ( $action and $service ) {
773             warn __PACKAGE__, ": incomplete rule at line $. of file $file\n";
774             next RULE;
775             }
776              
777             $action = uc $action;
778             unless ( $ACTIONS{ $action } ) {
779             warn __PACKAGE__, ": unknown action $action at line $. of file $file\n";
780             next RULE;
781             }
782              
783             unless ( $self->service( $service ) ) {
784             warn __PACKAGE__, ": unknown service $service at line $. of file $file\n";
785             next RULE;
786             }
787              
788             # Parse options
789             my %options = ( masq => 0,
790             mark => 0,
791             copy => 0,
792             account => 0,
793             );
794             $options{log} = $action =~ /REJECT|DENY/ ? 1 : 0;
795             {
796             local @ARGV = @opts;
797             local $SIG{__WARN__} = 'IGNORE';
798              
799             GetOptions( \%options, @STANDARD_OPTIONS,
800             $self->service($service)->valid_options )
801             or do {
802             warn __PACKAGE__, ": error while parsing options in service $service\n";
803             next RULE;
804             };
805              
806             if (@ARGV ) {
807             warn __PACKAGE__, ": unknown options", join( ",", @ARGV ), "\n";
808             next RULE;
809             }
810             if ( $options{portfw} && ! $PORTFW ) {
811             warn __PACKAGE__, ": can't use portfw because IPChains::PortFW ",
812             "isn't available at line $.\n";
813             next RULE;
814             }
815             if ( ($options{masq} || exists $options{portfw} ) &&
816             $action =~ /reject|deny/i )
817             {
818             warn __PACKAGE__, ": useless use of masq/portfw option at line $.\n";
819             next RULE;
820             }
821             if ($options{masq} && exists $options{portfw} ) {
822             warn __PACKAGE__, ": conflicting use of masq and portfw at line $.\n";
823             next RULE;
824             }
825             if ($options{account} && $action eq "ACCOUNT" ) {
826             warn __PACKAGE__, ": can't use account option with ACCOUNT action at line $.\n";
827             next RULE;
828             }
829             };
830              
831             # Parse portfw
832             my ($portfw,$portfw_if) = ( $options{portfw} );
833             if ( $portfw ) {
834             eval {
835             ($portfw, $portfw_if ) = @{($self->expand( $portfw ))[0]};
836             $options{portfw} = $portfw;
837             };
838             if ( $@ ) {
839             warn __PACKAGE__, ": invalid aliase expansion in portfw at line $.: $@\n";
840             next RULE;
841             }
842              
843             if ( $portfw_if->{name} eq 'ANY' ) {
844             warn __PACKAGE__, ": can't use ANY interface for portfw at line $.\n";
845             next RULE;
846             }
847             if ( $portfw_if->{ip} ne $portfw ) {
848             warn __PACKAGE__, ": not a local interface in portfw at line $.\n";
849             next RULE;
850             }
851             }
852              
853             # Parse src
854             my @src = ();
855             if ( $options{src} ) {
856             eval {
857             @src = $self->expand( $options{src} );
858             };
859             if ( $@ ) {
860             warn __PACKAGE__, ": error in src specification at line $.: $@\n";
861             next RULE;
862             }
863             # Check that all the sources are valid for portforwarding
864             if ( defined $portfw ) {
865             foreach my $s ( @src ) {
866             if ( $s->[1]{name} eq 'ANY' ) {
867             warn __PACKAGE__, ": can't use portfw with ANY src at line $.\n";
868             next RULE;
869             } elsif ( $portfw && $s->[1]{interface} ne $portfw_if->{interface} ) {
870             warn __PACKAGE__, ": src of portfw doesn't match interface at line $.\n";
871             next RULE;
872             }
873             }
874             }
875             delete $options{src};
876             } else {
877             if ( defined $portfw ) {
878             warn __PACKAGE__, ": can't use portfw with ANY src at line $.\n";
879             next RULE;
880             } else {
881             push @src, $self->expand( 'ANY' ) ;
882             }
883             }
884              
885             # Parse dst
886             my @dst = ();
887             if ( $options{dst} ) {
888             eval {
889             @dst =$self->expand( $options{dst} );
890             };
891             if ( $@ ) {
892             warn __PACKAGE__, ": error in dst specification at line $.: $@\n";
893             next RULE;
894             }
895             # Make sure that all destination are compatible with portfw
896             if ( defined $portfw ) {
897             foreach my $d ( @dst ) {
898             # With portfw only host can be used as dst.
899             eval {
900             my ($ip,$cidr) = ipv4_parse( $d->[0] );
901             unless ( ! defined $cidr || $cidr == 32 ) {
902             warn __PACKAGE__, ": can only use host in dst with portfw $.\n";
903             next RULE;
904             }
905             };
906             if ($@) {
907             warn __PACKAGE__, ": error in dst specification at line $.: $@\n";
908             next RULE;
909             }
910             }
911             }
912             delete $options{dst};
913             } else {
914             if ( defined $portfw ) {
915             warn __PACKAGE__, ": can't use portfw with ANY dst at line $.\n";
916             next RULE;
917             } else {
918             push @dst, $self->expand( 'ANY' );
919             }
920             }
921              
922             # Create standard IPChains options
923             my %ipchains_opts = ();
924             $ipchains_opts{Mark} = $options{mark} if $options{mark};
925             $ipchains_opts{Log} = 1 if $options{log};
926             $ipchains_opts{Output} = $options{copy} if $options{copy};
927             $options{ipchains} = \%ipchains_opts;
928              
929             # Name of accounting chain
930             my $chain = sprintf 'acct%04d', $self->{account}++
931             if $action eq "ACCOUNT" or $options{account};
932              
933             # OK this seems ok.
934             push @{$self->{rules}}, {
935             action => $action,
936             service => $service,
937             options => \%options,
938             src => \@src,
939             dst => \@dst,
940             ($action eq "ACCOUNT" ?
941             (account_chain => $chain ) : () ),
942             };
943              
944             # Add automatic accounting rule
945             if ($options{account}) {
946             my $new_options = { %options };
947             # No need to log, copy or output packets twice.
948             delete $new_options->{ipchains};
949             push @{$self->{rules}}, {
950             action => "ACCOUNT",
951             service => $service,
952             options => $new_options,
953             src => \@src,
954             dst => \@dst,
955             account_chain => $chain,
956             };
957              
958             }
959             $error--;
960             }
961             close RULES;
962             die __PACKAGE__, ": error while reading rules. Aborting\n" if $error;
963             }
964              
965             1;
966             __END__