Files
bpfire/html/cgi-bin/wireguard.cgi
Vincent Li c7e72c51bf wireguard: add IP on road warrior interface wg0
Choose one IP from client pool and add it to road warrior interface
wg0 so road warrior VPN client could reach firewall through the VPN

Signed-off-by: Vincent Li <vincent.mc.li@gmail.com>
2025-07-20 23:40:09 +00:00

1549 lines
37 KiB
Perl

#!/usr/bin/perl
###############################################################################
# #
# IPFire.org - A linux based firewall #
# Copyright (C) 2024 Michael Tremer <michael.tremer@ipfire.org> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
# #
###############################################################################
use strict;
# enable only the following on debugging purpose
use warnings;
use CGI::Carp 'fatalsToBrowser';
use Imager::QRCode;
use MIME::Base64;
require "/var/ipfire/general-functions.pl";
require "${General::swroot}/header.pl";
require "${General::swroot}/location-functions.pl";
require "${General::swroot}/wireguard-functions.pl";
my %cgiparams = ();
my @errormessages = ();
# Generate keys
&Wireguard::generate_keys();
# Fetch CGI parameters
&Header::getcgihash(\%cgiparams, {'wantfile' => 1, 'filevar' => 'FH'});
# Save on main page
if ($cgiparams{"ACTION"} eq $Lang::tr{'save'}) {
my @client_dns = ();
# Store whether enabled or not
if ($cgiparams{'ENABLED'} =~ m/^(on|off)?$/) {
$Wireguard::settings{'ENABLED'} = $cgiparams{'ENABLED'};
}
# Check endpoint
if (&General::validfqdn($cgiparams{'ENDPOINT'}) || &Network::check_ip_address($cgiparams{'ENDPOINT'}) || ($cgiparams{'ENDPOINT'} eq '')) {
$Wireguard::settings{'ENDPOINT'} = $cgiparams{'ENDPOINT'};
} else {
push(@errormessages, $Lang::tr{'invalid endpoint'});
}
# Check port
if (&General::validport($cgiparams{'PORT'})) {
$Wireguard::settings{'PORT'} = $cgiparams{'PORT'};
} else {
push(@errormessages, $Lang::tr{'invalid port'});
}
# Check client pool
if (&Wireguard::pool_is_in_use($Wireguard::settings{'CLIENT_POOL'})) {
# Ignore any changes if the pool is in use
} elsif (&Network::check_subnet($cgiparams{'CLIENT_POOL'})) {
$Wireguard::settings{'CLIENT_POOL'} = &Network::normalize_network($cgiparams{'CLIENT_POOL'});
} elsif ($cgiparams{'CLIENT_POOL'} ne '') {
push(@errormessages, $Lang::tr{'wg invalid client pool'});
}
# Check client DNS
if (defined $cgiparams{'CLIENT_DNS'}) {
@client_dns = split(/,/, $cgiparams{'CLIENT_DNS'});
foreach my $dns (@client_dns) {
unless (&Network::check_ip_address($dns)) {
push(@errormessages, "$Lang::tr{'wg invalid client dns'}: ${dns}");
}
}
# Store CLIENT_DNS
$Wireguard::settings{'CLIENT_DNS'} = join("|", @client_dns);
}
# Check wg0 ADDRESS - make it optional
if (defined $cgiparams{'ADDRESS'}) {
if ($cgiparams{'ADDRESS'} ne '') {
my $address = $cgiparams{'ADDRESS'};
unless (&Network::check_ip_address($address)) {
push(@errormessages, "$Lang::tr{'wg invalid wg0 address'}: ${address}");
}
# Store ADDRESS only if it's valid and not empty
$Wireguard::settings{'ADDRESS'} = $address;
} else {
# Explicitly set to empty string when field is empty
$Wireguard::settings{'ADDRESS'} = '';
}
}
# Don't continue on error
goto MAIN if (scalar @errormessages);
# Store the configuration file
&General::writehash("/var/ipfire/wireguard/settings", \%Wireguard::settings);
# Start if enabled
if (&Wireguard::is_enabled()) {
&General::system("/usr/local/bin/wireguardctrl", "start");
} else {
&General::system("/usr/local/bin/wireguardctrl", "stop");
}
# Delete an existing peer
} elsif ($cgiparams{"ACTION"} eq $Lang::tr{'remove'}) {
my $key = $cgiparams{'KEY'};
# Fail if the peer does not exist
unless (exists $Wireguard::peers{$key}) {
push(@errormessages, $Lang::tr{'wg peer does not exist'});
goto MAIN;
}
# Delete the peer
delete($Wireguard::peers{$key});
# Store the configuration
&General::writehasharray("/var/ipfire/wireguard/peers", \%Wireguard::peers);
# Reload if enabled
if (&Wireguard::is_enabled()) {
&General::system("/usr/local/bin/wireguardctrl", "start");
}
# Edit an existing peer
} elsif ($cgiparams{"ACTION"} eq $Lang::tr{'edit'}) {
my $key = $cgiparams{'KEY'};
# Fail if the peer does not exist
unless (exists $Wireguard::peers{$key}) {
push(@errormessages, $Lang::tr{'wg peer does not exist'});
goto MAIN;
}
# Fetch type
my $type = $Wireguard::peers{$key}[1];
my $remote_subnets = &Wireguard::decode_subnets($Wireguard::peers{$key}[8]);
my $local_subnets = &Wireguard::decode_subnets($Wireguard::peers{$key}[10]);
# Flush CGI parameters & load configuration
%cgiparams = (
"KEY" => $key,
"ENABLED" => $Wireguard::peers{$key}[0],
"TYPE" => $Wireguard::peers{$key}[1],
"NAME" => $Wireguard::peers{$key}[2],
"PUBLIC_KEY" => $Wireguard::peers{$key}[3],
"PRIVATE_KEY" => $Wireguard::peers{$key}[4],
"PORT" => $Wireguard::peers{$key}[5],
"ENDPOINT_ADDRESS" => $Wireguard::peers{$key}[6],
"ENDPOINT_PORT" => $Wireguard::peers{$key}[7],
"REMOTE_SUBNETS" => join(", ", @$remote_subnets),
"REMARKS" => &MIME::Base64::decode_base64($Wireguard::peers{$key}[9]),
"LOCAL_SUBNETS" => join(", ", @$local_subnets),
"PSK" => $Wireguard::peers{$key}[11],
"KEEPALIVE" => $Wireguard::peers{$key}[12],
"LOCAL_ADDRESS" => $Wireguard::peers{$key}[13],
);
# Jump to the editor
if ($type eq "host") {
goto EDITHOST;
} elsif ($type eq "net") {
goto EDITNET;
} else {
die "Unsupported type: $type";
}
} elsif ($cgiparams{"ACTION"} eq "IMPORT") {
my @local_subnets = ();
my $peer;
# Parse the configuration file
($peer, @errormessages) = &Wireguard::parse_configuration($cgiparams{'NAME'}, $cgiparams{'FH'});
# Check local subnets
if (defined $cgiparams{'LOCAL_SUBNETS'}) {
@local_subnets = split(/,/, $cgiparams{'LOCAL_SUBNETS'});
foreach my $subnet (@local_subnets) {
$subnet =~ s/^\s+//g;
$subnet =~ s/\s+$//g;
unless (&Network::check_subnet($subnet)) {
push(@errormessages, $Lang::tr{'wg invalid local subnet'} . ": ${subnet}");
}
}
} else {
push(@errormessages, $Lang::tr{'wg no local subnets'});
}
# Show any error messages
goto IMPORT if (@errormessages);
# Allocate a new key
my $key = &General::findhasharraykey(\%Wireguard::peers);
# Save the connection
$Wireguard::peers{$key} = [
# 0 = Enabled
"on",
# 1 = Type
"net",
# 2 = Name
$peer->{"NAME"},
# 3 = Remote Public Key
$peer->{"PUBLIC_KEY"},
# 4 = Local Private Key
$peer->{"PRIVATE_KEY"},
# 5 = Port
$peer->{"PORT"},
# 6 = Endpoint Address
$peer->{"ENDPOINT_ADDRESS"},
# 7 = Endpoint Port
$peer->{"ENDPOINT_PORT"},
# 8 = Remote Subnets
&Wireguard::encode_subnets(@{ $peer->{"REMOTE_SUBNETS"} }),
# 9 = Remark
&Wireguard::encode_remarks($cgiparams{"REMARKS"}),
# 10 = Local Subnets
&Wireguard::encode_subnets(@local_subnets),
# 11 = PSK
$peer->{"PSK"},
# 12 = Keepalive
$peer->{"KEEPALIVE"} || $Wireguard::DEFAULT_KEEPALIVE,
# 13 = Local Address
$peer->{"LOCAL_ADDRESS"},
];
# Store the configuration
&General::writehasharray("/var/ipfire/wireguard/peers", \%Wireguard::peers);
# Reload if enabled
if (&Wireguard::is_enabled()) {
&General::system("/usr/local/bin/wireguardctrl", "start");
}
} elsif ($cgiparams{"ACTION"} eq "CREATE-PEER-NET") {
my @local_subnets = ();
my @remote_subnets = ();
# Allocate a new key
my $key = &General::findhasharraykey(\%Wireguard::peers);
my $name = $cgiparams{"NAME"};
# Check if the name is valid
unless (&Wireguard::name_is_valid($name)) {
push(@errormessages, $Lang::tr{'wg invalid name'});
}
# Check if the name is free
unless (&Wireguard::name_is_free($name, $key)) {
push(@errormessages, $Lang::tr{'wg name is already used'});
}
# Check the endpoint address
if ($cgiparams{'ENDPOINT_ADDRESS'} eq '') {
# The endpoint address may be empty
} elsif (&General::validfqdn($cgiparams{'ENDPOINT_ADDRESS'})) {
# The endpoint is a valid FQDN
} elsif (&Network::check_ip_address($cgiparams{'ENDPOINT_ADDRESS'})) {
# The endpoint is a valid IP address
} else {
push(@errormessages, $Lang::tr{'wg invalid endpoint address'});
}
# Check local subnets
if (defined $cgiparams{'LOCAL_SUBNETS'}) {
@local_subnets = split(/,/, $cgiparams{'LOCAL_SUBNETS'});
foreach my $subnet (@local_subnets) {
$subnet =~ s/^\s+//g;
$subnet =~ s/\s+$//g;
unless (&Network::check_subnet($subnet)) {
push(@errormessages, $Lang::tr{'wg invalid local subnet'} . ": ${subnet}");
}
}
} else {
push(@errormessages, $Lang::tr{'wg no local subnets'});
}
# Check remote subnets
if (defined $cgiparams{'REMOTE_SUBNETS'}) {
@remote_subnets = split(/,/, $cgiparams{'REMOTE_SUBNETS'});
foreach my $subnet (@remote_subnets) {
$subnet =~ s/^\s+//g;
$subnet =~ s/\s+$//g;
unless (&Network::check_subnet($subnet)) {
push(@errormessages, $Lang::tr{'wg invalid remote subnet'} . ": ${subnet}");
}
}
} else {
push(@errormessages, $Lang::tr{'wg no remote subnets'});
}
# If there are any errors, we go back to the editor
goto CREATENET if (scalar @errormessages);
# Generate a new key pair
my $local_private_key = &Wireguard::generate_private_key();
my $remote_private_key = &Wireguard::generate_private_key();
# Derive the public key
my $remote_public_key = &Wireguard::derive_public_key($remote_private_key);
# Generate a new PSK
my $psk = &Wireguard::generate_private_key();
# Generate two new ports
my $local_port = &Wireguard::get_free_port();
my $remote_port = &Wireguard::get_free_port();
# Save the connection
$Wireguard::peers{$key} = [
# 0 = Enabled
"on",
# 1 = Type
"net",
# 2 = Name
$name,
# 3 = Remote Public Key
$remote_public_key,
# 4 = Local Private Key
$local_private_key,
# 5 = Port
$local_port,
# 6 = Endpoint Address
$cgiparams{"ENDPOINT_ADDRESS"},
# 7 = Endpoint Port
$remote_port,
# 8 = Remote Subnets
&Wireguard::encode_subnets(@remote_subnets),
# 9 = Remark
&Wireguard::encode_remarks($cgiparams{"REMARKS"}),
# 10 = Local Subnets
&Wireguard::encode_subnets(@local_subnets),
# 11 = PSK
$psk,
# 12 = Keepalive
$Wireguard::DEFAULT_KEEPALIVE,
# 13 = Local Address
"",
];
# Store the configuration
&General::writehasharray("/var/ipfire/wireguard/peers", \%Wireguard::peers);
# Reload if enabled
if (&Wireguard::is_enabled()) {
&General::system("/usr/local/bin/wireguardctrl", "start");
}
# Send HTTP Headers
&Header::showhttpheaders();
# Open the page
&Header::openpage($Lang::tr{'wireguard'}, 1, '');
# Generate the client configuration
my $config = &Wireguard::generate_peer_configuration($key, $remote_private_key);
# Encode the configuration as Base64
$config = &MIME::Base64::encode_base64($config);
# Open a new box
&Header::openbox('100%', '', "$Lang::tr{'wg peer configuration'}: $name");
# Make the filename for files
my $filename = &Header::normalize("${name}.conf");
print <<END;
<div class="text-center">
<p>
<a href="data:text/plain;base64,${config}" download="${filename}">
$Lang::tr{'wg download configuration file'}
</a>
</p>
<p class="text-error">
$Lang::tr{'wg warning configuration only shown once'}
</p>
<p>
<form method="GET" action="">
<button type="submit">$Lang::tr{'done'}</button>
</form>
</p>
</div>
END
&Header::closebox();
&Header::closepage();
exit(0);
} elsif ($cgiparams{"ACTION"} eq "SAVE-PEER-NET") {
my @local_subnets = ();
my @remote_subnets = ();
# Fetch or allocate a new key
my $key = $cgiparams{'KEY'};
# Load the existing peer
my $peer = &Wireguard::load_peer($key);
# Fail if we don't have the key
unless (defined $peer) {
die "Peer $key does not exist\n";
}
# Check if the name is valid
unless (&Wireguard::name_is_valid($cgiparams{"NAME"})) {
push(@errormessages, $Lang::tr{'wg invalid name'});
}
# Check if the name is free
unless (&Wireguard::name_is_free($cgiparams{"NAME"}, $key)) {
push(@errormessages, $Lang::tr{'wg name is already used'});
}
# Check the public key
unless (&Wireguard::key_is_valid($cgiparams{'PUBLIC_KEY'})) {
push(@errormessages, $Lang::tr{'wg invalid public key'});
}
# Check PSK
if ($cgiparams{'PSK'} eq '') {
# The PSK may be empty
} elsif (!&Wireguard::key_is_valid($cgiparams{'PSK'})) {
push(@errormessages, $Lang::tr{'wg invalid psk'});
}
# Select a new random port if none given
if ($cgiparams{'PORT'} eq "") {
$cgiparams{'PORT'} = &Wireguard::get_free_port();
# If a port was given we check that it is valid
} elsif (!&General::validport($cgiparams{'PORT'})) {
push(@errormessages, $LANG::tr{'invalid port'});
}
# Check the endpoint address
if ($cgiparams{'ENDPOINT_ADDRESS'} eq '') {
# The endpoint address may be empty
} elsif (&General::validfqdn($cgiparams{'ENDPOINT_ADDRESS'})) {
# The endpoint is a valid FQDN
} elsif (&Network::check_ip_address($cgiparams{'ENDPOINT_ADDRESS'})) {
# The endpoint is a valid IP address
} else {
push(@errormessages, $Lang::tr{'wg invalid endpoint address'});
}
# Check the endpoint port
unless (&General::validport($cgiparams{'ENDPOINT_PORT'})) {
push(@errormessages, $Lang::tr{'wg invalid endpoint port'});
}
# Check keepalive
unless (&Wireguard::keepalive_is_valid($cgiparams{'KEEPALIVE'})) {
push(@errormessages, $Lang::tr{'wg invalid keepalive interval'});
}
# Check local subnets
if (defined $cgiparams{'LOCAL_SUBNETS'}) {
@local_subnets = split(/,/, $cgiparams{'LOCAL_SUBNETS'});
foreach my $subnet (@local_subnets) {
$subnet =~ s/^\s+//g;
$subnet =~ s/\s+$//g;
unless (&Network::check_subnet($subnet)) {
push(@errormessages, $Lang::tr{'wg invalid local subnet'} . ": ${subnet}");
}
}
} else {
push(@errormessages, $Lang::tr{'wg no local subnets'});
}
# Check remote subnets
if (defined $cgiparams{'REMOTE_SUBNETS'}) {
@remote_subnets = split(/,/, $cgiparams{'REMOTE_SUBNETS'});
foreach my $subnet (@remote_subnets) {
$subnet =~ s/^\s+//g;
$subnet =~ s/\s+$//g;
unless (&Network::check_subnet($subnet)) {
push(@errormessages, $Lang::tr{'wg invalid remote subnet'} . ": ${subnet}");
}
}
} else {
push(@errormessages, $Lang::tr{'wg no remote subnets'});
}
# If there are any errors, we go back to the editor
goto EDITNET if (scalar @errormessages);
# Save the connection
$Wireguard::peers{$key} = [
# 0 = Enabled
"on",
# 1 = Type
"net",
# 2 = Name
$cgiparams{"NAME"},
# 3 = Public Key
$cgiparams{"PUBLIC_KEY"},
# 4 = Private Key
$peer->{"PRIVATE_KEY"},
# 5 = Port
$cgiparams{"PORT"},
# 6 = Endpoint Address
$cgiparams{"ENDPOINT_ADDRESS"},
# 7 = Endpoint Port
$cgiparams{"ENDPOINT_PORT"},
# 8 = Remote Subnets
&Wireguard::encode_subnets(@remote_subnets),
# 9 = Remark
&Wireguard::encode_remarks($cgiparams{"REMARKS"}),
# 10 = Local Subnets
&Wireguard::encode_subnets(@local_subnets),
# 11 = PSK
$cgiparams{"PSK"} || "",
# 12 = Keepalive
$cgiparams{"KEEPALIVE"} || 0,
# 13 = Local Address
"",
];
# Store the configuration
&General::writehasharray("/var/ipfire/wireguard/peers", \%Wireguard::peers);
# Reload if enabled
if (&Wireguard::is_enabled()) {
&General::system("/usr/local/bin/wireguardctrl", "start");
}
} elsif ($cgiparams{"ACTION"} eq "SAVE-PEER-HOST") {
my $private_key;
my @free_addresses = ();
my @local_subnets = ();
# Fetch or allocate a new key
my $key = $cgiparams{'KEY'} || &General::findhasharraykey(\%Wireguard::peers);
# Is this a new connection?
my $is_new = !exists $Wireguard::peers{$key};
# Check if the name is valid
unless (&Wireguard::name_is_valid($cgiparams{"NAME"})) {
push(@errormessages, $Lang::tr{'wg invalid name'});
}
# Check if the name is free
unless (&Wireguard::name_is_free($cgiparams{"NAME"}, $key)) {
push(@errormessages, $Lang::tr{'wg name is already used'});
}
# Check local subnets
if (defined $cgiparams{'LOCAL_SUBNETS'}) {
@local_subnets = split(/,/, $cgiparams{'LOCAL_SUBNETS'});
foreach my $subnet (@local_subnets) {
$subnet =~ s/^\s+//g;
$subnet =~ s/\s+$//g;
unless (&Network::check_subnet($subnet)) {
push(@errormessages, $Lang::tr{'wg invalid local subnet'} . ": ${subnet}");
}
}
} else {
push(@errormessages, $Lang::tr{'wg no local subnets'});
}
# Check if we have address space left in the pool
if ($is_new) {
# Fetch the next free address
@free_addresses = &Wireguard::free_pool_addresses($Wireguard::settings{'CLIENT_POOL'}, 1);
# Fail if we ran out of addresses
if (scalar @free_addresses == 0) {
push(@errormessages, $Lang::tr{'wg no more free addresses in pool'});
}
}
# If there are any errors, we go back to the editor
goto EDITHOST if (scalar @errormessages);
# Generate things for a new peer
if ($is_new) {
# Generate a new private key
$private_key = &Wireguard::generate_private_key();
# Derive the public key
$cgiparams{"PUBLIC_KEY"} = &Wireguard::derive_public_key($private_key);
# Generate a new PSK
$cgiparams{"PSK"} = &Wireguard::generate_private_key();
# Fetch a free address from the pool
foreach (@free_addresses) {
$cgiparams{'CLIENT_ADDRESS'} = $_;
last;
}
# Fetch some configuration parts
} else {
$cgiparams{"PUBLIC_KEY"} = $Wireguard::peers{$key}[3];
$cgiparams{'CLIENT_ADDRESS'} = $Wireguard::peers{$key}[8];
$cgiparams{"PSK"} = $Wireguard::peers{$key}[11];
}
# Save the connection
$Wireguard::peers{$key} = [
# 0 = Enabled
"on",
# 1 = Type
"host",
# 2 = Name
$cgiparams{"NAME"},
# 3 = Public Key
$cgiparams{"PUBLIC_KEY"},
# 4 = Private Key
"",
# 5 = Port
"",
# 6 = Endpoint Address
"",
# 7 = Endpoint Port
"",
# 8 = Remote Subnets
$cgiparams{'CLIENT_ADDRESS'},
# 9 = Remark
&Wireguard::encode_remarks($cgiparams{"REMARKS"}),
# 10 = Local Subnets
&Wireguard::encode_subnets(@local_subnets),
# 11 = PSK
$cgiparams{"PSK"},
# 12 = Keepalive
0,
# 13 = Local Address
"",
];
# Store the configuration
&General::writehasharray("/var/ipfire/wireguard/peers", \%Wireguard::peers);
# Reload if enabled
if (&Wireguard::is_enabled()) {
&General::system("/usr/local/bin/wireguardctrl", "start");
}
# Show the client configuration when creating a new peer
if ($is_new) {
# Send HTTP Headers
&Header::showhttpheaders();
# Open the page
&Header::openpage($Lang::tr{'wireguard'}, 1, '');
# Load the peer
my $peer = &Wireguard::load_peer($key);
# Generate the client configuration
my $config = &Wireguard::generate_peer_configuration($key, $private_key);
# Create a QR code generator
my $qrgen = Imager::QRCode->new(
size => 6,
margin => 0,
version => 0,
level => 'M',
mode => '8-bit',
casesensitive => 1,
lightcolor => Imager::Color->new(255, 255, 255),
darkcolor => Imager::Color->new(0, 0, 0),
);
# The generated QR code
my $qrcode;
# Encode the configuration
my $img = $qrgen->plot("$config");
# Encode the image as PNG
$img->write(data => \$qrcode, type => "png") or die $img->errstr;
# Encode the image as bas64
$qrcode = &MIME::Base64::encode_base64($qrcode);
# Encode the configuration as Base64
$config = &MIME::Base64::encode_base64($config);
# Open a new box
&Header::openbox('100%', '', "$Lang::tr{'wg peer configuration'}: $peer->{'NAME'}");
# Make the filename for files
my $filename = &Header::normalize($peer->{'NAME'}) . ".conf";
print <<END;
<div class="text-center">
<p>
<img src="data:image/png;base64,${qrcode}" alt="$Lang::tr{'qr code'}">
</p>
<p>
$Lang::tr{'wg scan the qr code'}
</p>
<p>
<a href="data:text/plain;base64,${config}" download="${filename}">
$Lang::tr{'wg download configuration file'}
</a>
</p>
<p class="text-error">
$Lang::tr{'wg warning configuration only shown once'}
</p>
<p>
<form method="GET" action="">
<button type="submit">$Lang::tr{'done'}</button>
</form>
</p>
</div>
END
&Header::closebox();
&Header::closepage();
exit(0);
}
} elsif ($cgiparams{"ACTION"} eq $Lang::tr{'add'}) {
if ($cgiparams{"TYPE"} eq "net") {
goto CREATENET;
} elsif ($cgiparams{"TYPE"} eq "host") {
goto CREATEHOST;
} elsif ($cgiparams{"TYPE"} eq "import") {
goto IMPORT;
# Ask the user what type they want
} else {
goto ADD;
}
# Toggle Enable/Disable
} elsif ($cgiparams{'ACTION'} eq 'TOGGLE-ENABLE-DISABLE') {
my $key = $cgiparams{'KEY'} || 0;
if (exists $Wireguard::peers{$key}) {
if ($Wireguard::peers{$key}[0] eq "on") {
$Wireguard::peers{$key}[0] = "off";
} else {
$Wireguard::peers{$key}[0] = "on";
}
}
# Store the configuration
&General::writehasharray("/var/ipfire/wireguard/peers", \%Wireguard::peers);
# Reload if enabled
if (&Wireguard::is_enabled()) {
&General::system("/usr/local/bin/wireguardctrl", "start");
}
}
# The main page starts here
MAIN:
# Send HTTP Headers
&Header::showhttpheaders();
# Open the page
&Header::openpage($Lang::tr{'wireguard'}, 1, '');
# Show any error messages
&Header::errorbox(@errormessages);
# Open a box for Global Settings
&Header::openbox('100%', '', $Lang::tr{'global settings'});
my %checked = (
"ENABLED" => (&Wireguard::is_enabled()) ? "checked" : "",
);
my %readonly = (
"CLIENT_POOL" => (&Wireguard::pool_is_in_use($Wireguard::settings{'CLIENT_POOL'}) ? "readonly" : ""),
);
my $client_dns = $Wireguard::settings{'CLIENT_DNS'} =~ s/\|/, /gr;
print <<END;
<form method="POST" action="">
<table class="form">
<tr>
<td>$Lang::tr{'enabled'}</td>
<td>
<input type="checkbox" name="ENABLED" $checked{'ENABLED'} />
</td>
</tr>
<tr>
<td>$Lang::tr{'wg endpoint'}&nbsp;<img src='/blob.gif' alt='*' /></td>
<td>
<input type="text" name="ENDPOINT" value="$Wireguard::settings{'ENDPOINT'}" placeholder="$General::mainsettings{'HOSTNAME'}.$General::mainsettings{'DOMAINNAME'}" />
</td>
</tr>
<tr>
<td>$Lang::tr{'wg address'}</td>
<td>
<input type="text" name="ADDRESS" value="$Wireguard::settings{'ADDRESS'}" />
</td>
</tr>
<tr>
<td>$Lang::tr{'port'}&nbsp;<img src='/blob.gif' alt='*' /></td>
<td>
<input type="number" name="PORT" value="$Wireguard::settings{'PORT'}"
min="1024" max="65535" />
</td>
</tr>
</table>
<h6>$Lang::tr{'wg host to net client settings'}</h6>
<table class="form">
<tr>
<td>$Lang::tr{'wg client pool'}&nbsp;<img src='/blob.gif' alt='*' /></td>
<td>
<input type="text" name="CLIENT_POOL"
value="$Wireguard::settings{'CLIENT_POOL'}" $readonly{'CLIENT_POOL'} />
</td>
</tr>
<tr>
<td>$Lang::tr{'wg dns'}&nbsp;<img src='/blob.gif' alt='*' /></td>
<td>
<input type="text" name="CLIENT_DNS"
value="$client_dns" />
</td>
</tr>
<tr class="action">
<td colspan="2">
<input type='submit' name='ACTION' value='$Lang::tr{'save'}' />
</td>
</tr>
</table>
</form>
END
&Header::closebox();
# Show a list with all peers
&Header::opensection();
if (%Wireguard::peers) {
print <<END;
<table class='tbl'>
<tr>
<th width='15%'>
$Lang::tr{'name'}
</th>
<th>
$Lang::tr{'remark'}
</th>
<th width='20%' colspan='2'>
$Lang::tr{'status'}
</th>
<th width='10%' colspan='3'>
$Lang::tr{'action'}
</th>
</tr>
END
# Dump all RW peers
my %DUMP = &Wireguard::dump("wg0");
# Iterate through all peers...
foreach my $key (sort { $Wireguard::peers{$a}[2] cmp $Wireguard::peers{$b}[2] } keys %Wireguard::peers) {
my $enabled = $Wireguard::peers{$key}[0];
my $type = $Wireguard::peers{$key}[1];
my $name = $Wireguard::peers{$key}[2];
my $pubkey = $Wireguard::peers{$key}[3];
#my $privkey = $Wireguard::peers{$key}[4]
#my $port = $Wireguard::peers{$key}[5];
my $endpoint = $Wireguard::peers{$key}[6];
#my $endpport = $Wireguard::peers{$key}[7];
my $routes = $Wireguard::peers{$key}[8];
my $remarks = &Wireguard::decode_remarks($Wireguard::peers{$key}[9]);
my $connected = $Lang::tr{'capsclosed'};
my $country = "ZZ";
my $location = "";
my $gif = ($enabled eq "on") ? "on.gif" : "off.gif";
my @status = ("status");
# Fetch the dump
my %dump = ($type eq "net") ? &Wireguard::dump("wg$key") : %DUMP;
# Fetch the status of the peer (if possible)
my $status = $dump{$pubkey} || ();
# Fetch the actual endpoint
my ($actual_endpoint, $actual_port) = split(/:/, $status->{"endpoint"}, 2);
# WireGuard performs a handshake very two minutes, so we should be considered online then
my $is_connected = (time - $status->{"latest-handshake"}) <= 120;
# We are connected!
if ($is_connected) {
push(@status, "is-connected");
$connected = $Lang::tr{'capsopen'};
# If we have an endpoint lets lookup the country
if ($actual_endpoint) {
$country = &Location::Functions::lookup_country_code($actual_endpoint);
# If we found a country, let's show it
if ($country) {
my $icon = &Location::Functions::get_flag_icon($country);
$location = <<EOF;
<a href="country.cgi#$country">
<img src="$icon" border='0' align='absmiddle'
alt='$country' title='$actual_endpoint:$actual_port - $country' />
</a>
EOF
}
}
# We are not connected...
} else {
push(@status, "is-disconnected");
}
# Escape remarks
if ($remarks) {
$remarks = &Header::escape($remarks);
}
print <<END;
<tr>
<th scope="row" title="${pubkey}">
$name
</th>
<td>
$remarks
</td>
END
if ($location) {
print <<END;
<td class="@status">
$connected
</td>
<td class="@status">
$location
</td>
END
} else {
print <<END;
<td class="@status" colspan="2">
$connected
</td>
END
}
print <<END;
<td class="text-center">
<form method='post'>
<input type='image' name='$Lang::tr{'toggle enable disable'}' src='/images/$gif'
alt='$Lang::tr{'toggle enable disable'}' title='$Lang::tr{'toggle enable disable'}' />
<input type='hidden' name='ACTION' value='TOGGLE-ENABLE-DISABLE' />
<input type='hidden' name='KEY' value='$key' />
</form>
</td>
<td class="text-center">
<form method='post'>
<input type='hidden' name='ACTION' value='$Lang::tr{'edit'}' />
<input type='image' name='$Lang::tr{'edit'}' src='/images/edit.gif'
alt='$Lang::tr{'edit'}' title='$Lang::tr{'edit'}' />
<input type='hidden' name='KEY' value='$key' />
</form>
</td>
<td class="text-center">
<form method='post'>
<input type='hidden' name='ACTION' value='$Lang::tr{'remove'}' />
<input type='image' name='$Lang::tr{'remove'}' src='/images/delete.gif'
alt='$Lang::tr{'remove'}' title='$Lang::tr{'remove'}' />
<input type='hidden' name='KEY' value='$key' />
</form>
</td>
</tr>
END
}
print"</table>";
}
# Show controls
print <<END;
<table class="form">
<tr class="action">
<td>
<form method='post'>
<input type='submit' name='ACTION' value='$Lang::tr{'add'}' />
</form>
</td>
</tr>
</table>
END
&Header::closesection();
&Header::closepage();
exit(0);
ADD:
# Send HTTP Headers
&Header::showhttpheaders();
# Open the page
&Header::openpage($Lang::tr{'wireguard'}, 1, '');
# Show any error messages
&Header::errorbox(@errormessages);
# Open a new box
&Header::openbox('100%', '', $Lang::tr{'connection type'});
my %disabled = (
"host" => "",
);
# If there is no CLIENT_POOL configured, we disable the option
if ($Wireguard::settings{'CLIENT_POOL'} eq "") {
$disabled{"host"} = "disabled";
# If the client pool is out of addresses, we do the same
} else {
my @free_addresses = &Wireguard::free_pool_addresses($Wireguard::settings{'CLIENT_POOL'}, 1);
if (scalar @free_addresses == 0) {
$disabled{"host"} = "disabled";
}
}
# Check the first available option
my %checked = (
"host" => ($disabled{"host"} eq "disabled") ? "" : "checked",
"net" => ($disabled{"host"} eq "disabled") ? "checked" : "",
);
print <<END;
<form method="POST" ENCTYPE="multipart/form-data">
<p>
<label>
<input type='radio' name='TYPE' value='host' $disabled{'host'} $checked{'host'} />
$Lang::tr{'host to net vpn'}
</label>
</p>
<p>
<label>
<input type='radio' name='TYPE' value='net' $checked{'net'} />
$Lang::tr{'net to net vpn'}
</label>
</p>
<p>
<label>
<input type='radio' name='TYPE' value='import' />
$Lang::tr{'import connection'}
</label>
</p>
<table class="form">
<tr class="action">
<td>
<input type='submit' name='ACTION' value='$Lang::tr{'add'}' />
</td>
</tr>
</table>
</form>
END
&Header::closebox();
&Header::closepage();
exit(0);
IMPORT:
# Send HTTP Headers
&Header::showhttpheaders();
# Open the page
&Header::openpage($Lang::tr{'wireguard'}, 1, '');
# Show any error messages
&Header::errorbox(@errormessages);
# Open a new box
&Header::openbox('100%', '', $Lang::tr{'wg import peer'});
print <<END;
<form method="POST" ENCTYPE="multipart/form-data">
<input type="hidden" name="ACTION" value="IMPORT">
<table class="form">
<tr>
<td>
$Lang::tr{'name'}
</td>
<td>
<input type="text" name="NAME"
value="$cgiparams{'NAME'}" required />
</td>
</tr>
<tr>
<td>
$Lang::tr{'remarks'}
</td>
<td>
<input type="text" name="REMARKS"
value="$cgiparams{'REMARKS'}" />
</td>
</tr>
<tr>
<td>
$Lang::tr{'configuration file'}
</td>
<td>
<input type='file' name='FH' required />
</td>
</tr>
</table>
<h6>$Lang::tr{'routing'}</h6>
<table class="form">
<tr>
<td>
$Lang::tr{'local subnets'}
</td>
<td>
<input type="text" name="LOCAL_SUBNETS"
value="$cgiparams{'LOCAL_SUBNETS'}" required />
</td>
</tr>
</table>
<table class="form">
<tr class="action">
<td colspan="2">
<input type='submit' value='$Lang::tr{'import'}' />
</td>
</tr>
</table>
</form>
END
&Header::closebox();
&Header::closepage();
exit(0);
CREATENET:
# Send HTTP Headers
&Header::showhttpheaders();
# Open the page
&Header::openpage($Lang::tr{'wireguard'}, 1, '');
# Show any error messages
&Header::errorbox(@errormessages);
# Open a new box
&Header::openbox('100%', '', $Lang::tr{'wg create net-to-net peer'});
# Set defaults
&General::set_defaults(\%cgiparams, {
"LOCAL_SUBNETS" =>
$Network::ethernet{"GREEN_NETADDRESS"}
. "/" . $Network::ethernet{"GREEN_NETMASK"},
});
print <<END;
<form method="POST" ENCTYPE="multipart/form-data">
<input type="hidden" name="ACTION" value="CREATE-PEER-NET">
<table class="form">
<tr>
<td>
$Lang::tr{'name'}
</td>
<td>
<input type="text" name="NAME"
value="$cgiparams{'NAME'}" required />
</td>
</tr>
<tr>
<td>
$Lang::tr{'remarks'}
</td>
<td>
<input type="text" name="REMARKS"
value="$cgiparams{'REMARKS'}" />
</td>
</tr>
</table>
<h6>$Lang::tr{'wg endpoint'}</h6>
<table class="form">
<tr>
<td>
$Lang::tr{'endpoint address'}
</td>
<td>
<input type="text" name="ENDPOINT_ADDRESS"
value="$cgiparams{'ENDPOINT_ADDRESS'}" />
</td>
</tr>
</table>
<h6>$Lang::tr{'routing'}</h6>
<table class="form">
<tr>
<td>
$Lang::tr{'local subnets'}
</td>
<td>
<input type="text" name="LOCAL_SUBNETS"
value="$cgiparams{'LOCAL_SUBNETS'}" required />
</td>
</tr>
<tr>
<td>
$Lang::tr{'remote subnets'}
</td>
<td>
<input type="text" name="REMOTE_SUBNETS"
value="$cgiparams{'REMOTE_SUBNETS'}" required />
</td>
</tr>
<tr class="action">
<td colspan="2">
<input type='submit' value='$Lang::tr{'create'}' />
</td>
</tr>
</table>
</form>
END
&Header::closebox();
&Header::closepage();
exit(0);
EDITNET:
# Send HTTP Headers
&Header::showhttpheaders();
# Open the page
&Header::openpage($Lang::tr{'wireguard'}, 1, '');
# Show any error messages
&Header::errorbox(@errormessages);
# Fetch the key
my $key = $cgiparams{'KEY'};
# Open a new box
&Header::openbox('100%', '', $Lang::tr{'wg edit net-to-net peer'});
# Derive our own public key
my $public_key = &Wireguard::derive_public_key($cgiparams{'PRIVATE_KEY'});
print <<END;
<form method="POST" ENCTYPE="multipart/form-data">
<input type="hidden" name="ACTION" value="SAVE-PEER-NET">
<input type="hidden" name="KEY" value="$cgiparams{'KEY'}">
<table class="form">
<tr>
<td>
$Lang::tr{'name'}
</td>
<td>
<input type="text" name="NAME"
value="$cgiparams{'NAME'}" required />
</td>
</tr>
<tr>
<td>
$Lang::tr{'remarks'}
</td>
<td>
<input type="text" name="REMARKS"
value="$cgiparams{'REMARKS'}" />
</td>
</tr>
<tr>
<td>
$Lang::tr{'public key'}
</td>
<td>
<input type="text" value="$public_key" readonly />
</td>
</tr>
</table>
<h6>$Lang::tr{'wg endpoint'}</h6>
<table class="form">
<tr>
<td>
$Lang::tr{'endpoint address'}
</td>
<td>
<input type="text" name="ENDPOINT_ADDRESS"
value="$cgiparams{'ENDPOINT_ADDRESS'}" />
</td>
</tr>
<tr>
<td>
$Lang::tr{'endpoint port'}
</td>
<td>
<input type="number" name="ENDPOINT_PORT"
value="$cgiparams{'ENDPOINT_PORT'}" required
min="1" max="65535" placeholder="${Wireguard::DEFAULT_PORT}"/>
</td>
</tr>
<tr>
<td>
$Lang::tr{'local port'}
</td>
<td>
<input type="number" name="PORT"
value="$cgiparams{'PORT'}" min="1" max="65535"
placeholder="$Lang::tr{'wg leave empty to automatically select'}" />
</td>
</tr>
<tr>
<td>$Lang::tr{'public key'}</td>
<td>
<input type="text" name="PUBLIC_KEY"
value="$cgiparams{'PUBLIC_KEY'}" required />
</td>
</tr>
<tr>
<td>$Lang::tr{'wg pre-shared key (optional)'}</td>
<td>
<input type="text" name="PSK"
value="$cgiparams{'PSK'}" />
</td>
</tr>
<tr>
<td>
$Lang::tr{'wg keepalive interval'}
</td>
<td>
<input type="number" name="KEEPALIVE"
value="$cgiparams{'KEEPALIVE'}" required
min="0" max="65535" />
</td>
</tr>
</table>
<h6>$Lang::tr{'routing'}</h6>
<table class="form">
<tr>
<td>
$Lang::tr{'local subnets'}
</td>
<td>
<input type="text" name="LOCAL_SUBNETS"
value="$cgiparams{'LOCAL_SUBNETS'}" required />
</td>
</tr>
<tr>
<td>
$Lang::tr{'remote subnets'}
</td>
<td>
<input type="text" name="REMOTE_SUBNETS"
value="$cgiparams{'REMOTE_SUBNETS'}" required />
</td>
</tr>
<tr class="action">
<td colspan="2">
<input type='submit' value='$Lang::tr{'save'}' />
</td>
</tr>
</table>
</form>
END
&Header::closebox();
&Header::closepage();
exit(0);
CREATEHOST:
EDITHOST:
# Send HTTP Headers
&Header::showhttpheaders();
# Open the page
&Header::openpage($Lang::tr{'wireguard'}, 1, '');
# Show any error messages
&Header::errorbox(@errormessages);
# Fetch the key
my $key = $cgiparams{'KEY'};
# Open a new box
&Header::openbox('100%', '',
(defined $key) ? $Lang::tr{'wg edit host-to-net peer'} : $Lang::tr{'wg create host-to-net peer'});
# Set defaults
unless (defined $key) {
&General::set_defaults(\%cgiparams, {
"LOCAL_SUBNETS" => "0.0.0.0/0",
});
}
print <<END;
<form method="POST" ENCTYPE="multipart/form-data">
<input type="hidden" name="ACTION" value="SAVE-PEER-HOST">
<input type="hidden" name="KEY" value="$cgiparams{'KEY'}">
<table class="form">
<tr>
<td>
$Lang::tr{'name'}
</td>
<td>
<input type="text" name="NAME"
value="$cgiparams{'NAME'}" required />
</td>
</tr>
<tr>
<td>
$Lang::tr{'remarks'}
</td>
<td>
<input type="text" name="REMARKS"
value="$cgiparams{'REMARKS'}" />
</td>
</tr>
</table>
<h6>$Lang::tr{'routing'}</h6>
<table class="form">
<tr>
<td>
$Lang::tr{'allowed subnets'}
</td>
<td>
<input type="text" name="LOCAL_SUBNETS"
value="$cgiparams{'LOCAL_SUBNETS'}" required />
</td>
</tr>
<tr class="action">
<td colspan="2">
<input type='submit' value='$Lang::tr{'save'}' />
</td>
</tr>
</table>
END
&Header::closebox();
&Header::closepage();
exit(0);