networkd/wireguard: support network.wireguard.* credentials

Closes #26702
This commit is contained in:
Mike Yuan 2023-12-27 21:38:32 +08:00
parent d7d6195953
commit fa724cd52c
No known key found for this signature in database
GPG key ID: 417471C0A40F58B3
11 changed files with 173 additions and 46 deletions

View file

@ -1891,13 +1891,22 @@
<varlistentry> <varlistentry>
<term><varname>PrivateKey=</varname></term> <term><varname>PrivateKey=</varname></term>
<listitem> <listitem>
<para>The Base64 encoded private key for the interface. It can be <para>The Base64 encoded private key for the interface. It can be generated using
generated using the <command>wg genkey</command> command the <command>wg genkey</command> command
(see <citerefentry project="wireguard"><refentrytitle>wg</refentrytitle><manvolnum>8</manvolnum></citerefentry>). (see <citerefentry project="wireguard"><refentrytitle>wg</refentrytitle><manvolnum>8</manvolnum></citerefentry>).
This option or <varname>PrivateKeyFile=</varname> is mandatory to use WireGuard. Specially, if the specified key is prefixed with <literal>@</literal>, it is interpreted as
Note that because this information is secret, you may want to set the name of the credential from which the actual key shall be read. <command>systemd-networkd.service</command>
the permissions of the .netdev file to be owned by <literal>root:systemd-network</literal> automatically imports credentials matching <literal>network.wireguard.*</literal>. For more details
with a <literal>0640</literal> file mode.</para> on credentials, refer to
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
A private key is mandatory to use WireGuard. If not set, the credential
<literal>network.wireguard.private.<replaceable>netdev</replaceable></literal> is used if exists.
I.e. for <filename>50-foobar.netdev</filename>, <literal>network.wireguard.private.50-foobar</literal>
is tried.</para>
<para>Note that because this information is secret, it's strongly recommended to use an (encrypted)
credential. Alternatively, you may want to set the permissions of the .netdev file to be owned
by <literal>root:systemd-network</literal> with a <literal>0640</literal> file mode.</para>
<xi:include href="version-info.xml" xpointer="v237"/> <xi:include href="version-info.xml" xpointer="v237"/>
</listitem> </listitem>
@ -1976,9 +1985,9 @@
<listitem> <listitem>
<para>Sets a Base64 encoded public key calculated by <command>wg pubkey</command> <para>Sets a Base64 encoded public key calculated by <command>wg pubkey</command>
(see <citerefentry project="wireguard"><refentrytitle>wg</refentrytitle><manvolnum>8</manvolnum></citerefentry>) (see <citerefentry project="wireguard"><refentrytitle>wg</refentrytitle><manvolnum>8</manvolnum></citerefentry>)
from a private key, and usually transmitted out of band to the from a private key, and usually transmitted out of band to the author of the configuration file.
author of the configuration file. This option is mandatory for this This option honors the <literal>@</literal> prefix in the same way as the <option>PrivateKey=</option>
section.</para> setting of the <option>[WireGuard]</option> section. This option is mandatory for this section.</para>
<xi:include href="version-info.xml" xpointer="v237"/> <xi:include href="version-info.xml" xpointer="v237"/>
</listitem> </listitem>
@ -1986,14 +1995,15 @@
<varlistentry> <varlistentry>
<term><varname>PresharedKey=</varname></term> <term><varname>PresharedKey=</varname></term>
<listitem> <listitem>
<para>Optional preshared key for the interface. It can be generated <para>Optional preshared key for the interface. It can be generated by the <command>wg genpsk</command>
by the <command>wg genpsk</command> command. This option adds an command. This option adds an additional layer of symmetric-key cryptography to be mixed into the
additional layer of symmetric-key cryptography to be mixed into the already existing public-key cryptography, for post-quantum resistance.
already existing public-key cryptography, for post-quantum This option honors the <literal>@</literal> prefix in the same way as the <option>PrivateKey=</option>
resistance. setting of the <option>[WireGuard]</option> section.</para>
Note that because this information is secret, you may want to set
the permissions of the .netdev file to be owned by <literal>root:systemd-network</literal> <para>Note that because this information is secret, it's strongly recommended to use an (encrypted)
with a <literal>0640</literal> file mode.</para> credential. Alternatively, you may want to set the permissions of the .netdev file to be owned
by <literal>root:systemd-network</literal> with a <literal>0640</literal> file mode.</para>
<xi:include href="version-info.xml" xpointer="v237"/> <xi:include href="version-info.xml" xpointer="v237"/>
</listitem> </listitem>
@ -2034,13 +2044,15 @@
<varlistentry> <varlistentry>
<term><varname>Endpoint=</varname></term> <term><varname>Endpoint=</varname></term>
<listitem> <listitem>
<para>Sets an endpoint IP address or hostname, followed by a colon, and then <para>Sets an endpoint IP address or hostname, followed by a colon, and then a port number.
a port number. IPv6 address must be in the square brackets. For example, IPv6 address must be in the square brackets. For example, <literal>111.222.333.444:51820</literal>
<literal>111.222.333.444:51820</literal> for IPv4 and <literal>[1111:2222::3333]:51820</literal> for IPv4 and <literal>[1111:2222::3333]:51820</literal> for IPv6 address. This endpoint will be
for IPv6 address. This endpoint will be updated automatically once to updated automatically once to the most recent source IP address and port of correctly
the most recent source IP address and port of correctly
authenticated packets from the peer at configuration time.</para> authenticated packets from the peer at configuration time.</para>
<para>This option honors the <literal>@</literal> prefix in the same way as the <option>PrivateKey=</option>
setting of the <option>[WireGuard]</option> section.</para>
<xi:include href="version-info.xml" xpointer="v237"/> <xi:include href="version-info.xml" xpointer="v237"/>
</listitem> </listitem>
</varlistentry> </varlistentry>

