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{'qr code'} +

+ +

+ $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{'enabled'} + +
$Lang::tr{'endpoint'} + +
$Lang::tr{'port'} + +
+ +
$Lang::tr{'wg host to net client settings'}
+ + + + + + + + + + + + + + + +
$Lang::tr{'wg client pool'} + +
$Lang::tr{'wg dns'} + +
+ +
+ +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 = < + $country + +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{'name'} + + +
+ $Lang::tr{'remarks'} + + +
+ $Lang::tr{'configuration file'} + + +
+ +
$Lang::tr{'routing'}
+ + + + + + + +
+ $Lang::tr{'local subnets'} + + +
+ + + + + +
+ +
+ +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{'name'} + + +
+ $Lang::tr{'remarks'} + + +
+ +
$Lang::tr{'endpoint'}
+ + + + + + + +
+ $Lang::tr{'endpoint address'} + + +
+ +
$Lang::tr{'routing'}
+ + + + + + + + + + + + + + + + + +
+ $Lang::tr{'local subnets'} + + +
+ $Lang::tr{'remote subnets'} + + +
+ +
+ +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{'name'} + + +
+ $Lang::tr{'remarks'} + + +
+ $Lang::tr{'public key'} + + +
+ +
$Lang::tr{'endpoint'}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ $Lang::tr{'endpoint address'} + + +
+ $Lang::tr{'endpoint port'} + + +
+ $Lang::tr{'local port'} + + +
$Lang::tr{'public key'} + +
$Lang::tr{'wg pre-shared key (optional)'} + +
+ $Lang::tr{'wg keepalive interval'} + + +
+ +
$Lang::tr{'routing'}
+ + + + + + + + + + + + + + + + + +
+ $Lang::tr{'local subnets'} + + +
+ $Lang::tr{'remote subnets'} + + +
+ +
+ +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{'name'} + + +
+ $Lang::tr{'remarks'} + + +
+ +
$Lang::tr{'routing'}
+ + + + + + + + + + + +
+ $Lang::tr{'allowed subnets'} + + +
+ +
+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',