From f4c22fcd5401937933b9f360b445bc9372836bdd Mon Sep 17 00:00:00 2001
From: Vincent Li
Date: Thu, 26 Jun 2025 21:46:30 -0700
Subject: [PATCH] wireguard.cgi: Add CGI to configure wireguard
commit 06dbc836a47160d51ab10f8b9d4ca356beaa7cdb
Author: Michael Tremer
Date: Tue Apr 16 18:06:47 2024 +0200
wireguard.cgi: Add a basic CGI to configure the global settings
Signed-off-by: Michael Tremer
Signed-off-by: Vincent Li
---
config/cfgroot/manualpages | 1 +
config/menu/40-services.menu | 6 +
config/rootfiles/common/web-user-interface | 1 +
doc/language_issues.en | 1 +
html/cgi-bin/wireguard.cgi | 1526 +++++++++++++++++
html/html/themes/ipfire/include/css/style.css | 7 +
langs/en/cgi-bin/en.pl | 42 +
7 files changed, 1584 insertions(+)
create mode 100644 html/cgi-bin/wireguard.cgi
diff --git a/config/cfgroot/manualpages b/config/cfgroot/manualpages
index 1f7e01efc..354fd9c0e 100644
--- a/config/cfgroot/manualpages
+++ b/config/cfgroot/manualpages
@@ -48,6 +48,7 @@ wakeonlan.cgi=configuration/network/wake-on-lan
# Services menu
vpnmain.cgi=configuration/services/ipsec
+wireguard.cgi=configuration/services/wireguard
ovpnmain.cgi=configuration/services/openvpn
ddns.cgi=configuration/services/dyndns
time.cgi=configuration/services/ntp
diff --git a/config/menu/40-services.menu b/config/menu/40-services.menu
index 83ce3bc1f..b904bcc7e 100644
--- a/config/menu/40-services.menu
+++ b/config/menu/40-services.menu
@@ -4,6 +4,12 @@
'title' => "$Lang::tr{'virtual private networking'}",
'enabled' => 1,
};
+ $subservices->{'15.wireguard'} = {
+ 'caption' => $Lang::tr{'wireguard'},
+ 'uri' => '/cgi-bin/wireguard.cgi',
+ 'title' => "$Lang::tr{'wireguard'}",
+ 'enabled' => 1,
+ };
$subservices->{'20.openvpn'} = {
'caption' => 'OpenVPN',
'uri' => '/cgi-bin/ovpnmain.cgi',
diff --git a/config/rootfiles/common/web-user-interface b/config/rootfiles/common/web-user-interface
index f28b2eb9d..fad919d49 100644
--- a/config/rootfiles/common/web-user-interface
+++ b/config/rootfiles/common/web-user-interface
@@ -88,6 +88,7 @@ srv/web/ipfire/cgi-bin/wakeonlan.cgi
srv/web/ipfire/cgi-bin/webaccess.cgi
#srv/web/ipfire/cgi-bin/wio.cgi
#srv/web/ipfire/cgi-bin/wiographs.cgi
+srv/web/ipfire/cgi-bin/wireguard.cgi
srv/web/ipfire/cgi-bin/wireless.cgi
srv/web/ipfire/cgi-bin/wirelessclient.cgi
srv/web/ipfire/cgi-bin/wlanap.cgi
diff --git a/doc/language_issues.en b/doc/language_issues.en
index 3f1626b68..65afd6315 100644
--- a/doc/language_issues.en
+++ b/doc/language_issues.en
@@ -1528,6 +1528,7 @@ WARNING: untranslated string: proxy reports today = Today
WARNING: untranslated string: proxy reports weekly = Weekly reports
WARNING: untranslated string: ptr = PTR
WARNING: untranslated string: ptr lookup failed = Reverse lookup failed
+WARNING: untranslated string: public key = unknown string
WARNING: untranslated string: pulse = Pulse
WARNING: untranslated string: pulse dial = Pulse dial:
WARNING: untranslated string: qos enter bandwidths = You will need to enter your downstream and upstream bandwidth!
diff --git a/html/cgi-bin/wireguard.cgi b/html/cgi-bin/wireguard.cgi
new file mode 100644
index 000000000..6fc01c404
--- /dev/null
+++ b/html/cgi-bin/wireguard.cgi
@@ -0,0 +1,1526 @@
+#!/usr/bin/perl
+###############################################################################
+# #
+# IPFire.org - A linux based firewall #
+# Copyright (C) 2024 Michael Tremer #
+# #
+# 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 . #
+# #
+###############################################################################
+
+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);
+ }
+
+ # 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::settings{'ENABLED'} eq "on") {
+ &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::settings{'ENABLED'} eq "on") {
+ &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::settings{'ENABLED'} eq "on") {
+ &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::settings{'ENABLED'} eq "on") {
+ &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 <
+
+
+ $Lang::tr{'wg download configuration file'}
+
+
+
+
+ $Lang::tr{'wg warning configuration only shown once'}
+
+
+
+
+
+
+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::settings{'ENABLED'} eq "on") {
+ &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::settings{'ENABLED'} eq "on") {
+ &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 <
+
+
+
+
+
+ $Lang::tr{'wg scan the qr code'}
+
+
+
+
+ $Lang::tr{'wg download configuration file'}
+
+
+
+
+ $Lang::tr{'wg warning configuration only shown once'}
+
+
+
+
+
+
+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::settings{'ENABLED'} eq "on") {
+ &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::settings{'ENABLED'} eq "on") ? "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 <
+
+
+ $Lang::tr{'wg host to net client settings'}
+
+
+
+END
+ &Header::closebox();
+
+ # Show a list with all peers
+ &Header::opensection();
+
+ if (%Wireguard::peers) {
+ print <
+
+ |
+ $Lang::tr{'name'}
+ |
+
+
+ $Lang::tr{'remark'}
+ |
+
+
+ $Lang::tr{'status'}
+ |
+
+
+ $Lang::tr{'action'}
+ |
+
+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
+ }
+ }
+
+ # We are not connected...
+ } else {
+ push(@status, "is-disconnected");
+ }
+
+ # Escape remarks
+ if ($remarks) {
+ $remarks = &Header::escape($remarks);
+ }
+
+ print <
+
+ $name
+ |
+
+
+ $remarks
+ |
+END
+
+ if ($location) {
+ print <
+ $connected
+
+
+
+ $location
+ |
+END
+ } else {
+ print <
+ $connected
+
+END
+ }
+
+ print <
+
+
+
+
+
+ |
+
+
+
+ |
+
+END
+ }
+
+ print"";
+ }
+
+ # Show controls
+ print <
+
+ |
+
+ |
+
+
+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
+
+ &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 <
+
+
+
+
+ $Lang::tr{'routing'}
+
+
+
+
+
+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 <
+
+
+
+
+ $Lang::tr{'endpoint'}
+
+
+
+ $Lang::tr{'routing'}
+
+
+
+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 <
+
+
+
+
+
+ $Lang::tr{'endpoint'}
+
+
+
+ $Lang::tr{'routing'}
+
+
+
+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 <
+
+
+
+
+
+ $Lang::tr{'routing'}
+
+
+END
+
+ &Header::closebox();
+ &Header::closepage();
+
+ exit(0);
diff --git a/html/html/themes/ipfire/include/css/style.css b/html/html/themes/ipfire/include/css/style.css
index 56e6f26df..d9c4c9d1c 100644
--- a/html/html/themes/ipfire/include/css/style.css
+++ b/html/html/themes/ipfire/include/css/style.css
@@ -155,6 +155,13 @@ iframe {
text-align: right;
}
+/*
+ Text Colors
+*/
+.text-error {
+ color: var(--color-red);
+}
+
/* Header */
#header {
diff --git a/langs/en/cgi-bin/en.pl b/langs/en/cgi-bin/en.pl
index 52aca8a83..319c90b8b 100644
--- a/langs/en/cgi-bin/en.pl
+++ b/langs/en/cgi-bin/en.pl
@@ -3030,6 +3030,48 @@
'weekly firewallhits' => 'weekly firewallhits',
'weeks' => 'Weeks',
'wg' => 'WireGuard',
+'wg client configuration file' => 'WireGuard Client Configuration File',
+'wg client pool' => 'Client Pool',
+'wg create host-to-net peer' => 'Create A New Host-To-Net Peer',
+'wg create net-to-net peer' => 'Create A New Net-To-Net Peer',
+'wg create peer' => 'Create A New Peer',
+'wg dns' => 'DNS',
+'wg download configuration' => 'Download Configuration',
+'wg download configuration file' => 'Download the configuration file',
+'wg edit host-to-net peer' => 'Edit Host-To-Net Peer',
+'wg edit net-to-net peer' => 'Edit Net-To-Net Peer',
+'wg edit peer' => 'Edit Peer',
+'wg host to net client settings' => 'Host-To-Net Client Settings',
+'wg import peer' => 'Import Peer',
+'wg invalid client dns' => 'Invalid client DNS address',
+'wg invalid client pool' => 'Invalid client pool',
+'wg invalid endpoint address' => 'Invalid endpoint address',
+'wg invalid endpoint port' => 'Invalid endpoint port',
+'wg invalid keepalive interval' => 'Invalid Keepalive Interval (Must be between 0 and 65535)',
+'wg invalid local subnet' => 'Invalid local subnet',
+'wg invalid name' => 'Invalid name (Only letters, numbers, space and hyphen are allowed)',
+'wg invalid psk' => 'Invalid pre-shared key',
+'wg invalid public key' => 'Invalid public key',
+'wg invalid remote subnet' => 'Invalid remote subnet',
+'wg keepalive interval' => 'Keepalive Interval',
+'wg leave empty to automatically select' => 'Leave empty to automatically select',
+'wg missing allowed ips' => 'Missing AllowedIPs',
+'wg missing endpoint address' => 'Missing Endpoint Address',
+'wg missing endpoint port' => 'Missing Endpoint Port',
+'wg missing port' => 'Missing Port',
+'wg missing private key' => 'Missing Private Key',
+'wg missing public key' => 'Missing Public Key',
+'wg name is already used' => 'The name is already in use',
+'wg no local subnets' => 'No local subnets given',
+'wg no more free addresses in pool' => 'No more free addresses in pool',
+'wg no remote subnets' => 'No remote subnets given',
+'wg peer configuration' => 'Peer Configuration',
+'wg peer does not exist' => 'Peer does not exist',
+'wg pre-shared key (optional)' => 'Pre-Shared Key (optional)',
+'wg rw peers' => 'WireGuard Roadwarrior Peers',
+'wg scan the qr code' => 'Scan the QR code to import the WireGuard configuration into a mobile client.',
+'wg show configuration qrcode' => 'Show Configuration QR Code',
+'wg warning configuration only shown once' => 'Attention: This WireGuard configuration file will only be shown this one time as it contains private key material that is not being stored on IPFire.',
'whitelisted' => 'Whitelisted',
'whois results from' => 'WHOIS results from',
'wildcards' => 'Wildcards',