File Coverage

lib/Rex/Commands/Iptables.pm
Criterion Covered Total %
statement 56 255 21.9
branch 8 86 9.3
condition 2 23 8.7
subroutine 13 26 50.0
pod 8 8 100.0
total 87 398 21.8


line stmt bran cond sub pod time code
1             #
2             # (c) Jan Gehring
3             #
4              
5             =head1 NAME
6              
7             Rex::Commands::Iptables - Iptable Management Commands
8              
9             =head1 DESCRIPTION
10              
11             With this Module you can manage basic Iptables rules.
12              
13             Version <= 1.0: All these functions will not be reported.
14              
15             Only I and I are idempotent.
16              
17             =head1 SYNOPSIS
18              
19             use Rex::Commands::Iptables;
20              
21             task "firewall", sub {
22             iptables_clear;
23              
24             open_port 22;
25             open_port [22, 80] => {
26             dev => "eth0",
27             };
28              
29             close_port 22 => {
30             dev => "eth0",
31             };
32             close_port "all";
33              
34             redirect_port 80 => 10080;
35             redirect_port 80 => {
36             dev => "eth0",
37             to => 10080,
38             };
39              
40             default_state_rule;
41             default_state_rule dev => "eth0";
42              
43             is_nat_gateway;
44              
45             iptables t => "nat",
46             A => "POSTROUTING",
47             o => "eth0",
48             j => "MASQUERADE";
49              
50             # The 'iptables' function also accepts long options,
51             # however, options with dashes need to be quoted
52             iptables table => "nat",
53             accept => "POSTROUTING",
54             "out-interface" => "eth0",
55             jump => "MASQUERADE";
56              
57             # Version of IP can be specified in the first argument
58             # of any function: -4 or -6 (defaults to -4)
59             iptables_clear -6;
60              
61             open_port -6, [22, 80];
62             close_port -6, "all";
63             redirect_port -6, 80 => 10080;
64             default_state_rule -6;
65              
66             iptables -6, "flush";
67             iptables -6,
68             t => "filter",
69             A => "INPUT",
70             i => "eth0",
71             m => "state",
72             state => "RELATED,ESTABLISHED",
73             j => "ACCEPT";
74             };
75              
76             =head1 EXPORTED FUNCTIONS
77              
78             =cut
79              
80             package Rex::Commands::Iptables;
81              
82 2     2   69253 use v5.12.5;
  2         20  
83 2     2   19 use warnings;
  2         4  
  2         61  
84 2     2   524 use version;
  2         2703  
  2         11  
85              
86             our $VERSION = '1.14.2.2'; # TRIAL VERSION
87              
88             require Rex::Exporter;
89 2     2   881 use Data::Dumper;
  2         7447  
  2         140  
90              
91 2     2   18 use base qw(Rex::Exporter);
  2         17  
  2         604  
92              
93 2     2   13 use vars qw(@EXPORT);
  2         5  
  2         74  
94              
95 2     2   415 use Rex::Commands::Sysctl;
  2         6  
  2         17  
96 2     2   16 use Rex::Commands::Gather;
  2         6  
  2         14  
97 2     2   14 use Rex::Commands::Fs;
  2         4  
  2         22  
98 2     2   13 use Rex::Commands::Run;
  2         4  
  2         14  
99 2     2   12 use Rex::Helper::Run;
  2         4  
  2         122  
100              
101 2     2   22 use Rex::Logger;
  2         5  
  2         15  
