our
$VERSION
=
'1.16.0'
;
@EXPORT
=
qw(iptables is_nat_gateway iptables_list iptables_clear
open_port close_port redirect_port
default_state_rule)
;
sub
iptables;
sub
open_port {
my
@params
=
@_
;
my
$ip_version
= _get_ip_version( \
@params
);
my
(
$port
,
$option
) =
@params
;
my
%option_h
;
if
(
ref
$option
ne
"HASH"
) {
(
$port
,
%option_h
) =
@params
;
if
(
exists
$option_h
{only_if} ) {
i_run(
$option_h
{only_if},
fail_ok
=> 1 );
if
( $? != 0 ) {
return
;
}
}
delete
$option_h
{only_if};
$option
= {
%option_h
};
}
_open_or_close_port(
$ip_version
,
"i"
,
"I"
,
"INPUT"
,
"ACCEPT"
,
$port
,
$option
);
}
sub
close_port {
my
@params
=
@_
;
my
$ip_version
= _get_ip_version( \
@params
);
my
(
$port
,
$option
) =
@params
;
my
%option_h
;
if
(
ref
$option
ne
"HASH"
) {
(
$port
,
%option_h
) =
@params
;
if
(
exists
$option_h
{only_if} ) {
i_run(
$option_h
{only_if},
fail_ok
=> 1 );
if
( $? != 0 ) {
return
;
}
}
delete
$option_h
{only_if};
$option
= {
%option_h
};
}
_open_or_close_port(
$ip_version
,
"i"
,
"A"
,
"INPUT"
,
"DROP"
,
$port
,
$option
);
}
sub
redirect_port {
my
@params
=
@_
;
my
$ip_version
= _get_ip_version( \
@params
);
if
(
$ip_version
== -6 ) {
my
$iptables_version
= _iptables_version(
$ip_version
);
if
(
$iptables_version
< v1.4.18 ) {
Rex::Logger::info(
"iptables < v1.4.18 doesn't support NAT for IPv6"
);
die
(
"iptables < v1.4.18 doesn't support NAT for IPv6"
);
}
}
my
(
$in_port
,
$option
) =
@params
;
my
@opts
;
push
(
@opts
,
"t"
,
"nat"
);
if
( !
ref
(
$option
) ) {
my
$net_info
= network_interfaces();
my
@devs
=
keys
%{
$net_info
};
for
my
$dev
(
@devs
) {
redirect_port(
$in_port
,
{
dev
=>
$dev
,
to
=>
$option
,
}
);
}
return
;
}
unless
(
exists
$option
->{
"dev"
} ) {
my
$net_info
= network_interfaces();
my
@devs
=
keys
%{
$net_info
};
for
my
$dev
(
@devs
) {
$option
->{
"dev"
} =
$dev
;
redirect_port(
$in_port
,
$option
);
}
return
;
}
if
(
$option
->{
"to"
} =~ m/^\d+$/ ) {
$option
->{
"proto"
} ||=
"tcp"
;
push
(
@opts
,
"I"
,
"PREROUTING"
,
"i"
,
$option
->{
"dev"
},
"p"
,
$option
->{
"proto"
},
"m"
,
$option
->{
"proto"
} );
push
(
@opts
,
"dport"
,
$in_port
,
"j"
,
"REDIRECT"
,
"to-ports"
,
$option
->{
"to"
} );
}
else
{
Rex::Logger::info(
"Redirect to other hosts isn't supported right now. Please do it by hand."
);
}
iptables
$ip_version
,
@opts
;
}
sub
iptables {
my
@params
=
@_
;
my
$iptables
= _get_executable( \
@params
);
if
(
$params
[0] eq
"flush"
||
$params
[0] eq
"-flush"
||
$params
[0] eq
"-F"
) {
if
(
$params
[1] ) {
i_run
"$iptables -F -t $params[1]"
;
}
else
{
i_run
"$iptables -F"
;
}
return
;
}
my
$cmd
=
""
;
my
$n
= -1;
while
(
$params
[ ++
$n
] ) {
my
(
$key
,
$val
) =
reverse
@params
[
$n
,
$n
++ ];
if
(
ref
(
$key
) eq
"ARRAY"
) {
$cmd
.=
join
(
" "
, @{
$key
} );
last
;
}
if
(
length
(
$key
) == 1 ) {
$cmd
.=
"-$key $val "
;
}
else
{
$cmd
.=
"--$key '$val' "
;
}
}
my
$output
= i_run
"$iptables $cmd"
,
fail_ok
=> 1;
if
( $? != 0 ) {
Rex::Logger::info(
"Error setting iptable rule: $cmd"
,
"warn"
);
die
(
"Error setting iptable rule: $cmd; command output: $output"
);
}
}
sub
is_nat_gateway {
my
@params
=
@_
;
my
$ip_version
= _get_ip_version( \
@params
);
Rex::Logger::debug(
"Changing this system to a nat gateway."
);
if
(
my
$ip
= can_run(
"ip"
) ) {
my
@iptables_option
= ();
my
(
$default_line
) = i_run
"$ip $ip_version r |grep ^default"
;
my
(
$dev
) = (
$default_line
=~ m/dev ([a-z0-9]+)/i );
Rex::Logger::debug(
"Default GW Device is $dev"
);
if
(
$ip_version
== -6 ) {
die
"NAT for IPv6 supported by iptables >= v1.4.18"
if
_iptables_version(
$ip_version
) < v1.4.18;
sysctl
"net.ipv6.conf.all.forwarding"
, 1;
sysctl
"net.ipv6.conf.default.forwarding"
, 1;
iptables
$ip_version
,
t
=>
"nat"
,
A
=>
"POSTROUTING"
,
o
=>
$dev
,
j
=>
"MASQUERADE"
;
}
else
{
sysctl
"net.ipv4.ip_forward"
=> 1;
iptables
t
=>
"nat"
,
A
=>
"POSTROUTING"
,
o
=>
$dev
,
j
=>
"MASQUERADE"
;
}
}
else
{
Rex::Logger::info(
"No ip command found."
);
}
}
sub
default_state_rule {
my
@params
=
@_
;
my
$ip_version
= _get_ip_version( \
@params
);
my
(
%option
) =
@params
;
unless
(
exists
$option
{
"dev"
} ) {
my
$net_info
= network_interfaces();
my
@devs
=
keys
%{
$net_info
};
for
my
$dev
(
@devs
) {
default_state_rule(
dev
=>
$dev
);
}
return
;
}
iptables
$ip_version
,
t
=>
"filter"
,
A
=>
"INPUT"
,
i
=>
$option
{
"dev"
},
m
=>
"state"
,
state
=>
"RELATED,ESTABLISHED"
,
j
=>
"ACCEPT"
;
}
sub
iptables_list {
my
@params
=
@_
;
my
$iptables
= _get_executable( \
@params
);
my
@lines
= i_run
"$iptables-save"
,
valid_retval
=> [ 0, 1 ];
_iptables_list(
@lines
);
}
sub
_iptables_list {
my
(
%tables
,
$ret
);
my
@lines
=
@_
;
my
(
$current_table
);
for
my
$line
(
@lines
) {
chomp
$line
;
next
if
(
$line
eq
"COMMIT"
);
next
if
(
$line
=~ m/^
next
if
(
$line
=~ m/^:/ );
if
(
$line
=~ m/^\*([a-z]+)$/ ) {
$current_table
= $1;
$tables
{
$current_table
} = [];
next
;
}
my
@parts
=
grep
{ !/^\s+$/ && !/^$/ }
split
( /^\-\-?|\s+\-\-?/i,
$line
);
my
@option
= ();
for
my
$part
(
@parts
) {
my
(
$key
,
$value
) =
split
( /\s/,
$part
, 2 );
push
(
@option
,
$key
=>
$value
);
}
push
( @{
$ret
->{
$current_table
} }, \
@option
);
}
return
$ret
;
}
sub
iptables_clear {
my
@params
=
@_
;
my
$ip_version
= _get_ip_version( \
@params
);
my
%tables_of
= (
-4
=>
"/proc/net/ip_tables_names"
,
-6
=>
"/proc/net/ip6_tables_names"
,
);
if
( is_file(
"$tables_of{$ip_version}"
) ) {
my
@tables
= i_run(
"cat $tables_of{$ip_version}"
,
fail_ok
=> 1 );
for
my
$table
(
@tables
) {
iptables
$ip_version
,
t
=>
$table
,
F
=>
''
;
iptables
$ip_version
,
t
=>
$table
,
X
=>
''
;
}
}
for
my
$p
(
qw/INPUT FORWARD OUTPUT/
) {
iptables
$ip_version
,
P
=>
$p
, [
"ACCEPT"
];
}
}
sub
_open_or_close_port {
my
(
$ip_version
,
$dev_type
,
$push_type
,
$chain
,
$jump
,
$port
,
$option
) =
@_
;
my
@opts
;
push
(
@opts
,
"t"
,
"filter"
,
"$push_type"
,
"$chain"
);
unless
(
exists
$option
->{
"dev"
} ) {
my
$net_info
= network_interfaces();
my
@dev
=
keys
%{
$net_info
};
$option
->{
"dev"
} = \
@dev
;
}
if
(
exists
$option
->{
"dev"
} && !
ref
(
$option
->{
"dev"
} ) ) {
push
(
@opts
,
"$dev_type"
,
$option
->{
"dev"
} );
}
elsif
(
ref
(
$option
->{
"dev"
} ) eq
"ARRAY"
) {
for
my
$dev
( @{
$option
->{
"dev"
} } ) {
my
$new_option
=
$option
;
$new_option
->{
"dev"
} =
$dev
;
_open_or_close_port(
$ip_version
,
$dev_type
,
$push_type
,
$chain
,
$jump
,
$port
,
$new_option
);
}
return
;
}
if
(
exists
$option
->{
"proto"
} ) {
push
(
@opts
,
"p"
,
$option
->{
"proto"
} );
push
(
@opts
,
"m"
,
$option
->{
"proto"
} );
}
else
{
push
(
@opts
,
"p"
,
"tcp"
);
push
(
@opts
,
"m"
,
"tcp"
);
}
if
(
$port
eq
"all"
) {
push
(
@opts
,
"j"
,
"$jump"
);
}
else
{
if
(
ref
(
$port
) eq
"ARRAY"
) {
for
my
$port_num
( @{
$port
} ) {
_open_or_close_port(
$ip_version
,
$dev_type
,
$push_type
,
$chain
,
$jump
,
$port_num
,
$option
);
}
return
;
}
push
(
@opts
,
"dport"
,
$port
);
push
(
@opts
,
"j"
,
$jump
);
}
if
( _rule_exists(
$ip_version
,
@opts
) ) {
Rex::Logger::debug(
"iptables rule already exists. skipping..."
);
return
;
}
iptables
$ip_version
,
@opts
;
}
sub
_rule_exists {
my
(
$ip_version
,
@check_rule
) =
@_
;
if
(
$check_rule
[0] eq
"t"
) {
shift
@check_rule
;
shift
@check_rule
;
}
if
(
$check_rule
[0] eq
"D"
||
$check_rule
[0] eq
"A"
) {
shift
@check_rule
;
}
my
$str_check_rule
=
join
(
" "
,
"A"
,
@check_rule
);
my
$current_tables
= iptables_list(
$ip_version
);
if
(
exists
$current_tables
->{filter} ) {
for
my
$rule
( @{
$current_tables
->{filter} } ) {
my
$str_rule
=
join
(
" "
, @{
$rule
} );
$str_rule
=~ s/\s$//;
Rex::Logger::debug(
"comparing: '$str_rule' == '$str_check_rule'"
);
if
(
$str_rule
eq
$str_check_rule
) {
return
1;
}
}
}
return
0;
}
sub
_get_ip_version {
my
(
$params
) =
@_
;
if
(
defined
$params
->[0] && !
ref
$params
->[0] ) {
if
(
$params
->[0] eq
"-4"
||
$params
->[0] eq
"-6"
) {
return
shift
@$params
;
}
}
return
-4;
}
sub
_get_executable {
my
(
$params
) =
@_
;
my
$ip_version
= _get_ip_version(
$params
);
my
$cache
= Rex::get_cache();
my
$cache_key_name
=
"iptables.$ip_version.executable"
;
return
$cache
->get(
$cache_key_name
)
if
$cache
->valid(
$cache_key_name
);
my
$binary
=
$ip_version
== -6 ?
"ip6tables"
:
"iptables"
;
my
$executable
= can_run(
$binary
);
die
"Can't find $binary in PATH"
if
$executable
eq '';
$cache
->set(
$cache_key_name
,
$executable
);
return
$executable
;
}
sub
_iptables_version {
my
@params
=
@_
;
my
$ip_version
= _get_ip_version( \
@params
);
my
$cache
= Rex::get_cache();
my
$cache_key_name
=
"iptables.$ip_version.version"
;
return
version->parse(
$cache
->get(
$cache_key_name
) )
if
$cache
->valid(
$cache_key_name
);
my
$iptables
= _get_executable( \
@params
);
my
$out
= i_run(
"$iptables -V"
,
fail_ok
=> 1 );
if
(
$out
=~ /v([.\d]+)/ms ) {
my
$version
= version->parse($1);
$cache
->set(
$cache_key_name
,
"$version"
);
return
$version
;
}
else
{
die
"Can't parse `$iptables -V' output `$out'"
;
}
}
1;