diff --git a/config/cfgroot/manualpages b/config/cfgroot/manualpages index fe5ebc0b8..35f72db8c 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 68142d290..37f86237f 100644 --- a/config/rootfiles/common/web-user-interface +++ b/config/rootfiles/common/web-user-interface @@ -86,6 +86,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 86d5890f2..bfa45b769 100644 --- a/doc/language_issues.en +++ b/doc/language_issues.en @@ -1559,6 +1559,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..b9661948c --- /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::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 < +

+ + $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::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 < +

+ $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::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 < + + + + + + + + + + + + + + + +
$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 7cf85d68d..b592e5386 100644 --- a/html/html/themes/ipfire/include/css/style.css +++ b/html/html/themes/ipfire/include/css/style.css @@ -118,6 +118,29 @@ iframe { float: right !important; } +/* + Text Alignment +*/ + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-right { + 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 fc5e5f6b2..14565584a 100644 --- a/langs/en/cgi-bin/en.pl +++ b/langs/en/cgi-bin/en.pl @@ -434,6 +434,7 @@ 'all services' => 'All Services', 'all updates installed' => 'All updates installed', 'allmsg' => 'show all', +'allowed subnets' => 'Allowed Subnets', 'alt dialup' => 'Dialup', 'alt home' => 'Home', 'alt information' => 'Information', @@ -671,6 +672,7 @@ 'compression' => 'Compression:', 'computer to modem rate' => 'Computer to modem rate:', 'concentrator name' => 'Concentrator name:', +'configuration file' => 'Configuration File', 'confirmation' => 'confirmation', 'connect' => 'OVPN Start / Connect', 'connect the modem' => 'Connect the modem', @@ -932,7 +934,7 @@ 'donation-link' => 'https://www.paypal.com/en_US/GB/i/btn/btn_donateCC_LG.gif', 'donation-ipfire-text' => 'IPFire is driven and maintained by volunteers in their free time. To keep this project running costs incurred, if you like to support us we would be pleased by a small donation.', 'donation-bpfire-text' => 'BPFire is to enable revolutionary eBPF technology for non-tech savvy users, make eBPF technology available to home users or any size of organizations to secure their network environment, It is driven and maintained by Vincent Li in his free time. To keep this project running costs incurred, if you like to support him he would be pleased by a small donation.', -'done' => 'Do it', +'done' => 'Done', 'dos charset' => 'DOS Charset', 'down and up speed' => 'Enter your Down- and Uplink-Speed
and then press Save.', 'downfall gather data sampling' => 'Downfall/Gather Data Sampling', @@ -1037,6 +1039,9 @@ 'encrypted' => 'Encrypted', 'encryption' => 'Encryption:', 'end address' => 'End address:', +'endpoint' => 'Endpoint', +'endpoint address' => 'Endpoint Address', +'endpoint port' => 'Endpoint Port', 'enter ack class' => 'Enter the ACK- Class
and then press Save.', 'enter data' => 'Enter your settings
and then press Save.', 'entropy' => 'Entropy', @@ -1484,6 +1489,7 @@ 'ike lifetime should be between 1 and 24 hours' => 'IKE lifetime should be between 1 and 24 hours.', 'imei' => 'IMEI', 'import' => 'Import', +'import connection' => 'Import a Connection', 'importkey' => 'Import PSK', 'imsi' => 'IMSI', 'in' => 'In', @@ -1542,6 +1548,8 @@ 'invalid domain name' => 'Invalid domain name.', 'invalid downlink speed' => 'Invalid downlink speed.', 'invalid end address' => 'Invalid end address.', +'invalid endpoint' => 'Invalid Endpoint', +'invalid endpoint address' => 'Invalid Endpoint Address', 'invalid fixed ip address' => 'Invalid fixed IP address', 'invalid fixed mac address' => 'Invalid fixed MAC address', 'invalid hostname' => 'Invalid hostname.', @@ -1576,8 +1584,10 @@ 'invalid input for subscription code' => 'Invalid input for subscription code', 'invalid input for valid till days' => 'Invalid input for Valid till (days).', 'invalid ip' => 'Invalid IP Address', +'invalid ip address' => 'Invalid IP Address', 'invalid ip or hostname' => 'Invalid IP Address or Hostname', 'invalid keep time' => 'Keep time must be a valid number', +'invalid keepalive interval' => 'Invalid Keepalive Interval', 'invalid key' => 'Invalid key.', 'invalid loaded file' => 'Invalid loaded file', 'invalid local-remote id' => 'local & remote id must not be equal and begin with a "@" sign. These are leftid and rightid in strongswan terminology.', @@ -1592,6 +1602,7 @@ 'invalid minimum object size' => 'Invalid minimum object size.', 'invalid mtu input' => 'Invalid MTU', 'invalid netmask' => 'Invalid netmask', +'invalid network' => 'Invalid Network', 'invalid port' => 'Invalid port. Must be a valid port number.', 'invalid port list' => 'Port list syntax is: port[,port]... where port is in /etc/services or number', 'invalid primary dns' => 'Invalid primary DNS.', @@ -1697,8 +1708,10 @@ 'local ip address' => 'Local IP Address', 'local master' => 'Local Master', 'local ntp server specified but not enabled' => 'Local NTP server specified but not enabled', +'local port' => 'Local Port', 'local subnet' => 'Local subnet:', 'local subnet is invalid' => 'Local subnet is invalid.', +'local subnets' => 'Local Subnets', 'local vpn hostname/ip' => 'Local VPN Hostname/IP', 'localkey' => 'Localkey', 'localkeyfile' => 'Localkeyfile', @@ -1768,6 +1781,9 @@ 'mailmethod' => 'Mailmethod', 'mailprogramm' => 'Mailprogramm', 'main page' => 'Main page', +'malformed preshared key' => 'Malformed Pre-Shared Key', +'malformed private key' => 'Malformed Private Key', +'malformed public key' => 'Malformed Public Key', 'manage ovpn' => '5. Tunnel Management:', 'manage printers' => 'manage printers', 'manage shares' => 'Manage Shares', @@ -2205,12 +2221,14 @@ 'psk' => 'PSK', 'ptr' => 'PTR', 'ptr lookup failed' => 'Reverse lookup failed', +'public key' => 'Public Key', 'pulse' => 'Pulse', 'pulse dial' => 'Pulse dial:', 'qos add subclass' => 'Add subclass', 'qos enter bandwidths' => 'You will need to enter your downstream and upstream bandwidth!', 'qos graphs' => 'Qos Graphs', 'qos warning' => 'The rule must be saved, otherwise it will be discarded!', +'qr code' => 'QR Code', 'quick control' => 'Quick Control', 'quick playlist' => 'Quick Playlist', 'ram' => 'RAM', @@ -2246,6 +2264,7 @@ 'reload' => 'reload', 'remark' => 'Remark', 'remark title' => 'Remark:', +'remarks' => 'Remarks', 'remote access' => 'Remote access', 'remote announce' => 'Remote Announce', 'remote browse sync' => 'Remote Browse Sync', @@ -2253,6 +2272,7 @@ 'remote logging' => 'Remote logging', 'remote subnet' => 'Remote subnet:', 'remote subnet is invalid' => 'Remote subnet is invalid.', +'remote subnets' => 'Remote Subnets', 'removable device advice' => 'Plug in a device, refresh, select and mount before usage. Umount before removal.', 'remove' => 'Remove', 'remove ca certificate' => 'Remove CA certificate', @@ -2286,6 +2306,7 @@ 'root user password' => 'Root password', 'route subnet is invalid' => 'Additional push route subnet is invalid', 'router ip' => 'Router IP address:', +'routing' => 'Routing', 'routing table entries' => 'Routing Table Entries', 'rsvd dst port overlap' => 'Destination Port Range overlaps a port reserved for IPFire:', 'rsvd src port overlap' => 'Source Port Range overlaps a port reserved for IPFire:', @@ -3102,6 +3123,48 @@ 'week-graph' => 'Week', 'weekly firewallhits' => 'weekly firewallhits', 'weeks' => 'Weeks', +'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.', 'whois results from' => 'WHOIS results from', 'wildcards' => 'Wildcards', 'winbind daemon' => 'Winbind Daemon',