102              
103             @EXPORT = qw(iptables is_nat_gateway iptables_list iptables_clear
104             open_port close_port redirect_port
105             default_state_rule);
106              
107             sub iptables;
108              
109             =head2 open_port($port, $option)
110              
111             Open a port for inbound connections.
112              
113             task "firewall", sub {
114             open_port 22;
115             open_port [22, 80];
116             open_port [22, 80],
117             dev => "eth1";
118             };
119              
120             task "firewall", sub {
121             open_port 22,
122             dev => "eth1",
123             only_if => "test -f /etc/firewall.managed";
124             } ;
125              
126              
127             =cut
128              
129             sub open_port {
130 0     0 1 0 my @params = @_;
131 0         0 my $ip_version = _get_ip_version( \@params );
132 0         0 my ( $port, $option ) = @params;
133              
134 0         0 my %option_h;
135 0 0       0 if ( ref $option ne "HASH" ) {
136 0         0 ( $port, %option_h ) = @params;
137              
138 0 0       0 if ( exists $option_h{only_if} ) {
139 0         0 i_run( $option_h{only_if}, fail_ok => 1 );
140 0 0       0 if ( $? != 0 ) {
141 0         0 return;
142             }
143             }
144              
145 0         0 delete $option_h{only_if};
146 0         0 $option = {%option_h};
147             }
148 0         0 _open_or_close_port( $ip_version, "i", "I", "INPUT", "ACCEPT", $port,
149             $option );
150              
151             }
152              
153             =head2 close_port($port, $option)
154              
155             Close a port for inbound connections.
156              
157             task "firewall", sub {
158             close_port 22;
159             close_port [22, 80];
160             close_port [22, 80],
161             dev => "eth0",
162             only_if => "test -f /etc/firewall.managed";
163             };
164              
165             =cut
166              
167             sub close_port {
168 0     0 1 0 my @params = @_;
169 0         0 my $ip_version = _get_ip_version( \@params );
170 0         0 my ( $port, $option ) = @params;
171              
172 0         0 my %option_h;
173 0 0       0 if ( ref $option ne "HASH" ) {
174 0         0 ( $port, %option_h ) = @params;
175              
176 0 0       0 if ( exists $option_h{only_if} ) {
177 0         0 i_run( $option_h{only_if}, fail_ok => 1 );
178 0 0       0 if ( $? != 0 ) {
179 0         0 return;
180             }
181             }
182              
183 0         0 delete $option_h{only_if};
184 0         0 $option = {%option_h};
185             }
186              
187 0         0 _open_or_close_port( $ip_version, "i", "A", "INPUT", "DROP", $port, $option );
188              
189             }
190              
191             =head2 redirect_port($in_port, $option)
192              
193             Redirect $in_port to another local port.
194              
195             task "redirects", sub {
196             redirect_port 80 => 10080;
197             redirect_port 80 => {
198             to => 10080,
199             dev => "eth0",
200             };
201             };
202              
203             =cut
204              
205             sub redirect_port {
206 0     0 1 0 my @params = @_;
207 0         0 my $ip_version = _get_ip_version( \@params );
208 0 0       0 if ( $ip_version == -6 ) {
209 0         0 my $iptables_version = _iptables_version($ip_version);
210 0 0       0 if ( $iptables_version < v1.4.18 ) {
211 0         0 Rex::Logger::info("iptables < v1.4.18 doesn't support NAT for IPv6");
212 0         0 die("iptables < v1.4.18 doesn't support NAT for IPv6");
213             }
214             }
215              
216 0         0 my ( $in_port, $option ) = @params;
217 0         0 my @opts;
218              
219 0         0 push( @opts, "t", "nat" );
220              
221 0 0       0 if ( !ref($option) ) {
222 0         0 my $net_info = network_interfaces();
223 0         0 my @devs = keys %{$net_info};
  0         0  
224              
225 0         0 for my $dev (@devs) {
226 0         0 redirect_port(
227             $in_port,
228             {
229             dev => $dev,
230             to => $option,
231             }
232             );
233             }
234              
235 0         0 return;
236             }
237              
238 0 0       0 unless ( exists $option->{"dev"} ) {
239 0         0 my $net_info = network_interfaces();
240 0         0 my @devs = keys %{$net_info};
  0         0  
241              
242 0         0 for my $dev (@devs) {
243 0         0 $option->{"dev"} = $dev;
244 0         0 redirect_port( $in_port, $option );
245             }
246              
247 0         0 return;
248             }
249              
250 0 0       0 if ( $option->{"to"} =~ m/^\d+$/ ) {
251 0   0     0 $option->{"proto"} ||= "tcp";
252              
253             push( @opts,
254             "I", "PREROUTING", "i", $option->{"dev"},
255 0         0 "p", $option->{"proto"}, "m", $option->{"proto"} );
256             push( @opts,
257 0         0 "dport", $in_port, "j", "REDIRECT", "to-ports", $option->{"to"} );
258              
259             }
260             else {
261 0         0 Rex::Logger::info(
262             "Redirect to other hosts isn't supported right now. Please do it by hand."
263             );
264             }
265              
266 0         0 iptables $ip_version, @opts;
267             }
268              
269             =head2 iptables(@params)
270              
271             Write standard iptable comands.
272              
273             Note that there is a short form for the iptables C<--flush> option; when you
274             pass the option of C<-F|"flush"> as the only argument, the command
275             C is run on the connected host. With the two argument form of
276             C shown in the examples below, the second argument is table you want to
277             flush.
278              
279             task "firewall", sub {
280             iptables t => "nat", A => "POSTROUTING", o => "eth0", j => "MASQUERADE";
281             iptables t => "filter", i => "eth0", m => "state", state => "RELATED,ESTABLISHED", j => "ACCEPT";
282              
283             # automatically flushes all tables; equivalent to 'iptables -F'
284             iptables "flush";
285             iptables -F;
286              
287             # flush only the "filter" table
288             iptables flush => "filter";
289             iptables -F => "filter";
290             };
291              
292             # Note: options with dashes "-" need to be quoted to escape them from Perl
293             task "long_form_firewall", sub {
294             iptables table => "nat",
295             append => "POSTROUTING",
296             "out-interface" => "eth0",
297             jump => "MASQUERADE";
298             iptables table => "filter",
299             "in-interface" => "eth0",
300             match => "state",
301             state => "RELATED,ESTABLISHED",
302             jump => "ACCEPT";
303             };
304              
305             =cut
306              
307             sub iptables {
308 0     0 1 0 my @params = @_;
309 0         0 my $iptables = _get_executable( \@params );
310              
311 0 0 0     0 if ( $params[0] eq "flush" || $params[0] eq "-flush" || $params[0] eq "-F" ) {
      0        
312 0 0       0 if ( $params[1] ) {
313 0         0 i_run "$iptables -F -t $params[1]";
314             }
315             else {
316 0         0 i_run "$iptables -F";
317             }
318              
319 0         0 return;
320             }
321              
322 0         0 my $cmd = "";
323 0         0 my $n = -1;
324 0         0 while ( $params[ ++$n ] ) {
325 0         0 my ( $key, $val ) = reverse @params[ $n, $n++ ];
326              
327 0 0       0 if ( ref($key) eq "ARRAY" ) {
328 0         0 $cmd .= join( " ", @{$key} );
  0         0  
329 0         0 last;
330             }
331              
332 0 0       0 if ( length($key) == 1 ) {
333 0         0 $cmd .= "-$key $val ";
334             }
335             else {
336 0         0 $cmd .= "--$key '$val' ";
337             }
338             }
339              
340 0         0 my $output = i_run "$iptables $cmd", fail_ok => 1;
341              
342 0 0       0 if ( $? != 0 ) {
343 0         0 Rex::Logger::info( "Error setting iptable rule: $cmd", "warn" );
344 0         0 die("Error setting iptable rule: $cmd; command output: $output");
345             }
346             }
347              
348             =head2 is_nat_gateway
349              
350             This function creates a NAT gateway for the device the default route points to.
351              
352             task "make-gateway", sub {
353             is_nat_gateway;
354             is_nat_gateway -6;
355             };
356              
357             =cut
358              
359             sub is_nat_gateway {
360 0     0 1 0 my @params = @_;
361 0         0 my $ip_version = _get_ip_version( \@params );
362              
363 0         0 Rex::Logger::debug("Changing this system to a nat gateway.");
364              
365 0 0       0 if ( my $ip = can_run("ip") ) {
366              
367 0         0 my @iptables_option = ();
368              
369 0         0 my ($default_line) = i_run "$ip $ip_version r |grep ^default";
370 0         0 my ($dev) = ( $default_line =~ m/dev ([a-z0-9]+)/i );
371 0         0 Rex::Logger::debug("Default GW Device is $dev");
372              
373 0 0       0 if ( $ip_version == -6 ) {
374 0 0       0 die "NAT for IPv6 supported by iptables >= v1.4.18"
375             if _iptables_version($ip_version) < v1.4.18;
376 0         0 sysctl "net.ipv6.conf.all.forwarding", 1;
377 0         0 sysctl "net.ipv6.conf.default.forwarding", 1;
378 0         0 iptables $ip_version,
379             t => "nat",
380             A => "POSTROUTING",
381             o => $dev,
382             j => "MASQUERADE";
383             }
384             else {
385 0         0 sysctl "net.ipv4.ip_forward" => 1;
386 0         0 iptables t => "nat", A => "POSTROUTING", o => $dev, j => "MASQUERADE";
387             }
388             }
389             else {
390              
391 0         0 Rex::Logger::info("No ip command found.");
392              
393             }
394              
395             }
396              
397             =head2 default_state_rule(%option)
398              
399             Set the default state rules for the given device.
400              
401             task "firewall", sub {
402             default_state_rule(dev => "eth0");
403             };
404              
405             =cut
406              
407             sub default_state_rule {
408 0     0 1 0 my @params = @_;
409 0         0 my $ip_version = _get_ip_version( \@params );
410 0         0 my (%option) = @params;
411              
412 0 0       0 unless ( exists $option{"dev"} ) {
413 0         0 my $net_info = network_interfaces();
414 0         0 my @devs = keys %{$net_info};
  0         0  
415              
416 0         0 for my $dev (@devs) {
417 0         0 default_state_rule( dev => $dev );
418             }
419              
420 0         0 return;
421             }
422              
423             iptables $ip_version,
424             t => "filter",
425             A => "INPUT",
426 0         0 i => $option{"dev"},
427             m => "state",
428             state => "RELATED,ESTABLISHED",
429             j => "ACCEPT";
430             }
431              
432             =head2 iptables_list
433              
434             List all iptables rules.
435              
436             task "list-iptables", sub {
437             print Dumper iptables_list;
438             print Dumper iptables_list -6;
439             };
440              
441             =cut
442              
443             sub iptables_list {
444 0     0 1 0 my @params = @_;
445 0         0 my $iptables = _get_executable( \@params );
446 0         0 my @lines = i_run "$iptables-save", valid_retval => [ 0, 1 ];
447 0         0 _iptables_list(@lines);
448             }
449              
450             sub _iptables_list {
451 1     1   144 my ( %tables, $ret );
452 1         6 my @lines = @_;
453              
454 1         4 my ($current_table);
455 1         6 for my $line (@lines) {
456 10         16 chomp $line;
457              
458 10 100       22 next if ( $line eq "COMMIT" );
459 9 100       20 next if ( $line =~ m/^#/ );
460 8 100       21 next if ( $line =~ m/^:/ );
461              
462 4 100       14 if ( $line =~ m/^\*([a-z]+)$/ ) {
463 2         6 $current_table = $1;
464 2         7 $tables{$current_table} = [];
465 2         6 next;
466             }
467              
468             #my @parts = grep { ! /^\s+$/ && ! /^$/ } split (/(\-\-?[^\s]+\s[^\s]+)/i, $line);
469 2   66     39 my @parts = grep { !/^\s+$/ && !/^$/ } split( /^\-\-?|\s+\-\-?/i, $line );
  17         108  
470              
471 2         6 my @option = ();
472 2         4 for my $part (@parts) {
473 15         42 my ( $key, $value ) = split( /\s/, $part, 2 );
474              
475             #$key =~ s/^\-+//;
476 15         37 push( @option, $key => $value );
477             }
478              
479 2         6 push( @{ $ret->{$current_table} }, \@option );
  2         9  
480              
481             }
482              
483 1         10 return $ret;
484             }
485              
486             =head2 iptables_clear
487              
488             Remove all iptables rules.
489              
490             task "no-firewall", sub {
491             iptables_clear;
492             };
493              
494             =cut
495              
496             sub iptables_clear {
497 0     0 1   my @params = @_;
498 0           my $ip_version = _get_ip_version( \@params );
499 0           my %tables_of = (
500             -4 => "/proc/net/ip_tables_names",
501             -6 => "/proc/net/ip6_tables_names",
502             );
503              
504 0 0         if ( is_file("$tables_of{$ip_version}") ) {
505 0           my @tables = i_run( "cat $tables_of{$ip_version}", fail_ok => 1 );
506 0           for my $table (@tables) {
507 0           iptables $ip_version, t => $table, F => '';
508 0           iptables $ip_version, t => $table, X => '';
509             }
510             }
511              
512 0           for my $p (qw/INPUT FORWARD OUTPUT/) {
513 0           iptables $ip_version, P => $p, ["ACCEPT"];
514             }
515              
516             }
517              
518             sub _open_or_close_port {
519 0     0     my ( $ip_version, $dev_type, $push_type, $chain, $jump, $port, $option ) = @_;
520              
521 0           my @opts;
522              
523 0           push( @opts, "t", "filter", "$push_type", "$chain" );
524              
525 0 0         unless ( exists $option->{"dev"} ) {
526 0           my $net_info = network_interfaces();
527 0           my @dev = keys %{$net_info};
  0            
528 0           $option->{"dev"} = \@dev;
529             }
530              
531 0 0 0       if ( exists $option->{"dev"} && !ref( $option->{"dev"} ) ) {
    0          
532 0           push( @opts, "$dev_type", $option->{"dev"} );
533             }
534             elsif ( ref( $option->{"dev"} ) eq "ARRAY" ) {
535 0           for my $dev ( @{ $option->{"dev"} } ) {
  0            
536 0           my $new_option = $option;
537 0           $new_option->{"dev"} = $dev;
538              
539 0           _open_or_close_port( $ip_version, $dev_type, $push_type, $chain, $jump,
540             $port, $new_option );
541             }
542              
543 0           return;
544             }
545              
546 0 0         if ( exists $option->{"proto"} ) {
547 0           push( @opts, "p", $option->{"proto"} );
548 0           push( @opts, "m", $option->{"proto"} );
549             }
550             else {
551 0           push( @opts, "p", "tcp" );
552 0           push( @opts, "m", "tcp" );
553             }
554              
555 0 0         if ( $port eq "all" ) {
556 0           push( @opts, "j", "$jump" );
557             }
558             else {
559 0 0         if ( ref($port) eq "ARRAY" ) {
560 0           for my $port_num ( @{$port} ) {
  0            
561 0           _open_or_close_port( $ip_version, $dev_type, $push_type, $chain, $jump,
562             $port_num, $option );
563             }
564 0           return;
565             }
566              
567 0           push( @opts, "dport", $port );
568 0           push( @opts, "j", $jump );
569             }
570              
571 0 0         if ( _rule_exists( $ip_version, @opts ) ) {
572 0           Rex::Logger::debug("iptables rule already exists. skipping...");
573 0           return;
574             }
575              
576 0           iptables $ip_version, @opts;
577              
578             }
579              
580             sub _rule_exists {
581 0     0     my ( $ip_version, @check_rule ) = @_;
582              
583 0 0         if ( $check_rule[0] eq "t" ) {
584 0           shift @check_rule;
585 0           shift @check_rule;
586             }
587              
588 0 0 0       if ( $check_rule[0] eq "D" || $check_rule[0] eq "A" ) {
589 0           shift @check_rule;
590             }
591              
592 0           my $str_check_rule = join( " ", "A", @check_rule );
593              
594 0           my $current_tables = iptables_list($ip_version);
595 0 0         if ( exists $current_tables->{filter} ) {
596 0           for my $rule ( @{ $current_tables->{filter} } ) {
  0            
597 0           my $str_rule = join( " ", @{$rule} );
  0            
598 0           $str_rule =~ s/\s$//;
599              
600 0           Rex::Logger::debug("comparing: '$str_rule' == '$str_check_rule'");
601 0 0         if ( $str_rule eq $str_check_rule ) {
602 0           return 1;
603             }
604             }
605             }
606              
607 0           return 0;
608             }
609              
610             sub _get_ip_version {
611 0     0     my ($params) = @_;
612 0 0 0       if ( defined $params->[0] && !ref $params->[0] ) {
613 0 0 0       if ( $params->[0] eq "-4" || $params->[0] eq "-6" ) {
614 0           return shift @$params;
615             }
616             }
617 0           return -4;
618             }
619              
620             sub _get_executable {
621 0     0     my ($params) = @_;
622 0           my $ip_version = _get_ip_version($params);
623 0           my $cache = Rex::get_cache();
624 0           my $cache_key_name = "iptables.$ip_version.executable";
625 0 0         return $cache->get($cache_key_name) if $cache->valid($cache_key_name);
626              
627 0 0         my $binary = $ip_version == -6 ? "ip6tables" : "iptables";
628 0           my $executable = can_run($binary);
629 0 0         die "Can't find $binary in PATH" if $executable eq '';
630 0           $cache->set( $cache_key_name, $executable );
631              
632 0           return $executable;
633             }
634              
635             sub _iptables_version {
636 0     0     my @params = @_;
637 0           my $ip_version = _get_ip_version( \@params );
638 0           my $cache = Rex::get_cache();
639 0           my $cache_key_name = "iptables.$ip_version.version";
640 0 0         return version->parse( $cache->get($cache_key_name) )
641             if $cache->valid($cache_key_name);
642              
643 0           my $iptables = _get_executable( \@params );
644 0           my $out = i_run( "$iptables -V", fail_ok => 1 );
645 0 0         if ( $out =~ /v([.\d]+)/ms ) {
646 0           my $version = version->parse($1);
647 0           $cache->set( $cache_key_name, "$version" );
648 0           return $version;
649             }
650             else {
651 0           die "Can't parse `$iptables -V' output `$out'";
652             }
653             }
654              
655             1;