]> git.ipfire.org Git - people/ms/ipfire-2.x.git/commitdiff
wireguard: Implement creating an extra interface per N2N peer
authorMichael Tremer <michael.tremer@ipfire.org>
Mon, 30 Sep 2024 16:53:45 +0000 (18:53 +0200)
committerMichael Tremer <michael.tremer@ipfire.org>
Tue, 22 Apr 2025 14:48:53 +0000 (16:48 +0200)
When importing a configuration, we will receive a new private key which
we cannot apply to the original interface. Therefore we need to create a
new one for each peer. RW peers will remain on wg0 which will always
exist.

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
html/cgi-bin/wireguard.cgi
src/initscripts/system/wireguard

index e4614724039dba159033a7dfb3ea3bc9ce21e166..a2724597428f046b323d8f0c1755f08078988443 100644 (file)
@@ -134,8 +134,8 @@ if ($cgiparams{"ACTION"} eq $Lang::tr{'save'}) {
        # Fetch type
        my $type = $Wireguard::peers{$key}[1];
 
-       my @remote_subnets = &Wireguard::decode_subnets($Wireguard::peers{$key}[6]);
-       my @local_subnets  = &Wireguard::decode_subnets($Wireguard::peers{$key}[8]);
+       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 = (
@@ -144,13 +144,15 @@ if ($cgiparams{"ACTION"} eq $Lang::tr{'save'}) {
                "TYPE"                          => $Wireguard::peers{$key}[1],
                "NAME"                          => $Wireguard::peers{$key}[2],
                "PUBLIC_KEY"            => $Wireguard::peers{$key}[3],
-               "ENDPOINT_ADDRESS"      => $Wireguard::peers{$key}[4],
-               "ENDPOINT_PORT"         => $Wireguard::peers{$key}[5],
+               "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}[7]),
+               "REMARKS"                       => &MIME::Base64::decode_base64($Wireguard::peers{$key}[9]),
                "LOCAL_SUBNETS"         => join(", ", @local_subnets),
-               "PSK"                           => $Wireguard::peers{$key}[9],
-               "KEEPALIVE"                     => $Wireguard::peers{$key}[10],
+               "PSK"                           => $Wireguard::peers{$key}[11],
+               "KEEPALIVE"                     => $Wireguard::peers{$key}[12],
        );
 
        # Jump to the editor
@@ -184,6 +186,13 @@ if ($cgiparams{"ACTION"} eq $Lang::tr{'save'}) {
                push(@errormessages, $Lang::tr{'wg invalid public key'});
        }
 
+       # Check private key
+       #if ($cgiparams{'PRIVATE_KEY'} eq '') {
+       #               # The private key may be empty
+       #} elsif (!&Wireguard::key_is_valid($cgiparams{'PRIVATE_KEY'})) {
+       #               push(@errormessages, $Lang::tr{'wg invalid private key'});
+       #}
+
        # Check PSK
        if ($cgiparams{'PSK'} eq '') {
                # The PSK may be empty
@@ -191,6 +200,11 @@ if ($cgiparams{"ACTION"} eq $Lang::tr{'save'}) {
                push(@errormessages, $Lang::tr{'wg invalid psk'});
        }
 
+       # Check port
+       #unless (&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
@@ -251,21 +265,25 @@ if ($cgiparams{"ACTION"} eq $Lang::tr{'save'}) {
                "net",
                # 2 = Name
                $cgiparams{"NAME"},
-               # 3 = Pubkey
+               # 3 = Public Key
                $cgiparams{"PUBLIC_KEY"},
-               # 4 = Endpoint Address
+               # 4 = Private Key
+               $cgiparams{"PRIVATE_KEY"},
+               # 5 = Port
+               $cgiparams{"PORT"},
+               # 6 = Endpoint Address
                $cgiparams{"ENDPOINT_ADDRESS"},
-               # 5 = Endpoint Port
+               # 7 = Endpoint Port
                $cgiparams{"ENDPOINT_PORT"},
-               # 6 = Remote Subnets
+               # 8 = Remote Subnets
                &Wireguard::encode_subnets(@remote_subnets),
-               # 7 = Remark
+               # 9 = Remark
                &Wireguard::encode_remarks($cgiparams{"REMARKS"}),
-               # 8 = Local Subnets
+               # 10 = Local Subnets
                &Wireguard::encode_subnets(@local_subnets),
-               # 9 = PSK
+               # 11 = PSK
                $cgiparams{"PSK"} || "",
-               # 10 = Keepalive
+               # 12 = Keepalive
                $cgiparams{"KEEPALIVE"} || 0,
        ];
 
@@ -348,8 +366,8 @@ if ($cgiparams{"ACTION"} eq $Lang::tr{'save'}) {
        # Fetch some configuration parts
        } else {
                $cgiparams{"PUBLIC_KEY"}     = $Wireguard::peers{$key}[3];
-               $cgiparams{'CLIENT_ADDRESS'} = $Wireguard::peers{$key}[6];
-               $cgiparams{"PSK"}            = $Wireguard::peers{$key}[9];
+               $cgiparams{'CLIENT_ADDRESS'} = $Wireguard::peers{$key}[8];
+               $cgiparams{"PSK"}            = $Wireguard::peers{$key}[11];
        }
 
        # Save the connection
@@ -360,21 +378,25 @@ if ($cgiparams{"ACTION"} eq $Lang::tr{'save'}) {
                "host",
                # 2 = Name
                $cgiparams{"NAME"},
-               # 3 = Pubkey
+               # 3 = Public Key
                $cgiparams{"PUBLIC_KEY"},
-               # 4 = Endpoint Address
+               # 4 = Private Key
                "",
-               # 5 = Endpoint Port
+               # 5 = Port
                "",
-               # 6 = Remote Subnets
+               # 6 = Endpoint Address
+               "",
+               # 7 = Endpoint Port
+               "",
+               # 8 = Remote Subnets
                $cgiparams{'CLIENT_ADDRESS'},
-               # 7 = Remark
+               # 9 = Remark
                &Wireguard::encode_remarks($cgiparams{"REMARKS"}),
-               # 8 = Local Subnets
+               # 10 = Local Subnets
                &Wireguard::encode_subnets(@local_subnets),
-               # 9 = PSK
+               # 11 = PSK
                $cgiparams{"PSK"},
-               # 10 = Keepalive
+               # 12 = Keepalive
                0,
        ];
 
@@ -550,10 +572,12 @@ END
                        my $type     = $Wireguard::peers{$key}[1];
                        my $name     = $Wireguard::peers{$key}[2];
                        my $pubkey   = $Wireguard::peers{$key}[3];
-                       my $endpoint = $Wireguard::peers{$key}[4];
-                       my $port     = $Wireguard::peers{$key}[5];
-                       my $routes   = $Wireguard::peers{$key}[6];
-                       my $remarks  = &Wireguard::decode_remarks($Wireguard::peers{$key}[7]);
+                       #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";
@@ -790,6 +814,8 @@ EDITNET:
                        <input type="hidden" name="ACTION" value="SAVE-PEER-NET">
                        <input type="hidden" name="KEY" value="$cgiparams{'KEY'}">
 
+                       <input type="hidden" name="PRIVATE_KEY" value="$cgiparams{'PRIVATE_KEY'}">
+
                        <table class="form">
                                <tr>
                                        <td>
@@ -1007,9 +1033,9 @@ sub show_peer_configuration($$) {
        my %peer = (
                "NAME"                          => $Wireguard::peers{$key}[2],
                "PUBLIC_KEY"            => $Wireguard::peers{$key}[3],
-               "CLIENT_ADDRESS"        => $Wireguard::peers{$key}[6],
-               "LOCAL_SUBNETS"         => &Wireguard::decode_subnets($Wireguard::peers{$key}[8]),
-               "PSK"                           => $Wireguard::peers{$key}[9],
+               "CLIENT_ADDRESS"        => $Wireguard::peers{$key}[8],
+               "LOCAL_SUBNETS"         => &Wireguard::decode_subnets($Wireguard::peers{$key}[10]),
+               "PSK"                           => $Wireguard::peers{$key}[11],
 
                # Other stuff
                "PRIVATE_KEY"           => $private_key,
index cb78d80abf3f9fbc3989f641c70a9d45d1811567..df50259f078ee58223c09f4713f8d008e39262ad 100644 (file)
 . ${rc_functions}
 . /etc/rc.d/init.d/networking/functions.network
 
-INTF="wg0"
-
 eval $(/usr/local/bin/readhash /var/ipfire/wireguard/settings)
 
-generate_config() {
-       echo "[Interface]"
-       echo "PrivateKey = ${PRIVATE_KEY}"
+interfaces() {
+       local id
+       local enabled
+       local type
+       local _rest
+
+       local IFS=','
+
+       # wg0 will always be created for roadwarrior
+       echo "wg0"
+
+       while read -r id enabled type _rest; do
+               # Skip peers that are not enabled
+               [ "${enabled}" = "on" ] || continue
+
+               # Skip anything that isn't a net-to-net connection
+               [ "${type}" = "net" ] || continue
+
+               echo "wg${id}"
+       done < /var/ipfire/wireguard/peers
+
+       return 0
+}
+
+interface_is_rw() {
+       local intf="${1}"
+
+       [ "${intf}" = "wg0" ]
+}
+
+setup_interface() {
+       local intf="${1}"
+
+       # Create the interface if it does not exist
+       if [ ! -d "/sys/class/net/${intf}" ]; then
+               ip link add "${intf}" type wireguard || return $?
+       fi
 
-       # Optionally set the port
-       if [ -n "${PORT}" ]; then
-               echo "ListenPort = ${PORT}"
+       # Set up the interface
+       ip link set "${intf}" up
+
+       # Set the MTU
+       if [ -n "${MTU}" ]; then
+               ip link set "${intf}" mtu "${MTU}" || return $?
        fi
 
+       # Load the configuration into the kernel
+       wg syncconf "${intf}" <(generate_config "${intf}") || return $?
+
+       return 0
+}
+
+cleanup_interfaces() {
+       local interfaces=( "$(interfaces)" )
+
+       local intf
+       for intf in /sys/class/net/wg[0-9]*; do
+               [ -d "${intf}" ] || continue
+
+               # Remove the path
+               intf="${intf##*/}"
+
+               local found=0
+               local i
+
+               for i in ${interfaces[@]}; do
+                       if [ "${intf}" = "${i}" ]; then
+                               found=1
+                               break
+                       fi
+               done
+
+               if [ "${found}" -eq 0 ]; then
+                       ip link del "${intf}"
+               fi
+       done
+
+       return 0
+}
+
+generate_config() {
+       local intf="${1}"
+
+       # Flush all previously set routes
+       ip route flush dev "${intf}"
+
        local IFS=','
 
        local id
@@ -43,8 +118,10 @@ generate_config() {
        local type
        local name
        local pubkey
-       local endpoint
+       local privkey
        local port
+       local endpoint_addr
+       local endpoint_port
        local remote_subnets
        local remarks
        local local_subnets
@@ -52,39 +129,83 @@ generate_config() {
        local keepalive
        local _rest
 
-       local local_subnet
-       local remote_subnet
+       # Handles the special case of the RW interface
+       if interface_is_rw "${intf}"; then
+               echo "[Interface]"
+               echo "PrivateKey = ${PRIVATE_KEY}"
 
-       # Flush firewall rules
-       iptables -F WGBLOCK
+               # Optionally set the port
+               if [ -n "${PORT}" ]; then
+                       echo "ListenPort = ${PORT}"
+               fi
 
-       # Flush all previously set routes
-       ip route flush dev "${INTF}"
+               # Add the client pool
+               if [ -n "${CLIENT_POOL}" ]; then
+                       ip route add "${CLIENT_POOL}" dev "${intf}"
+               fi
+
+               while read -r id enabled type name pubkey privkey port endpoint_addr endpoint_port \
+                               remote_subnets remarks local_subnets psk keepalive _rest; do
+                       # Skip peers that are not hosts or not enabled
+                       [ "${type}" = "host" ] || continue
+                       [ "${enabled}" = "on" ] || continue
+
+                       echo "[Peer]"
+                       echo "PublicKey = ${pubkey}"
+
+                       # Set PSK (if set)
+                       if [ -n "${psk}" ]; then
+                               echo "PresharedKey = ${psk}"
+                       fi
+
+                       # Set routes
+                       if [ -n "${remote_subnets}" ]; then
+                               echo "AllowedIPs = ${remote_subnets//|/, }"
+                       fi
+
+                       echo # newline
+               done < /var/ipfire/wireguard/peers
 
-       # Add the client pool
-       if [ -n "${CLIENT_POOL}" ]; then
-               ip route add "${CLIENT_POOL}" dev "${INTF}"
+               return 0
        fi
 
-       local args
-       local src
+       local local_subnet
+       local remote_subnet
+
+       while read -r id enabled type name pubkey privkey port endpoint_addr endpoint_port \
+                       remote_subnets remarks local_subnets psk keepalive _rest; do
+               # Check for the matching connection
+               [ "${type}" = "net" ] || continue
+               [ "${intf}" = "wg${id}" ] || continue
 
-       while read -r id enabled type name pubkey endpoint port remote_subnets \
-                       remarks local_subnets psk keepalive _rest; do
                # Skip peers that are not enabled
                [ "${enabled}" = "on" ] || continue
 
+               echo "[Interface]"
+
+               if [ -n "${privkey}" ]; then
+                       echo "PrivateKey = ${privkey}"
+               fi
+
+               # Optionally set the port
+               if [ -n "${port}" ]; then
+                       echo "ListenPort = ${port}"
+
+                       # Open the port
+                       iptables -A WGINPUT -p udp --dport "${port}" -j ACCEPT
+               fi
+
                echo "[Peer]"
                echo "PublicKey = ${pubkey}"
 
                # Set PSK (if set)
                if [ -n "${psk}" ]; then
-                       echo "PresharedKey= ${psk}"
+                       echo "PresharedKey = ${psk}"
                fi
 
                # Set endpoint
-               if [ -n "${endpoint}" ]; then
-                       echo "Endpoint = ${endpoint}${port:+:}${port}"
+               if [ -n "${endpoint_addr}" ]; then
+                       echo "Endpoint = ${endpoint_addr}${endpoint_port:+:}${endpoint_port}"
                fi
 
                # Set routes
@@ -92,25 +213,23 @@ generate_config() {
                        echo "AllowedIPs = ${remote_subnets//|/, }"
 
                        # Apply the routes
-                       if [ "${type}" = "net" ]; then
-                               local_subnets=( "${local_subnets//|/,}" )
+                       local_subnets=( "${local_subnets//|/,}" )
 
-                               # Find an IP address of the firewall that is inside the routed subnet
-                               src="$(ipfire_address_in_networks "${local_subnets[@]}")"
+                       # Find an IP address of the firewall that is inside the routed subnet
+                       local src="$(ipfire_address_in_networks "${local_subnets[@]}")"
 
-                               for remote_subnet in ${remote_subnets//|/,}; do
-                                       args=(
-                                               "${remote_subnet}" "dev" "${INTF}"
-                                       )
+                       for remote_subnet in ${remote_subnets//|/,}; do
+                               local args=(
+                                       "${remote_subnet}" "dev" "${intf}"
+                               )
 
-                                       # Add the preferred source if we found one
-                                       if [ -n "${src}" ]; then
-                                               args+=( "src" "${src}" )
-                                       fi
+                               # Add the preferred source if we found one
+                               if [ -n "${src}" ]; then
+                                       args+=( "src" "${src}" )
+                               fi
 
-                                       ip route add "${args[@]}"
-                               done
-                       fi
+                               ip route add "${args[@]}"
+                       done
                fi
 
                # Set keepalive
@@ -121,14 +240,14 @@ generate_config() {
                # Set blocking rules
                for local_subnet in ${local_subnets//|/ }; do
                        for remote_subnet in ${remote_subnets//|/ }; do
-                               iptables -A WGBLOCK \
+                               iptables -I WGBLOCK \
                                        -s "${remote_subnet}" -d "${local_subnet}" -j RETURN
                        done
                done
-       done < /var/ipfire/wireguard/peers
 
-       # Block all other traffic
-       iptables -A WGBLOCK -j REJECT --reject-with icmp-admin-prohibited
+               # There will only be one match, so we can break as soon we get here
+               break
+       done < /var/ipfire/wireguard/peers
 }
 
 reload_firewall() {
@@ -138,38 +257,45 @@ reload_firewall() {
        if [ "${ENABLED}" = "on" ]; then
                iptables -A WGINPUT -p udp --dport "${PORT}" -j ACCEPT
        fi
+
+       iptables -F WGBLOCK
+
+       # Block all other traffic
+       iptables -A WGBLOCK -j REJECT --reject-with icmp-admin-prohibited
 }
 
 wg_start() {
-       # Reload the firewall
-       reload_firewall
+       local failed=0
+       local intf
 
-       # Create the interface if it does not exist
-       if [ ! -d "/sys/class/net/${INTF}" ]; then
-               ip link add "${INTF}" type wireguard || return $?
-       fi
+       # Find all interfaces
+       local interfaces=( "$(interfaces)" )
 
-       # Set up the interface
-       ip link set "${INTF}" up
+       # Shut down any unwanted interfaces
+       cleanup_interfaces
 
-       # Set the MTU
-       if [ -n "${MTU}" ]; then
-               ip link set "${INTF}" mtu "${MTU}" || return $?
-       fi
+       # Reload the firewall
+       reload_firewall
 
-       # Load the configuration into the kernel
-       wg syncconf "${INTF}" <(generate_config) || return $?
+       # Setup all interfaces
+       for intf in ${interfaces[@]}; do
+               setup_interface "${intf}" || failed=1
+       done
 
-       return 0
+       return ${failed}
 }
 
 wg_stop() {
+       local intf
+
        # Reload the firewall
        ENABLED=off reload_firewall
 
-       if [ -d "/sys/class/net/${INTF}" ]; then
-               ip link del "${INTF}" || return $?
-       fi
+       for intf in /sys/class/net/wg[0-9]*; do
+               ip link del "${intf##*/}"
+       done
+
+       return 0
 }
 
 case "${1}" in