Net-OpenVPN-Manager/lib/Net/OpenVPN/Manager/Plugin/Iptables.pm
package Net::OpenVPN::Manager::Plugin::Iptables;
use namespace::autoclean;
use Moose;
use AnyEvent::Util qw(fork_call);
use IPTables::ChainMgr;
use Net::IP qw(ip_is_ipv4 ip_is_ipv6 ip_splitprefix);
use Net::OpenVPN::Manager::Plugin;
use MooseX::Types::Moose qw(Str Int ArrayRef HashRef);
use MooseX::Types::Structured qw(Dict Optional);
use Digest::MD5 qw(md5_base64);
use JSON;
with 'Net::OpenVPN::Manager::Startable';
with 'Net::OpenVPN::Manager::Addressable';
with 'Net::OpenVPN::Manager::Disconnectable';
with 'Net::OpenVPN::Manager::Reloadable';
has 'chain_prefix' => (
is => 'ro',
isa => 'Str',
default => "openvpn-users-",
);
has 'user_chain_prefix' => (
is => 'ro',
isa => 'Str',
default => "user-",
);
has 'generate_profiles' => (
is => 'ro',
isa => 'Bool',
default => 1,
);
has 'hash_chain_name' => (
is => 'ro',
isa => 'Bool',
default => 0,
);
has 'final_drop' => (
is => 'ro',
isa => 'Bool',
default => 1,
);
has 'profile_attrs' => (
is => 'ro',
isa => 'ArrayRef[Str]',
traits => ['Array'],
default => sub { [] },
handles => {
all_profile_attrs => 'elements',
},
);
has 'default_profiles' => (
is => 'ro',
isa => 'ArrayRef[Str]',
traits => ['Array'],
default => sub { [] },
handles => {
all_default_profiles => 'elements',
},
);
has 'profiles' => (
is => 'ro',
traits => ['Hash'],
isa => HashRef[
ArrayRef|Dict["desc" => Optional[Str], "prio" => Optional[Int], "entries" => ArrayRef, "hidden" => Optional[Int]]],
default => sub { {} },
handles => {
get_profile => 'get',
profile_names => 'keys',
has_profile => 'exists',
}
);
sub _chain_name {
my ($self, $name) = @_;
return $self->hash_chain_name || length($name) > 28 ? md5_base64($name) : $name;
}
sub _main_chain_name {
my ($self, $side) = @_;
my $name = $self->chain_prefix.$side;
return $self->_chain_name($name);
}
sub _profile_chain_name {
my ($self, $profile, $side) = @_;
my $name = $profile.'-'.$side;
return $self->_chain_name($name);
}
sub _user_chain_name {
my ($self, $client, $side) = @_;
my $name = $self->user_chain_prefix.$client->username.$client->cid.$side;
return $self->_chain_name($name);
}
sub _create_or_flush {
my ($self, $name, $ipt_obj) = @_;
my ($rv) = $ipt_obj->chain_exists('filter', $name);
if ($rv) {
$ipt_obj->flush_chain('filter', $name);
} else {
my ($rv) = $ipt_obj->create_chain('filter', $name);
}
}
sub _create_profiles {
my ($self, $ipt4_obj, $ipt6_obj) = @_;
foreach my $name ($self->profile_names) {
my $profile = $self->get_profile($name);
my $inchain = $self->_profile_chain_name($name, 'in');
my $outchain = $self->_profile_chain_name($name, 'out');
$self->_create_or_flush($inchain, $ipt4_obj);
$self->_create_or_flush($outchain, $ipt4_obj);
$self->_create_or_flush($inchain, $ipt6_obj);
$self->_create_or_flush($outchain, $ipt6_obj);
foreach my $entry (@{$profile->{entries}}) {
unless (defined $entry->{dst} || defined $entry->{src}) {
$self->log("Rules must have src or dst defined");
next;
}
if (defined $entry->{dst} && defined $entry->{src}) {
$self->log("Rules cannot have src and dst defined");
next;
}
my ($ip) = split(/\//, $entry->{dst} || $entry->{src});
my $default;
my $ipt_obj;
if (ip_is_ipv6($ip)) {
$default = "::/0";
$ipt_obj = $ipt6_obj;
} elsif (ip_is_ipv4($ip)) {
$default = "0.0.0.0/0";
$ipt_obj = $ipt4_obj;
} else {
$self->log("Rule does not contain valid ipv4 nor ipv6 address : $ip");
next;
}
$ipt_obj->add_ip_rule(
defined $entry->{src} ? $entry->{src} : $default,
defined $entry->{dst} ? $entry->{dst} : $default,
-1,
'filter',
defined $entry->{src} ? $outchain : $inchain,
$entry->{target} || 'ACCEPT',
$entry->{extras} || {}
);
}
}
}
sub _get_user_profiles {
my ($self, $client) = @_;
my @profiles;
# defaults
foreach my $profile ($self->all_default_profiles) {
push(@profiles, $profile) if $self->has_profile($profile);
}
# user profiles
foreach my $attr ($self->all_profile_attrs) {
foreach my $profile (@{$client->get_attr($attr)}) {
push(@profiles, $profile) if $self->has_profile($profile);
}
}
return sort { ($self->get_profile($a)->{prio} || 100) <=> ($self->get_profile($b)->{prio} || 100) } @profiles;
}
sub start {
my ($self) = @_;
my $cv = AE::cv;
$self->manager->add_dispatcher(sub {
my $env = shift;
'/api/iptables/...' => sub {
'' => sub {
$self->manager->middleware_apikey;
},
'GET + /profiles + ?hidden~' => sub {
my $hidden = $_[1];
my @profiles = grep { !$self->get_profile($_)->{hidden} || $hidden } $self->profile_names;
[200, ['Content-type' => 'application/json'], [encode_json({ profiles => [@profiles]})]]
},
'GET + /profile/*' => sub {
my $name = $_[1];
unless ($self->has_profile($name)) {
[404, ['Content-type' => 'application/json'], [encode_json({ error => "profile does not exist" })]]
} else {
[200, ['Content-type' => 'application/json'], [encode_json({ name => $name, content => $self->get_profile($name) })]]
}
},
},
});
fork_call {
my $rv;
my $ipt4_obj = IPTables::ChainMgr->new();
my $ipt6_obj = IPTables::ChainMgr->new('use_ipv6' => 1);
# create main chains and jumps
foreach (('in', 'out')) {
my $chain = $self->_main_chain_name($_);
# v4
($rv) = $ipt4_obj->chain_exists('filter', $chain);
unless ($rv) {
($rv) = $ipt4_obj->create_chain('filter', $chain);
$ipt4_obj->add_ip_rule('0.0.0.0/0', '0.0.0.0/0', -1, 'filter', 'FORWARD', $chain, { "intf_$_" => $self->manager->netdev });
if ($self->final_drop) {
$ipt4_obj->add_ip_rule('0.0.0.0/0', '0.0.0.0/0', -1, 'filter', 'FORWARD', 'REJECT', { "intf_$_" => $self->manager->netdev });
}
}
# v6
($rv) = $ipt6_obj->chain_exists('filter', $chain);
unless ($rv) {
($rv) = $ipt6_obj->create_chain('filter', $chain);
$ipt6_obj->add_ip_rule('::/0', '::/0', -1, 'filter', 'FORWARD', $chain, { "intf_$_" => $self->manager->netdev });
if ($self->final_drop) {
$ipt6_obj->add_ip_rule('::/0', '::/0', -1, 'filter', 'FORWARD', 'REJECT', { "intf_$_" => $self->manager->netdev });
}
}
}
$self->_create_profiles($ipt4_obj, $ipt6_obj) if $self->generate_profiles;
return $rv;
} sub {
if ($@) {
$self->log($@);
$cv->send(PLUG_ERROR);
return;
}
$cv->send(PLUG_OK);
};
return $cv;
}
sub address {
my ($self, $client, $addr, $prio) = @_;
my $cv = AE::cv;
my $ipt_obj = IPTables::ChainMgr->new('use_ipv6' => ip_is_ipv6($addr));
my $default = ip_is_ipv6($addr) ? '::/0' : '0.0.0.0/0';
my @profiles = $self->_get_user_profiles($client);
my $inmainchain = $self->_main_chain_name('in');
my $outmainchain = $self->_main_chain_name('out');
my $inchain = $self->_user_chain_name($client, 'in');
my $outchain = $self->_user_chain_name($client, 'out');
$self->log("profiles ".join(",", @profiles), $client);
fork_call {
$self->_create_or_flush($inchain, $ipt_obj);
$self->_create_or_flush($outchain, $ipt_obj);
$ipt_obj->add_ip_rule($addr, $default, -1, 'filter', $inmainchain, $inchain);
$ipt_obj->add_ip_rule($default, $addr, -1, 'filter', $outmainchain, $outchain);
foreach my $profile (reverse @profiles) {
my $inprofilechain = $self->_profile_chain_name($profile, 'in');
my $outprofilechain = $self->_profile_chain_name($profile, 'out');
$ipt_obj->add_jump_rule('filter', $inchain, 1, $inprofilechain);
$ipt_obj->add_jump_rule('filter', $outchain, 1, $outprofilechain);
}
if ($self->final_drop) {
$ipt_obj->add_ip_rule($default, $default, -1, 'filter', $inchain, 'REJECT');
$ipt_obj->add_ip_rule($default, $default, -1, 'filter', $outchain, 'REJECT');
}
} sub {
if ($@) {
$self->log($@);
$cv->send(PLUG_ERROR);
return;
}
$cv->send(PLUG_OK);
};
return $cv;
}
sub disconnect {
my ($self, $client) = @_;
my $cv = AE::cv;
my $inmainchain = $self->_main_chain_name('in');
my $outmainchain = $self->_main_chain_name('out');
my $inchain = $self->_user_chain_name($client, 'in');
my $outchain = $self->_user_chain_name($client, 'out');
$self->log("clearing rules and chains", $client);
fork_call {
foreach my $addr ($client->all_addresses) {
my $ipt_obj = IPTables::ChainMgr->new('use_ipv6' => ip_is_ipv6($addr));
my $default = ip_is_ipv6($addr) ? '::/0' : '0.0.0.0/0';
$ipt_obj->delete_ip_rule($addr, $default, 'filter', $inmainchain, $inchain);
$ipt_obj->delete_ip_rule($default, $addr, 'filter', $outmainchain, $outchain);
$ipt_obj->delete_chain('filter', $inmainchain, $inchain);
$ipt_obj->delete_chain('filter', $outmainchain, $outchain);
}
} sub {
if ($@) {
$self->log($@);
$cv->send(PLUG_ERROR);
return;
}
$cv->send(PLUG_OK);
};
return $cv;
}
sub reload {
my ($self) = @_;
my $cv = AE::cv;
my $ipt4_obj = IPTables::ChainMgr->new();
my $ipt6_obj = IPTables::ChainMgr->new('use_ipv6' => 1);
$self->log("Regenerate profile chains");
fork_call {
$self->_create_profiles($ipt4_obj, $ipt6_obj);
$self->log("Profile chains regenerated");
} sub {
if ($@) {
$self->log($@);
$cv->send(PLUG_ERROR);
return;
}
$cv->send(PLUG_OK);
};
return $cv;
}
__PACKAGE__->meta->make_immutable;
1;