View file

@ -155,7 +155,21 @@
<para>Note that the resulting files are created world-readable, it's hence recommended to not include <para>Note that the resulting files are created world-readable, it's hence recommended to not include
secrets in these credentials, but supply them via separate credentials directly to secrets in these credentials, but supply them via separate credentials directly to
<filename>systemd-networkd.service</filename>.</para> <filename>systemd-networkd.service</filename>, e.g. <varname>network.wireguard.*</varname>
as described below.</para>
<xi:include href="version-info.xml" xpointer="v256"/>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>network.wireguard.*</varname></term>
<listitem>
<para>Configures secrets for WireGuard netdevs. Read by
<citerefentry><refentrytitle>systemd-networkd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
For more information, refer to the <option>[WireGuard]</option> section of
<citerefentry><refentrytitle>systemd.netdev</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
</para>
<xi:include href="version-info.xml" xpointer="v256"/> <xi:include href="version-info.xml" xpointer="v256"/>
</listitem> </listitem>

View file

@ -12,6 +12,7 @@
#include "sd-resolve.h" #include "sd-resolve.h"
#include "alloc-util.h" #include "alloc-util.h"
#include "creds-util.h"
#include "dns-domain.h" #include "dns-domain.h"
#include "event-util.h" #include "event-util.h"
#include "fd-util.h" #include "fd-util.h"
@ -25,6 +26,7 @@
#include "networkd-util.h" #include "networkd-util.h"
#include "parse-helpers.h" #include "parse-helpers.h"
#include "parse-util.h" #include "parse-util.h"
#include "path-util.h"
#include "random-util.h" #include "random-util.h"
#include "resolve-private.h" #include "resolve-private.h"
#include "string-util.h" #include "string-util.h"
@ -480,6 +482,8 @@ static int wireguard_decode_key_and_warn(
const char *lvalue) { const char *lvalue) {
_cleanup_(erase_and_freep) void *key = NULL; _cleanup_(erase_and_freep) void *key = NULL;
_cleanup_(erase_and_freep) char *cred = NULL;
const char *cred_name;
size_t len; size_t len;
int r; int r;
@ -493,10 +497,22 @@ static int wireguard_decode_key_and_warn(
return 0; return 0;
} }
if (!streq(lvalue, "PublicKey")) cred_name = startswith(rvalue, "@");
if (cred_name) {
r = read_credential(cred_name, (void**) &cred, /* ret_size = */ NULL);
if (r == -ENOMEM)
return log_oom();
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to read credential for wireguard key (%s=), ignoring assignment: %m",
lvalue);
return 0;
}
} else if (!streq(lvalue, "PublicKey"))
(void) warn_file_is_world_accessible(filename, NULL, unit, line); (void) warn_file_is_world_accessible(filename, NULL, unit, line);
r = unbase64mem_full(rvalue, strlen(rvalue), true, &key, &len); r = unbase64mem_full(cred ?: rvalue, SIZE_MAX, /* secure = */ true, &key, &len);
if (r == -ENOMEM) if (r == -ENOMEM)
return log_oom(); return log_oom();
if (r < 0) { if (r < 0) {
@ -721,23 +737,39 @@ int config_parse_wireguard_endpoint(
void *data, void *data,
void *userdata) { void *userdata) {
assert(filename);
assert(rvalue);
assert(userdata);
Wireguard *w = WIREGUARD(userdata); Wireguard *w = WIREGUARD(userdata);
_cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL;
_cleanup_free_ char *host = NULL; _cleanup_free_ char *cred = NULL;
union in_addr_union addr; const char *cred_name, *endpoint;
const char *p;
uint16_t port; uint16_t port;
int family, r; int r;
assert(filename);
assert(rvalue);
r = wireguard_peer_new_static(w, filename, section_line, &peer); r = wireguard_peer_new_static(w, filename, section_line, &peer);
if (r < 0) if (r < 0)
return log_oom(); return log_oom();
r = in_addr_port_ifindex_name_from_string_auto(rvalue, &family, &addr, &port, NULL, NULL); cred_name = startswith(rvalue, "@");
if (cred_name) {
r = read_credential(cred_name, (void**) &cred, /* ret_size = */ NULL);
if (r == -ENOMEM)
return log_oom();
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to read credential for wireguard endpoint, ignoring assignment: %m");
return 0;
}
endpoint = strstrip(cred);
} else
endpoint = rvalue;
union in_addr_union addr;
int family;
r = in_addr_port_ifindex_name_from_string_auto(endpoint, &family, &addr, &port, NULL, NULL);
if (r >= 0) { if (r >= 0) {
if (family == AF_INET) if (family == AF_INET)
peer->endpoint.in = (struct sockaddr_in) { peer->endpoint.in = (struct sockaddr_in) {
@ -761,17 +793,23 @@ int config_parse_wireguard_endpoint(
return 0; return 0;
} }
p = strrchr(rvalue, ':'); _cleanup_free_ char *host = NULL;
const char *p;
p = strrchr(endpoint, ':');
if (!p) { if (!p) {
log_syntax(unit, LOG_WARNING, filename, line, 0, log_syntax(unit, LOG_WARNING, filename, line, 0,
"Unable to find port of endpoint, ignoring assignment: %s", "Unable to find port of endpoint, ignoring assignment: %s",
rvalue); rvalue); /* We log the original assignment instead of resolved credential here,
as the latter might be previously encrypted and we'd expose them in
unprotected logs otherwise. */
return 0; return 0;
} }
host = strndup(rvalue, p - rvalue); host = strndup(endpoint, p - endpoint);
if (!host) if (!host)
return log_oom(); return log_oom();
p++;
if (!dns_name_is_valid(host)) { if (!dns_name_is_valid(host)) {
log_syntax(unit, LOG_WARNING, filename, line, 0, log_syntax(unit, LOG_WARNING, filename, line, 0,
@ -780,7 +818,6 @@ int config_parse_wireguard_endpoint(
return 0; return 0;
} }
p++;
r = parse_ip_port(p, &port); r = parse_ip_port(p, &port);
if (r < 0) { if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r, log_syntax(unit, LOG_WARNING, filename, line, r,
@ -1078,6 +1115,55 @@ static int wireguard_peer_verify(WireguardPeer *peer) {
return 0; return 0;
} }
static int wireguard_read_default_key_cred(NetDev *netdev, const char *filename) {
Wireguard *w = WIREGUARD(netdev);
_cleanup_free_ char *config_name = NULL;
int r;
assert(filename);
r = path_extract_filename(filename, &config_name);
if (r < 0)
return log_netdev_error_errno(netdev, r,
"%s: Failed to extract config name, ignoring network device: %m",
filename);
char *p = endswith(config_name, ".netdev");
if (!p)
/* Fuzzer run? Then we just ignore this device. */
return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
"%s: Invalid netdev config name, refusing default key lookup.",
filename);
*p = '\0';
_cleanup_(erase_and_freep) char *cred = NULL;
r = read_credential(strjoina("network.wireguard.private.", config_name), (void**) &cred, /* ret_size = */ NULL);
if (r < 0)
return log_netdev_error_errno(netdev, r,
"%s: No private key specified and default key isn't available, "
"ignoring network device: %m",
filename);
_cleanup_(erase_and_freep) void *key = NULL;
size_t len;
r = unbase64mem_full(cred, SIZE_MAX, /* secure = */ true, &key, &len);
if (r < 0)
return log_netdev_error_errno(netdev, r,
"%s: No private key specified and default key cannot be parsed, "
"ignoring network device: %m",
filename);
if (len != WG_KEY_LEN)
return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
"%s: No private key specified and default key is invalid. "
"Ignoring network device.",
filename);
memcpy(w->private_key, key, WG_KEY_LEN);
return 0;
}
static int wireguard_verify(NetDev *netdev, const char *filename) { static int wireguard_verify(NetDev *netdev, const char *filename) {
Wireguard *w = WIREGUARD(netdev); Wireguard *w = WIREGUARD(netdev);
int r; int r;
@ -1088,10 +1174,11 @@ static int wireguard_verify(NetDev *netdev, const char *filename) {
"Failed to read private key from %s. Ignoring network device.", "Failed to read private key from %s. Ignoring network device.",
w->private_key_file); w->private_key_file);
if (eqzero(w->private_key)) if (eqzero(w->private_key)) {
return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), r = wireguard_read_default_key_cred(netdev, filename);
"%s: Missing PrivateKey= or PrivateKeyFile=, " if (r < 0)
"Ignoring network device.", filename); return r;
}
LIST_FOREACH(peers, peer, w->peers) { LIST_FOREACH(peers, peer, w->peers) {
if (wireguard_peer_verify(peer) < 0) { if (wireguard_peer_verify(peer) < 0) {

View file

@ -0,0 +1 @@
192.168.27.3:51820

View file

@ -0,0 +1 @@
EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong=

View file

@ -4,6 +4,6 @@ Name=wg97
Kind=wireguard Kind=wireguard
[WireGuard] [WireGuard]
PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong= #PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong=
ListenPort=51821 ListenPort=51821
FwMark=1235 FwMark=1235

View file

@ -0,0 +1 @@
6Fsg8XN0DE6aPQgAX4r2oazEYJOGqyHUz3QRH/jCB+I=

View file

@ -13,8 +13,8 @@ RouteMetric=456
[WireGuardPeer] [WireGuardPeer]
PublicKey=RDf+LSpeEre7YEIKaxg+wbpsNV7du+ktR99uBEtIiCA= PublicKey=RDf+LSpeEre7YEIKaxg+wbpsNV7du+ktR99uBEtIiCA=
AllowedIPs=fd31:bf08:57cb::/48,192.168.26.3/24 AllowedIPs=fd31:bf08:57cb::/48,192.168.26.3/24
#Endpoint=wireguard.example.com:51820 #Endpoint=192.168.27.3:51820
Endpoint=192.168.27.3:51820 Endpoint=@network.wireguard.peer0.endpoint
PresharedKey=IIWIV17wutHv7t4cR6pOT91z6NSz/T8Arh0yaywhw3M= PresharedKey=IIWIV17wutHv7t4cR6pOT91z6NSz/T8Arh0yaywhw3M=
PersistentKeepalive=20 PersistentKeepalive=20
RouteTable=1234 RouteTable=1234

View file

@ -1,5 +1,5 @@
[WireGuardPeer] [WireGuardPeer]
PublicKey=9uioxkGzjvGjkse3V35I9AhorWfIjBcrf3UPMS0bw2c= PublicKey=9uioxkGzjvGjkse3V35I9AhorWfIjBcrf3UPMS0bw2c=
PresharedKey=6Fsg8XN0DE6aPQgAX4r2oazEYJOGqyHUz3QRH/jCB+I= PresharedKey=@network.wireguard.peer2.psk
AllowedIPs=192.168.124.3 AllowedIPs=192.168.124.3

View file

@ -27,6 +27,7 @@ network_unit_dir = '/run/systemd/network'
networkd_conf_dropin_dir = '/run/systemd/networkd.conf.d' networkd_conf_dropin_dir = '/run/systemd/networkd.conf.d'
networkd_ci_temp_dir = '/run/networkd-ci' networkd_ci_temp_dir = '/run/networkd-ci'
udev_rules_dir = '/run/udev/rules.d' udev_rules_dir = '/run/udev/rules.d'
credstore_dir = '/run/credstore'
dnsmasq_pid_file = '/run/networkd-ci/test-dnsmasq.pid' dnsmasq_pid_file = '/run/networkd-ci/test-dnsmasq.pid'
dnsmasq_log_file = '/run/networkd-ci/test-dnsmasq.log' dnsmasq_log_file = '/run/networkd-ci/test-dnsmasq.log'
@ -298,6 +299,11 @@ def copy_network_unit(*units, copy_dropins=True):
if has_link: if has_link:
udev_reload() udev_reload()
def copy_credential(src, target):
mkdir_p(credstore_dir)
cp(os.path.join(networkd_ci_temp_dir, src),
os.path.join(credstore_dir, target))
def remove_network_unit(*units): def remove_network_unit(*units):
""" """
Remove previously copied unit files from the testbed. Remove previously copied unit files from the testbed.
@ -1784,6 +1790,10 @@ class NetworkdNetDevTests(unittest.TestCase, Utilities):
@expectedFailureIfModuleIsNotAvailable('wireguard') @expectedFailureIfModuleIsNotAvailable('wireguard')
def test_wireguard(self): def test_wireguard(self):
copy_credential('25-wireguard-endpoint-peer0-cred.txt', 'network.wireguard.peer0.endpoint')
copy_credential('25-wireguard-preshared-key-peer2-cred.txt', 'network.wireguard.peer2.psk')
copy_credential('25-wireguard-no-peer-private-key-cred.txt', 'network.wireguard.private.25-wireguard-no-peer')
copy_network_unit('25-wireguard.netdev', '25-wireguard.network', copy_network_unit('25-wireguard.netdev', '25-wireguard.network',
'25-wireguard-23-peers.netdev', '25-wireguard-23-peers.network', '25-wireguard-23-peers.netdev', '25-wireguard-23-peers.network',
'25-wireguard-preshared-key.txt', '25-wireguard-private-key.txt', '25-wireguard-preshared-key.txt', '25-wireguard-private-key.txt',

View file

@ -50,6 +50,7 @@ SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service SystemCallFilter=@system-service
Type=notify-reload Type=notify-reload
User=systemd-network User=systemd-network
ImportCredential=network.wireguard.*
{{SERVICE_WATCHDOG}} {{SERVICE_WATCHDOG}}
[Install] [Install]