package SrvMngr::Controller::Wireguard; #---------------------------------------------------------------------- # heading : VPN # description : Wireguard # navigation : 6500 100 # # name : wireguard, method : get, url : /wireguard, ctlact : wireguard#main # name : wireguardd, method : post, url : /wireguard, ctlact : wireguard#do_display # name : wireguardu, method : post, url : /wireguard2, ctlact : wireguard#do_action # name : wireguardr, method : get, url : /wireguard2, ctlact : wireguard#do_display # # routes : end #---------------------------------------------------------------------- use strict; use warnings; use Mojo::Base 'Mojolicious::Controller'; use Locale::gettext; use SrvMngr::I18N; use SrvMngr qw( theme_list init_session is_normal_password ); use esmith::ConfigDB::UTF8; use Net::IP; my $adb; my $cdb; my $wdb; my $ndb; sub main { my $c = shift; $c->app->log->info($c->log_req); my %wrg_datas = (); $wdb = esmith::ConfigDB::UTF8->open('wireguard') || esmith::ConfigDB::UTF8->create('wireguard'); my $title = $c->l('wrg_FORM_TITLE'); $wrg_datas{'trt'} = 'LST'; $cdb = esmith::ConfigDB::UTF8->open() || die "Couldn't open config DB\n"; my $wg = $cdb->get('wg-quick@wg0'); $wrg_datas{'wgpub'} = $wg->prop('public'); $wrg_datas{'wgip'} = $wg->prop('ip'); $wrg_datas{'wgmask'} = $wg->prop('mask'); $wrg_datas{'wgport'} = $wg->prop('UDPPort'); $wrg_datas{'sstatus'} = $wg->prop('status'); my @wgstatus = `/usr/bin/wg show wg0 dump`; my $type = 'wg0'; my @wgconf = $wdb->get_all_by_prop(type => $type); $c->stash( title => $title, wrg_datas => \%wrg_datas, wgstatus => \@wgstatus, wgconf => \@wgconf ); $c->render(template => 'wireguard'); } ## end sub main sub do_display { my $c = shift; $c->app->log->info($c->log_req); my $rt = $c->current_route; my $trt = ($c->param('trt') || ''); my $wgconf = $c->param('Wgconf') || ''; my %wrg_datas = (); my $title = $c->l('wrg_FORM_TITLE'); my $modul = ''; $adb = esmith::AccountsDB->open() || die "Couldn't open accounts DB\ndb"; $cdb = esmith::ConfigDB::UTF8->open() || die "Couldn't open config DB\n"; $wdb = esmith::ConfigDB::UTF8->open('wireguard') || esmith::ConfigDB::UTF8->create('wireguard'); #$ndb = esmith::NetworksDB->open_ro || die "Error opening networks DB\n"; $wrg_datas{'trt'} = $trt; if ($trt eq 'QRC') { $wrg_datas{'wgconf'} = $wgconf; } if ($trt eq 'MOD') { $wrg_datas{'wgconf'} = $wgconf; my $rec = $wdb->get($wgconf); if ($rec) { $wrg_datas{'info'} = $rec->prop('info') || ''; $wrg_datas{'allowedips'} = $rec->prop('allowedips') || ''; $wrg_datas{'private'} = $rec->prop('private') || ''; $wrg_datas{'public'} = $rec->prop('public') || ''; $wrg_datas{'account'} = $rec->prop('user') || ''; $wrg_datas{'status'} = $rec->prop('status') || ''; $wrg_datas{'dns'} = $rec->prop('dns') || ''; } ## end if ($rec) } ## end if ($trt eq 'MOD') if ($trt eq 'REM') { $wrg_datas{'wgconf'} = $wgconf; my $rec = $wdb->get($wgconf); $wrg_datas{'wgcomment'} = $rec->prop('info') || ''; } if ($trt eq 'NEW') { # nothing for a new client } if ($trt eq 'UPD') { my $wg = $cdb->get('wg-quick@wg0'); $wrg_datas{'ip'} = $wg->prop('ip'); $wrg_datas{'mask'} = $wg->prop('mask'); $wrg_datas{'private'} = $wg->prop('private'); $wrg_datas{'public'} = $wg->prop('public'); $wrg_datas{'status'} = $wg->prop('status'); } ## end if ($trt eq 'UPD') if ($trt eq 'LST') { my @wgss = $adb->wgss(); $c->stash(wgss => \@wgss); } $c->stash(title => $title, modul => $modul, wrg_datas => \%wrg_datas); $c->render(template => 'wireguard'); } ## end sub do_display sub do_action { my $c = shift; $c->app->log->info($c->log_req); my $rt = $c->current_route; my $trt = ($c->param('trt') || ''); my %wrg_datas = (); my $title = $c->l('wrg_FORM_TITLE'); $wrg_datas{'trt'} = $trt; my $result = ''; my $res = ''; $adb = esmith::AccountsDB->open() || die "Couldn't open accounts DB\ndb"; $cdb = esmith::ConfigDB::UTF8->open() || die "Couldn't open config DB\n"; $wdb = esmith::ConfigDB::UTF8->open('wireguard') || esmith::ConfigDB::UTF8->create('wireguard'); $ndb = esmith::NetworksDB->open_ro || die "Error opening networks DB\n"; if ($trt eq 'QRC') { # NEVER } if ($trt eq 'LST') { # NEVER } if ($trt eq 'MOD') { $wrg_datas{'wgconf'} = $c->param('Wgconf'); # controls $res = 'OK'; # no controls here... $result .= $res unless $res eq 'OK'; if (!$result) { $res = performModifyClient($c); $result .= $res unless $res eq 'OK'; if (!$result) { $result = $c->l('wrg_SUCCESSFULLY_MODIFIED_CONF'); } } ## end if (!$result) } ## end if ($trt eq 'MOD') if ($trt eq 'REM') { if ($c->param("cancel")) { $c->stash(error => $c->l('wrg_CANCELLED')); $c->redirect_to('/wireguard'); } # controls $res = 'OK'; # no controls here... $result .= $res unless $res eq 'OK'; if (!$result) { $res = performRemoveClient($c); $result .= $res unless $res eq 'OK'; if (!$result) { $result = $c->l('wrg_SUCCESSFULLY_REMOVED_CONF'); } } ## end if (!$result) } ## end if ($trt eq 'REM') if ($trt eq 'NEW') { # controls $res = 'OK'; # no controls here... $result .= $res unless $res eq 'OK'; if (!$result) { $res = performCreateClient($c); $result .= $res unless $res eq 'OK'; if (!$result) { $result = $c->l('wrg_SUCCESSFULLY_ADDED_CONF'); } } ## end if (!$result) } ## end if ($trt eq 'NEW') if ($trt eq 'UPD') { # controls $res = 'OK'; # no controls here... $result .= $res unless $res eq 'OK'; if (!$result) { $res = performUpdateConfig($c); $result .= $res unless $res eq 'OK'; if (!$result) { $result = $c->l('wrg_SUCCESSFULLY_UPDATED_CONF'); } } ## end if (!$result) } ## end if ($trt eq 'UPD') # common parts if ($res ne 'OK') { $c->stash(error => $result); $c->stash(title => $title, wrg_datas => \%wrg_datas); return $c->render('wireguard'); } #force reload as successfull (for Main) $wdb = esmith::ConfigDB::UTF8->open('wireguard'); my $message = "'Wireguard' update ($trt) DONE"; $c->app->log->info($message); $c->flash(success => $result); $c->redirect_to('/wireguard'); } ## end sub do_action # action for 'MOD' sub performModifyClient { my $c = shift; my $msg = "OK"; my $wgacc = $c->param('Wgconf'); my $account = $c->param('Account'); my $private = $c->param('Private') || ''; my $public = $c->param('Public') || ''; my $info = $c->param('Info'); my $status = $c->param('Status') || 'disabled'; my $allowedips = $c->param('Allowedips') || ''; #todo validate fields # Untaint info and account before use in system() ($info) = $info =~ /([A-Za-z0-9_\-. ]+)/; # trim both ends $info =~ s/^ +| +$//g; ($account) = $account =~ /([A-Za-z0-9_-]+)/; return $c->l('wrg_ERROR_FIELD_CONTENT') unless ($account and $info); my %props = ( 'user' => $account, 'private' => $private, 'public' => $public, 'info' => $info, 'status' => $status, 'allowedips' => $allowedips ); $wdb->get($wgacc)->merge_props(%props) or $msg = "Error occurred while modifying pseudonym in database."; # Untaint before use in system() ($wgacc) = ($wgacc =~ /(\d+\.+\d+\.+\d+\.+\d+\.+\/\d+\.+)/); system("/sbin/e-smith/signal-event", "wireguard-user-modify", "$wgacc",) == 0 or $msg = "Error occurred while modifying wirequard account."; return "$msg"; } ## end sub performModifyClient # action for 'NEW' sub performCreateClient { my $c = shift; my $type = shift; my $username = $c->param('Account'); my $info = $c->param('Info'); # Untaint info and account before use in system() ($info) = $info =~ /([A-Za-z0-9_\-. ]+)/; # trim both ends $info =~ s/^ +| +$//g; ($username) = $username =~ /([A-Za-z0-9_-]+)/; return $c->l('wrg_ERROR_FIELD_CONTENT') unless ($username and $info); #get username my $user = $adb->get($username) or return "$username does not exist"; return $c->l('wrg_ERROR_WRONG_ACCT_TYPE') unless $user->prop("type") eq "user" or $user->key eq "admin"; $username = $user->key; # execute the event wireguard-user-create username info unless (system("/sbin/e-smith/signal-event", "wireguard-user-create", "$username", "$info") == 0) { return $c->error('wrg_ERROR_OCCURED'); } return 'OK'; } ## end sub performCreateClient # action for 'UPD' sub performUpdateConfig { my $c = shift; my $msg = "OK"; my $ip = $c->param('Ip'); my $mask = $c->param('Mask'); my $private = $c->param('Private'); my $public = $c->param('Public'); my $status = $c->param('Status'); unless (defined $private) { $private = `/usr/bin/wg genkey`; ($private) = ($private =~ /(\w+)/); $public = `/usr/bin/echo $private | /usr/bin/wg pubkey`; } # we get number of entries in wireguard db my @num = $wdb->get_all_by_prop(type => "wg0"); if (scalar @num > 0) { # we get current values my $pprivate = $cdb->get('wg-quick@wg0')->prop('private'); my $ppublic = $cdb->get('wg-quick@wg0')->prop('public'); my $pip = $cdb->get('wg-quick@wg0')->prop('ip'); my $pmask = $cdb->get('wg-quick@wg0')->prop('mask'); # if # entries >0 and private |public | ip is chnaged then we push an error and stop if ($pprivate ne $private || $ppublic ne $public || $pip ne $ip || $mask ne $pmask) { return $c->l('wrg_CLIENTS_ALREADY_CONFIGURED'); } } ## end if (scalar @num > 0) #todo validate fields my %props = ( 'ip' => $ip, 'mask' => $mask, 'private' => $private, 'public' => $public, 'status' => $status ); # Test Ip is inside CIDR if (!test_for_private_ip($ip, $mask)) { $msg = "IP must be in private range"; #$fm->error($msg);return; } $cdb->get('wg-quick@wg0')->merge_props(%props) or $msg = "Error occurred while modifying server details."; if ($msg eq "OK") { # Untaint before use in system() ($ip) = ($ip =~ /(\d+\.+\d+\.+\d+\.+\d+\.+\/\d+\.+)/); system("/sbin/e-smith/signal-event", "wireguard-conf-modify", "$ip",) == 0 or $msg = "Error occurred while modifying wireguard conf."; } ## end if ($msg eq "OK") return "$msg"; } ## end sub performUpdateConfig # action for 'REM' sub performRemoveClient { my ($c) = @_; my $conf = $c->param('Wgconf'); if ($c->param("remove")) { unless ($wdb->get($conf)->delete()) { return $c->l('wrg_ERROR_OCCURED'); } unless (system("/sbin/e-smith/signal-event", "wireguard-user-delete") == 0) { return $c->l('wrg_ERROR_OCCURED'); } return 'OK'; } ## end if ($c->param("remove"...)) return $c->l('wrg_CANCELLED'); } ## end sub performRemoveClient # called from templates sub get_existing_accounts { my $c = shift; my @existingAccounts = ('Administrator'); foreach my $account ($adb->get_all_by_prop(type => 'user')) { push @existingAccounts, $account->key; } return \@existingAccounts; } ## end sub get_existing_accounts # called from templates sub get_wgs_info { my ($c, $attr, $data) = @_; return undef if (not defined $attr or not defined $data); my $value; $value = $wdb->get("$data")->prop('info') if ($attr eq 'info' and $wdb->get("$data")); $value = $wdb->get("$data")->prop('user') if ($attr eq 'user' and $wdb->get("$data")); return $value; } ## end sub get_wgs_info # called from templates sub get_conf_info { my ($c, $ipacc) = @_; ##my $ipacc = $c->param('Wgconf'); #untaint ($ipacc) = $ipacc =~ /(\d+\.\d+\.\d+\.\d+\/\d+)/; #get from db # return if does not exist my $acc = $wdb->get($ipacc) or return undef; # return if current user is not admin or the user return undef unless $c->is_admin; my $key = $acc->key; my $info = $acc->prop('info'); my $private = $acc->prop('private'); my $wg0 = $cdb->get('wg-quick@wg0'); my $ServPublic = $wg0->prop('public'); my $Port = $wg0->prop('UDPPort'); my $allowedips = $acc->prop('allowedips') || "0.0.0.0/0"; #here we guess wan IP # are we server-gateway mode ? so external lan, should do # else we should guess from an external service my $ExternalIP = $cdb->get('ExternalInterface')->prop('IPAddress'); $ExternalIP = get_internet_ip_address() unless defined $ExternalIP; #DNS my $IPAddress = $cdb->get('InternalInterface')->prop('IPAddress'); my $dns = ($allowedips =~ /0.0.0.0\/0/) ? "DNS = $IPAddress" : ""; my $fulltext = "#configuration for $key $info [Interface] PrivateKey = $private Address = $key $dns [Peer] PublicKey = $ServPublic AllowedIPs = $allowedips Endpoint = $ExternalIP:$Port "; my @fulltext = split("\n", $fulltext); return \@fulltext; } ## end sub get_conf_info # called from templates sub get_conf_qr { my ($c, $fulltext, $type) = @_; my $qr = `echo "$fulltext" |qrencode -t PNG -o - |base64`; return $qr; } sub get_internet_ip_address { #we could use DNS to do this faster but some provider will block DNS #dig +short myip.opendns.com @resolver1.opendns.com #also resolver1.opendns.com resolver2.opendns.com resolver3.opendns.com #here a list of available site with https use Net::DNS; use LWP::Simple; my $timeout = 1; my @httpslist = qw( checkip.amazonaws.com myexternalip.com/raw ifconfig.me/ icanhazip.com/ ident.me/ tnx.nl/ip ipecho.net/plain wgetip.com/ ip.tyk.nu/ bot.whatismyipaddress.com/ ipof.in/txt l2.io/ip eth0.me/ ); my @dns = ( [ 'myip.opendns.com', 'resolver1.opendns.com', 'A' ], [ 'myip.opendns.com', 'resolver2.opendns.com', 'A' ], [ 'myip.opendns.com', 'resolver3.opendns.com', 'A' ], [ 'myip.opendns.com', 'resolver4.opendns.com', 'A' ], [ 'whoami.akamai.net', 'ns1-1.akamaitech.net', 'A' ], [ 'o-o.myaddr.l.google.com', 'ns1.google.com', 'TXT' ] ); my $ip; #foreach my $i ( 0 .. $#dns) { # dns calls; test only one random... my $i = rand(@httpslist); my $res = Net::DNS::Resolver->new( nameservers => [ $dns[$i][1] ], udp_timeout => $timeout, tcp_timeout => $timeout ); my $reply = $res->search($dns[$i][0], $dns[$i][2]); if ($reply) { foreach my $rr ($reply->answer) { $ip = $rr->txtdata if $rr->can("txtdata"); $ip = $rr->address if $rr->can("address"); # untaint, dns output is tainted ($ip) = $ip =~ /(\d+\.\d+\.\d+\.\d+)/; return $ip if $ip =~ /(\d+\.\d+\.\d+\.\d+)/; } ## end foreach my $rr ($reply->answer) } else { warn "query failed: ", $res->errorstring, "\n"; } #} # https calls my $ii = 0; my $service; while ($ii < 5) { $service = $httpslist[ rand(@httpslist) ]; $ip = (get "https://$service"); chomp $ip; $ii++; last if $ip =~ /(\d+\.\d+\.\d+\.\d+)/; } ## end while ($ii < 5) # not needed but in case, untaint ($ip) = $ip =~ /(\d+\.\d+\.\d+\.\d+)/; return $ip; } ## end sub get_internet_ip_address sub test_for_private_ip { use NetAddr::IP; $_ = shift; my $mask = shift; return unless /(\d+\.\d+\.\d+\.\d+)/; my $iprange = NetAddr::IP->new($1, "$mask"); return unless $iprange; return ($iprange->first()->is_rfc1918() and $iprange->last()->is_rfc1918()); } ## end sub test_for_private_ip 1 __END__