mirror of
https://github.com/vincentmli/bpfire.git
synced 2026-04-09 18:45:54 +02:00
commit 68a3334413efb1a963b7cc6c6dca1ec0126e1cc1
Author: Michael Tremer <michael.tremer@ipfire.org>
Date: Fri Jul 18 08:42:12 2025 +0000
wireguard-functions.pl: Automatically skip IPv6 subnets
Since we do not support this and some VPN providers generate
configuration files that send any data over to them, we simply ignore
any IPv6 subnets.
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
commit f7565a885b55384a64edd8bd73079143a04da519
Author: Michael Tremer <michael.tremer@ipfire.org>
Date: Fri Jul 18 09:57:34 2025 +0000
wireguard-functions.pl: Remove any carriage returns on import
Some files might include carriage returns which won't be removed by
chomp() on Linux. To be extra safe, we remove them manually.
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Signed-off-by: Vincent Li <vincent.mc.li@gmail.com>
676 lines
16 KiB
Perl
676 lines
16 KiB
Perl
#!/usr/bin/perl
|
|
###############################################################################
|
|
# #
|
|
# IPFire.org - A linux based firewall #
|
|
# Copyright (C) 2024 Michael Tremer <michael.tremer@ipfire.org> #
|
|
# #
|
|
# This program is free software: you can redistribute it and/or modify #
|
|
# it under the terms of the GNU General Public License as published by #
|
|
# the Free Software Foundation, either version 3 of the License, or #
|
|
# (at your option) any later version. #
|
|
# #
|
|
# This program is distributed in the hope that it will be useful, #
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
|
# GNU General Public License for more details. #
|
|
# #
|
|
# You should have received a copy of the GNU General Public License #
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
|
# #
|
|
###############################################################################
|
|
|
|
package Wireguard;
|
|
|
|
use strict;
|
|
use MIME::Base64;
|
|
|
|
require "/var/ipfire/general-functions.pl";
|
|
require "/var/ipfire/network-functions.pl";
|
|
|
|
our @DEFAULT_PORTRANGE = (60000, 62000);
|
|
our $DEFAULT_PORT = 51820;
|
|
our $DEFAULT_KEEPALIVE = 25;
|
|
|
|
# Read the global configuration
|
|
our %settings = ();
|
|
|
|
if (-e "/var/ipfire/wireguard/settings") {
|
|
&General::readhash("/var/ipfire/wireguard/settings", \%settings);
|
|
}
|
|
|
|
# Read all peers
|
|
our %peers = ();
|
|
|
|
if (-e "/var/ipfire/wireguard/peers") {
|
|
&General::readhasharray("/var/ipfire/wireguard/peers", \%peers);
|
|
}
|
|
|
|
# Set any defaults
|
|
&General::set_defaults(\%settings, {
|
|
"ENABLED" => "off",
|
|
"PORT" => $DEFAULT_PORT,
|
|
"CLIENT_DNS" => $Network::ethernet{'GREEN_ADDRESS'},
|
|
});
|
|
|
|
# Returns true if WireGuard is enabled
|
|
sub is_enabled() {
|
|
return ($settings{'ENABLED'} eq "on");
|
|
}
|
|
|
|
# Returns the local endpoint
|
|
sub get_endpoint() {
|
|
my $endpoint = $settings{'ENDPOINT'};
|
|
|
|
# If no endpoint is set, we fall back to the FQDN of the firewall
|
|
if ($endpoint eq "") {
|
|
$endpoint = $General::mainsettings{'HOSTNAME'} . "." . $General::mainsettings{'DOMAINNAME'};
|
|
}
|
|
|
|
return $endpoint;
|
|
}
|
|
|
|
# This function generates a set of keys for this host if none exist
|
|
sub generate_keys($) {
|
|
my $force = shift || 0;
|
|
|
|
# Reset any previous keys if re-generation forced
|
|
if ($force) {
|
|
$settings{"PRIVATE_KEY"} = undef;
|
|
$settings{"PUBLIC_KEY"} = undef;
|
|
}
|
|
|
|
# Return if we already have keys
|
|
return if (defined $settings{"PRIVATE_KEY"} && defined $settings{"PUBLIC_KEY"});
|
|
|
|
# Generate a new private key
|
|
unless (defined $settings{'PRIVATE_KEY'}) {
|
|
# Generate a new private key
|
|
$settings{"PRIVATE_KEY"} = &generate_private_key();
|
|
|
|
# Reset the public key
|
|
$settings{"PUBLIC_KEY"} = undef;
|
|
}
|
|
|
|
# Derive the public key
|
|
unless (defined $settings{"PUBLIC_KEY"}) {
|
|
# Derive the public key
|
|
$settings{"PUBLIC_KEY"} = &derive_public_key($settings{"PRIVATE_KEY"});
|
|
}
|
|
|
|
# Store the configuration file
|
|
&General::writehash("/var/ipfire/wireguard/settings", \%settings);
|
|
}
|
|
|
|
# Generates a new private key
|
|
sub generate_private_key() {
|
|
# Generate a new private key
|
|
my @output = &General::system_output("wg", "genkey");
|
|
|
|
# Store the key
|
|
foreach (@output) {
|
|
chomp;
|
|
|
|
return $_;
|
|
}
|
|
|
|
# Return undefined on error
|
|
return undef;
|
|
}
|
|
|
|
# Takes a private key and derives the public key
|
|
sub derive_public_key($) {
|
|
my $private_key = shift;
|
|
my @output = ();
|
|
|
|
# Derive the public key
|
|
if (open(STDIN, "-|")) {
|
|
@output = &General::system_output("wg", "pubkey");
|
|
} else {
|
|
print $private_key . "\n";
|
|
exit (0);
|
|
}
|
|
|
|
# Return the first line
|
|
foreach (@output) {
|
|
chomp;
|
|
|
|
return $_;
|
|
}
|
|
|
|
# Return undefined on error
|
|
return undef;
|
|
}
|
|
|
|
sub dump($) {
|
|
my $intf = shift;
|
|
|
|
my %dump = ();
|
|
my $lineno = 0;
|
|
|
|
# Fetch the dump
|
|
my @output = &General::system_output("/usr/local/bin/wireguardctrl", "dump", $intf);
|
|
|
|
foreach my $line (@output) {
|
|
# Increment the line numbers
|
|
$lineno++;
|
|
|
|
# Skip the first line
|
|
next if ($lineno <= 1);
|
|
|
|
# Split the line into its fields
|
|
my @fields = split(/\t/, $line);
|
|
|
|
# Create a new hash indexed by the public key
|
|
$dump{$fields[0]} = {
|
|
"psk" => $fields[1],
|
|
"endpoint" => $fields[2],
|
|
"allowed-ips" => $fields[3],
|
|
"latest-handshake" => $fields[4],
|
|
"transfer-rx" => $fields[5],
|
|
"transfer-tx" => $fields[6],
|
|
"persistent-keepalive" => $fields[7],
|
|
};
|
|
}
|
|
|
|
return %dump;
|
|
}
|
|
|
|
sub load_peer($) {
|
|
my $key = shift;
|
|
|
|
my $type = $peers{$key}[1];
|
|
|
|
my %peer = (
|
|
"ENABLED" => $peers{$key}[0],
|
|
"TYPE" => $type,
|
|
"NAME" => $peers{$key}[2],
|
|
"PUBLIC_KEY" => $peers{$key}[3],
|
|
"PRIVATE_KEY" => $peers{$key}[4],
|
|
"PORT" => $peers{$key}[5],
|
|
"ENDPOINT_ADDR" => $peers{$key}[6],
|
|
"ENDPOINT_PORT" => $peers{$key}[7],
|
|
($type eq "host") ? "CLIENT_ADDRESS" : "REMOTE_SUBNETS"
|
|
=> &decode_subnets($peers{$key}[8]),
|
|
"REMARKS" => &decode_remarks($peers{$key}[9]),
|
|
"LOCAL_SUBNETS" => &decode_subnets($peers{$key}[10]),
|
|
"PSK" => $peers{$key}[11],
|
|
"KEEPALIVE" => $peers{$key}[12],
|
|
"LOCAL_ADDRESS" => $peers{$key}[13],
|
|
"INTERFACE" => ($type eq "host") ? "wg0" : "wg${key}",
|
|
);
|
|
|
|
return \%peer;
|
|
}
|
|
|
|
sub get_peer_by_name($) {
|
|
my $name = shift;
|
|
|
|
foreach my $key (keys %peers) {
|
|
my $peer = &load_peer($key);
|
|
|
|
# Return the peer if the name matches
|
|
if ($peer->{"NAME"} eq $name) {
|
|
return $peer;
|
|
}
|
|
}
|
|
|
|
# Return undefined if nothing was found
|
|
return undef;
|
|
}
|
|
|
|
sub name_is_valid($) {
|
|
my $name = shift;
|
|
|
|
# The name must be between 1 and 63 characters
|
|
if (length ($name) < 1 || length ($name) > 63) {
|
|
return 0;
|
|
}
|
|
|
|
# Only valid characters are a-z, A-Z, 0-9, space and -
|
|
if ($name !~ /^[a-zA-Z0-9 -]*$/) {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub name_is_free($) {
|
|
my $name = shift;
|
|
my $key = shift || 0;
|
|
|
|
foreach my $i (keys %peers) {
|
|
# Skip the connection with ID
|
|
next if ($key eq $i);
|
|
|
|
# Return if we found a match
|
|
return 0 if ($peers{$i}[2] eq $name);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub key_is_valid($) {
|
|
my $key = shift;
|
|
|
|
# Try to decode the key
|
|
$key = &MIME::Base64::decode_base64($key);
|
|
|
|
# All keys must be 32 bytes long
|
|
return length($key) == 32;
|
|
}
|
|
|
|
sub keepalive_is_valid($) {
|
|
my $keepalive = shift;
|
|
|
|
# Must be a number
|
|
return 0 unless ($keepalive =~ m/^[0-9]+$/);
|
|
|
|
# Must be between 0 and 65535 (inclusive)
|
|
return 0 if ($keepalive lt 0);
|
|
return 0 if ($keepalive gt 65535);
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub encode_remarks($) {
|
|
my $remarks = shift;
|
|
|
|
# Encode to Base64
|
|
$remarks = &MIME::Base64::encode_base64($remarks);
|
|
|
|
# Remove the trailing newline
|
|
chomp($remarks);
|
|
|
|
return $remarks;
|
|
}
|
|
|
|
sub decode_remarks($) {
|
|
my $remarks = shift;
|
|
|
|
# Decode from base64
|
|
return &MIME::Base64::decode_base64($remarks);
|
|
}
|
|
|
|
sub encode_subnets($) {
|
|
my @subnets = @_;
|
|
|
|
my @formatted = ();
|
|
|
|
# wg only handles the CIDR notation
|
|
foreach my $subnet (@subnets) {
|
|
my $netaddr = &Network::get_netaddress($subnet);
|
|
my $prefix = &Network::get_prefix($subnet);
|
|
|
|
next unless (defined $netaddr && defined $prefix);
|
|
|
|
push(@formatted, "${netaddr}/${prefix}");
|
|
}
|
|
|
|
# Join subnets together separated by |
|
|
return join("|", @formatted);
|
|
}
|
|
|
|
sub decode_subnets($) {
|
|
my $subnets = shift;
|
|
|
|
# Split the string
|
|
my @subnets = split(/\|/, $subnets);
|
|
|
|
return \@subnets;
|
|
}
|
|
|
|
sub pool_is_in_use($) {
|
|
my $pool = shift;
|
|
|
|
foreach my $key (keys %peers) {
|
|
my $type = $peers{$key}[1];
|
|
my $address = $peers{$key}[6];
|
|
|
|
# Check if a host is using an IP address from the pool
|
|
if ($type eq "host" && &Network::ip_address_in_network($address, $pool)) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
# No match found
|
|
return 0;
|
|
}
|
|
|
|
# Takes the pool and an optional limit of up to how many addresses to return
|
|
sub free_pool_addresses($$) {
|
|
my $pool = shift;
|
|
my $limit = shift || 0;
|
|
|
|
my @used_addresses = ();
|
|
my @free_addresses = ();
|
|
|
|
# wg0 IP is reserved so put in @used_addresses
|
|
push(@used_addresses, &Network::ip2bin($settings{'ADDRESS'}));
|
|
|
|
# Collect all used addresses
|
|
foreach my $key (keys %peers) {
|
|
my $peer = &load_peer($key);
|
|
|
|
# Only check hosts
|
|
next if ($peer->{"TYPE"} ne "host");
|
|
|
|
foreach my $address (@{ $peer->{"CLIENT_ADDRESS"} }) {
|
|
push(@used_addresses, &Network::ip2bin($address));
|
|
}
|
|
}
|
|
|
|
# Fetch the first address
|
|
my $address = &Network::get_netaddress($pool);
|
|
|
|
# Fetch the last address
|
|
my $broadcast = &Network::get_broadcast($pool);
|
|
$broadcast = &Network::ip2bin($broadcast);
|
|
|
|
# Walk through all addresses excluding the first and last address.
|
|
# No technical reason, we just don't want to confuse people.
|
|
OUTER: for (my $i = &Network::ip2bin($address) + 1; $i < $broadcast; $i++) {
|
|
# Skip any addresses that already in use
|
|
foreach my $used_address (@used_addresses) {
|
|
next OUTER if ($i == $used_address);
|
|
}
|
|
|
|
push(@free_addresses, &Network::bin2ip($i));
|
|
|
|
# Check limit
|
|
last if ($limit > 0 && scalar @free_addresses >= $limit);
|
|
}
|
|
|
|
return @free_addresses;
|
|
}
|
|
|
|
sub generate_peer_configuration($$) {
|
|
my $key = shift;
|
|
my $private_key = shift;
|
|
|
|
my @conf = ();
|
|
|
|
# Load the peer
|
|
my $peer = &load_peer($key);
|
|
|
|
# Return if we could not find the peer
|
|
return undef unless ($peer);
|
|
|
|
my @allowed_ips = ();
|
|
|
|
# Convert all subnets into CIDR notation
|
|
foreach my $subnet (@{ $peer->{'LOCAL_SUBNETS'} }) {
|
|
my $netaddress = &Network::get_netaddress($subnet);
|
|
my $prefix = &Network::get_prefix($subnet);
|
|
|
|
# Skip invalid subnets
|
|
next if (!defined $netaddress || !defined $prefix);
|
|
|
|
push(@allowed_ips, "${netaddress}/${prefix}");
|
|
}
|
|
|
|
# Fetch the endpoint
|
|
my $endpoint = &get_endpoint();
|
|
|
|
# Net-2-Net
|
|
if ($peer->{'TYPE'} eq "net") {
|
|
# Derive our own public key
|
|
my $public_key = &derive_public_key($peer->{'PRIVATE_KEY'});
|
|
|
|
push(@conf,
|
|
"[Interface]",
|
|
"PrivateKey = $private_key",
|
|
"ListenPort = $peer->{'ENDPOINT_PORT'}",
|
|
"",
|
|
"[Peer]",
|
|
"Endpoint = ${endpoint}:$peer->{'PORT'}",
|
|
"PublicKey = $public_key",
|
|
"PresharedKey = $peer->{'PSK'}",
|
|
"AllowedIPs = " . join(", ", @allowed_ips),
|
|
"PersistentKeepalive = $peer->{'KEEPALIVE'}",
|
|
);
|
|
|
|
# Host-2-Net
|
|
} elsif ($peer->{'TYPE'} eq "host") {
|
|
# Fetch any DNS servers for hosts
|
|
my @dns = split(/\|/, $settings{'CLIENT_DNS'});
|
|
|
|
push(@conf,
|
|
"[Interface]",
|
|
"PrivateKey = $private_key",
|
|
"Address = @{ $peer->{'CLIENT_ADDRESS'} }/32",
|
|
);
|
|
|
|
# Optionally add DNS servers
|
|
if (scalar @dns) {
|
|
push(@conf, "DNS = " . join(", ", @dns));
|
|
}
|
|
|
|
# Finish the [Interface] section
|
|
push(@conf, "");
|
|
|
|
# Add peer configuration
|
|
push(@conf, (
|
|
"[Peer]",
|
|
"Endpoint = ${endpoint}:$settings{'PORT'}",
|
|
"PublicKey = $settings{'PUBLIC_KEY'}",
|
|
"PresharedKey = $peer->{'PSK'}",
|
|
"AllowedIPs = " . join(", ", @allowed_ips),
|
|
"PersistentKeepalive = $DEFAULT_KEEPALIVE",
|
|
));
|
|
}
|
|
|
|
return join("\n", @conf);
|
|
}
|
|
|
|
sub parse_configuration($$) {
|
|
my $name = shift;
|
|
my $fh = shift;
|
|
|
|
my %peer = (
|
|
"NAME" => $name,
|
|
);
|
|
|
|
# Collect any errors
|
|
my @errormessages = ();
|
|
|
|
my $section = undef;
|
|
my $key = undef;
|
|
my $val = undef;
|
|
|
|
# Check if the name is valid
|
|
unless (&Wireguard::name_is_valid($name)) {
|
|
push(@errormessages, $Lang::tr{'wg invalid name'});
|
|
}
|
|
|
|
# Check if the name is already taken
|
|
unless (&Wireguard::name_is_free($name)) {
|
|
push(@errormessages, $Lang::tr{'wg name is already used'});
|
|
}
|
|
|
|
while (<$fh>) {
|
|
# Remove line breaks
|
|
chomp;
|
|
|
|
# Remove any carriage returns
|
|
$_ =~ s/\r$//;
|
|
|
|
# Search for section headers
|
|
if ($_ =~ m/^\[(\w+)\]$/) {
|
|
$section = $1;
|
|
next;
|
|
|
|
# Search for key = value lines
|
|
} elsif ($_ =~ m/^(\w+)\s+=\s+(.*)$/) {
|
|
# Skip anything before the first section header
|
|
next unless (defined $section);
|
|
|
|
# Store keys and values
|
|
$key = $1;
|
|
$val = $2;
|
|
|
|
# Skip any unhandled lines
|
|
} else {
|
|
next;
|
|
}
|
|
|
|
# Interface section
|
|
if ($section eq "Interface") {
|
|
# Address
|
|
if ($key eq "Address") {
|
|
my $address = &Network::get_netaddress($val);
|
|
my $prefix = &Network::get_prefix($val);
|
|
|
|
# There must be an address
|
|
unless ($address) {
|
|
push(@errormessages, $Lang::tr{'invalid ip address'});
|
|
}
|
|
|
|
# If there was a prefix it must be /32
|
|
if (defined $prefix) {
|
|
unless ($prefix == 32) {
|
|
push(@errormessages, $Lang::tr{'invalid ip address'});
|
|
}
|
|
}
|
|
|
|
# Store the address
|
|
$peer{'LOCAL_ADDRESS'} = ${address};
|
|
|
|
# ListenPort
|
|
} elsif ($key eq "ListenPort") {
|
|
if (&General::validport($val)) {
|
|
$peer{'PORT'} = $val;
|
|
} else {
|
|
push(@errormessages, $Lang::tr{'wg invalid endpoint port'});
|
|
}
|
|
|
|
# PrivateKey
|
|
} elsif ($key eq "PrivateKey") {
|
|
if (&key_is_valid($val)) {
|
|
$peer{'PRIVATE_KEY'} = $val;
|
|
} else {
|
|
push(@errormessages, $Lang::tr{'malformed private key'});
|
|
}
|
|
}
|
|
|
|
# Peer section
|
|
} elsif ($section eq "Peer") {
|
|
# PublicKey
|
|
if ($key eq "PublicKey") {
|
|
if (&key_is_valid($val)) {
|
|
$peer{'PUBLIC_KEY'} = $val;
|
|
} else {
|
|
push(@errormessages, $Lang::tr{'malformed public key'});
|
|
}
|
|
|
|
# PresharedKey
|
|
} elsif ($key eq "PresharedKey") {
|
|
if (&key_is_valid($val)) {
|
|
$peer{'PSK'} = $val;
|
|
} else {
|
|
push(@errormessages, $Lang::tr{'malformed preshared key'});
|
|
}
|
|
|
|
# AllowedIPs
|
|
} elsif ($key eq "AllowedIPs") {
|
|
my @networks = split(/,/, $val);
|
|
|
|
# Check if all networks are valid
|
|
foreach my $network (@networks) {
|
|
# Skip any IPv6 networks
|
|
next if ($network =~ m/:/);
|
|
|
|
unless (&Network::check_subnet($network)) {
|
|
push(@errormessages, $Lang::tr{'invalid network'} . " $network");
|
|
}
|
|
}
|
|
|
|
$peer{'REMOTE_SUBNETS'} = \@networks;
|
|
# Endpoint
|
|
} elsif ($key eq "Endpoint") {
|
|
my $address = $val;
|
|
my $port = $DEFAULT_PORT;
|
|
|
|
# Try to separate the port (if any)
|
|
if ($val =~ m/^(.*):(\d+)$/) {
|
|
$address = $1;
|
|
$port = $2;
|
|
}
|
|
|
|
# Check if we have a valid IP address
|
|
if (&Network::check_ip_address($address)) {
|
|
# nothing
|
|
|
|
# Check if we have a valid FQDN
|
|
} elsif (&General::validfqdn($address)) {
|
|
# nothing
|
|
|
|
# Otherwise this fails
|
|
} else {
|
|
push(@errormessages, $Lang::tr{'invalid endpoint address'});
|
|
next;
|
|
}
|
|
|
|
# Store the values
|
|
$peer{'ENDPOINT_ADDRESS'} = $address;
|
|
$peer{'ENDPOINT_PORT'} = $port;
|
|
|
|
# PersistentKeepalive
|
|
} elsif ($key eq "PersistentKeepalive") {
|
|
# Must be an integer
|
|
if ($val =~ m/^(\d+)$/) {
|
|
$peer{'KEEPALIVE'} = $1;
|
|
} else {
|
|
push(@errormessages, $Lang::tr{'invalid keepalive interval'});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check if we have all required properties
|
|
unless (exists $peer{"PRIVATE_KEY"}) {
|
|
push(@errormessages, $Lang::tr{'wg missing private key'});
|
|
}
|
|
|
|
unless (exists $peer{"PUBLIC_KEY"}) {
|
|
push(@errormessages, $Lang::tr{'wg missing public key'});
|
|
}
|
|
|
|
unless (exists $peer{"REMOTE_SUBNETS"}) {
|
|
push(@errormessages, $Lang::tr{'wg missing allowed ips'});
|
|
}
|
|
|
|
unless (exists $peer{"ENDPOINT_ADDRESS"}) {
|
|
push(@errormessages, $Lang::tr{'wg missing endpoint address'});
|
|
}
|
|
|
|
unless (exists $peer{"ENDPOINT_PORT"}) {
|
|
push(@errormessages, $Lang::tr{'wg missing endpoint port'});
|
|
}
|
|
|
|
return \%peer, @errormessages;
|
|
}
|
|
|
|
sub get_free_port() {
|
|
my @used_ports = ();
|
|
|
|
my $tries = 100;
|
|
|
|
# Collect all ports that are already in use
|
|
foreach my $key (keys %peers) {
|
|
push(@used_ports, $peers{$key}[5]);
|
|
}
|
|
|
|
my ($port_start, $port_end) = @DEFAULT_PORTRANGE;
|
|
|
|
while ($tries-- > 0) {
|
|
my $port = $port_start + int(rand($port_end - $port_start));
|
|
|
|
# Return the port unless it is already in use
|
|
return $port unless (grep { $port == $_ } @used_ports);
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
1;
|