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