diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig index 0fad94538..733aa7226 100644 --- a/board/aarch64/linux_defconfig +++ b/board/aarch64/linux_defconfig @@ -244,6 +244,7 @@ CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y CONFIG_NETDEVICES=y CONFIG_BONDING=m CONFIG_DUMMY=m +CONFIG_WIREGUARD=m CONFIG_MACVLAN=m CONFIG_MACVTAP=m CONFIG_IPVLAN=m diff --git a/board/x86_64/linux_defconfig b/board/x86_64/linux_defconfig index 957e9f7b2..05932e6e3 100644 --- a/board/x86_64/linux_defconfig +++ b/board/x86_64/linux_defconfig @@ -191,6 +191,7 @@ CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y CONFIG_NETDEVICES=y CONFIG_BONDING=m CONFIG_DUMMY=m +CONFIG_WIREGUARD=m CONFIG_MACVLAN=m CONFIG_MACVTAP=m CONFIG_IPVLAN=m diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index 314037a46..9b0f71b75 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -102,6 +102,7 @@ BR2_PACKAGE_TCPDUMP=y BR2_PACKAGE_TRACEROUTE=y BR2_PACKAGE_ULOGD=y BR2_PACKAGE_WHOIS=y +BR2_PACKAGE_WIREGUARD_TOOLS=y BR2_PACKAGE_BASH_COMPLETION=y BR2_PACKAGE_NEOFETCH=y BR2_PACKAGE_SUDO=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index 3c35501a2..2cdf59d16 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -85,6 +85,7 @@ BR2_PACKAGE_OPENSSH=y BR2_PACKAGE_SOCAT=y BR2_PACKAGE_TCPDUMP=y BR2_PACKAGE_WHOIS=y +BR2_PACKAGE_WIREGUARD_TOOLS=y BR2_PACKAGE_BASH_COMPLETION=y BR2_PACKAGE_SUDO=y BR2_PACKAGE_KMOD_TOOLS=y diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig index 6bcef2b22..adbbfcf98 100644 --- a/configs/x86_64_defconfig +++ b/configs/x86_64_defconfig @@ -98,6 +98,7 @@ BR2_PACKAGE_TCPDUMP=y BR2_PACKAGE_TRACEROUTE=y BR2_PACKAGE_ULOGD=y BR2_PACKAGE_WHOIS=y +BR2_PACKAGE_WIREGUARD_TOOLS=y BR2_PACKAGE_BASH_COMPLETION=y BR2_PACKAGE_NEOFETCH=y BR2_PACKAGE_SUDO=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index 7dc46d3a0..b195716eb 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -80,6 +80,7 @@ BR2_PACKAGE_OPENSSH=y BR2_PACKAGE_SOCAT=y BR2_PACKAGE_TCPDUMP=y BR2_PACKAGE_WHOIS=y +BR2_PACKAGE_WIREGUARD_TOOLS=y BR2_PACKAGE_BASH_COMPLETION=y BR2_PACKAGE_SUDO=y BR2_PACKAGE_GETENT=y diff --git a/doc/tunnels.md b/doc/tunnels.md index 4240c4795..173119007 100644 --- a/doc/tunnels.md +++ b/doc/tunnels.md @@ -210,3 +210,334 @@ non-standard VXLAN ports. > [!NOTE] > VXLAN tunnels also support the `ttl` and `tos` settings described in > the [Advanced Tunnel Settings](#advanced-tunnel-settings) section above. + +## WireGuard VPN + +WireGuard is a modern, high-performance VPN protocol that uses state-of-the-art +cryptography. It is significantly simpler and faster than traditional VPN +solutions like IPsec or OpenVPN, while maintaining strong security guarantees. + +Key features of WireGuard: + +- **Simple Configuration:** Minimal settings required compared to IPsec +- **High Performance:** Runs in kernel space with efficient cryptography +- **Strong Cryptography:** Uses Curve25519, ChaCha20, Poly1305, and BLAKE2 +- **Roaming Support:** Seamlessly handles endpoint IP address changes +- **Dual-Stack:** Supports IPv4 and IPv6 for both tunnel endpoints and traffic + +> [!TIP] +> If you name your WireGuard interface `wgN`, where `N` is a number, the +> CLI infers the interface type automatically. + +### Key Management + +WireGuard uses public-key cryptography similar to SSH. Each WireGuard interface +requires a private key, and each peer is identified by its public key. + +**Generate a WireGuard key pair using the `wg` command:** + +```bash +admin@example:~$ wg genkey | tee privatekey | wg pubkey > publickey +admin@example:~$ cat privatekey +aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP= +admin@example:~$ cat publickey +bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP= +``` + +This generates a private key, saves it to `privatekey`, derives the public key, +and saves it to `publickey`. + +**Import the private key into the keystore:** + +``` +admin@example:/> configure +admin@example:/config/> edit keystore asymmetric-key wg-site-a +admin@example:/config/keystore/asymmetric-key/wg-site-a/> set public-key-format x25519-public-key-format +admin@example:/config/keystore/asymmetric-key/wg-site-a/> set private-key-format x25519-private-key-format +admin@example:/config/keystore/asymmetric-key/wg-site-a/> set public-key bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP= +admin@example:/config/keystore/asymmetric-key/wg-site-a/> set private-key aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP= +admin@example:/config/keystore/asymmetric-key/wg-site-a/> leave +admin@example:/> +``` + +**Import peer public keys into the truststore:** + +``` +admin@example:/> configure +admin@example:/config/> edit truststore public-key-bag wg-peers public-key peer-b +admin@example:/config/truststore/…/peer-b/> set public-key-format x25519-public-key-format +admin@example:/config/truststore/…/peer-b/> set public-key PEER_PUBLIC_KEY_HERE +admin@example:/config/truststore/…/peer-b/> leave +admin@example:/> +``` + +> [!IMPORTANT] +> Keep private keys secure! Never share your private key. Only exchange +> public keys with peers. Delete the `privatekey` file after importing it +> into the keystore. + +### Point-to-Point Configuration + +A basic WireGuard tunnel between two sites: + +**Site A configuration:** + +``` +admin@siteA:/> configure +admin@siteA:/config/> edit interface wg0 +admin@siteA:/config/interface/wg0/> set wireguard listen-port 51820 +admin@siteA:/config/interface/wg0/> set wireguard private-key wg-site-a +admin@siteA:/config/interface/wg0/> set ipv4 address 10.0.0.1 prefix-length 24 +admin@siteA:/config/interface/wg0/> edit wireguard peer wg-peers peer-b +admin@siteA:/config/interface/wg0/wireguard/peer/…/> set endpoint 203.0.113.2 +admin@siteA:/config/interface/wg0/wireguard/peer/…/> set endpoint-port 51820 +admin@siteA:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 10.0.0.2/32 +admin@siteA:/config/interface/wg0/wireguard/peer/…/> set persistent-keepalive 25 +admin@siteA:/config/interface/wg0/wireguard/peer/…/> leave +admin@siteA:/> +``` + +**Site B configuration:** + +``` +admin@siteB:/> configure +admin@siteB:/config/> edit interface wg0 +admin@siteB:/config/interface/wg0/> set wireguard listen-port 51820 +admin@siteB:/config/interface/wg0/> set wireguard private-key wg-site-b +admin@siteB:/config/interface/wg0/> set ipv4 address 10.0.0.2 prefix-length 24 +admin@siteB:/config/interface/wg0/> edit wireguard peer wg-peers peer-a +admin@siteB:/config/interface/wg0/wireguard/peer/…/> set endpoint 203.0.113.1 +admin@siteB:/config/interface/wg0/wireguard/peer/…/> set endpoint-port 51820 +admin@siteB:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 10.0.0.1/32 +admin@siteB:/config/interface/wg0/wireguard/peer/…/> set persistent-keepalive 25 +admin@siteB:/config/interface/wg0/wireguard/peer/…/> leave +admin@siteB:/> +``` + +This creates an encrypted tunnel with Site A at 10.0.0.1 and Site B at 10.0.0.2. + +### Understanding Allowed IPs + +The `allowed-ips` setting in WireGuard serves two critical purposes: + +1. **Ingress Filtering:** Only packets with source IPs in the allowed list + are accepted from the peer +2. **Cryptokey Routing:** Determines which peer receives outbound packets + for a given destination + +Think of `allowed-ips` as a combination of firewall rules and routing table. + +For a simple point-to-point tunnel, you typically allow only the peer's +tunnel IP address (e.g., `10.0.0.2/32`). For site-to-site VPNs connecting +entire networks, include the remote network prefixes: + +``` +admin@siteA:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 10.0.0.2/32 +admin@siteA:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 192.168.2.0/24 +``` + +This allows traffic to/from the peer at 10.0.0.2 and routes traffic destined +for 192.168.2.0/24 through this peer. + +> [!NOTE] +> When routing traffic to networks behind WireGuard peers, you also need +> to configure static routes pointing to the WireGuard interface. See +> [Static Routes](networking.md#static-routes) for more information. + +### Hub-and-Spoke Topology + +WireGuard excels at hub-and-spoke (star) topologies where multiple remote +sites connect to a central hub. + +**Hub configuration:** + +``` +admin@hub:/> configure +admin@hub:/config/> edit interface wg0 +admin@hub:/config/interface/wg0/> set wireguard listen-port 51820 +admin@hub:/config/interface/wg0/> set wireguard private-key wg-hub +admin@hub:/config/interface/wg0/> set ipv4 address 10.0.0.1 prefix-length 24 +admin@hub:/config/interface/wg0/> end + +# Spoke 1 +admin@hub:/config/> edit interface wg0 wireguard peer wg-peers spoke1 +admin@hub:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 10.0.0.2/32 +admin@hub:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 192.168.1.0/24 +admin@hub:/config/interface/wg0/wireguard/peer/…/> end + +# Spoke 2 +admin@hub:/config/> edit interface wg0 wireguard peer wg-peers spoke2 +admin@hub:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 10.0.0.3/32 +admin@hub:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 192.168.2.0/24 +admin@hub:/config/interface/wg0/wireguard/peer/…/> leave +admin@hub:/> + +# Add routes for spoke networks +admin@hub:/> configure +admin@hub:/config/> edit routing control-plane-protocol static name default +admin@hub:/config/routing/…/static/> set ipv4 route 192.168.1.0/24 wg0 +admin@hub:/config/routing/…/static/> set ipv4 route 192.168.2.0/24 wg0 +admin@hub:/config/routing/…/static/> leave +admin@hub:/> +``` + +**Spoke 1 configuration:** + +``` +admin@spoke1:/> configure +admin@spoke1:/config/> edit interface wg0 +admin@spoke1:/config/interface/wg0/> set wireguard listen-port 51820 +admin@spoke1:/config/interface/wg0/> set wireguard private-key wg-spoke1 +admin@spoke1:/config/interface/wg0/> set ipv4 address 10.0.0.2 prefix-length 24 +admin@spoke1:/config/interface/wg0/> edit wireguard peer wg-peers hub +admin@spoke1:/config/interface/wg0/wireguard/peer/…/> set endpoint 203.0.113.1 +admin@spoke1:/config/interface/wg0/wireguard/peer/…/> set endpoint-port 51820 +admin@spoke1:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 10.0.0.1/32 +admin@spoke1:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 10.0.0.3/32 +admin@spoke1:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 192.168.0.0/24 +admin@spoke1:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 192.168.2.0/24 +admin@spoke1:/config/interface/wg0/wireguard/peer/…/> set persistent-keepalive 25 +admin@spoke1:/config/interface/wg0/wireguard/peer/…/> end +admin@spoke1:/config/> edit routing control-plane-protocol static name default +admin@spoke1:/config/routing/…/static/> set ipv4 route 192.168.0.0/24 wg0 +admin@spoke1:/config/routing/…/static/> set ipv4 route 192.168.2.0/24 wg0 +admin@spoke1:/config/routing/…/static/> leave +admin@spoke1:/> +``` + +This configuration allows Spoke 1 to reach both the hub network (192.168.0.0/24) +and Spoke 2's network (192.168.2.0/24) via the hub, enabling spoke-to-spoke +communication through the central hub. + +### Persistent Keepalive + +The `persistent-keepalive` setting sends periodic packets to keep the tunnel +active through NAT devices and firewalls: + +``` +admin@example:/config/interface/wg0/wireguard/peer/…/> set persistent-keepalive 25 +``` + +This is particularly important when: + +- The peer is behind NAT +- Intermediate firewalls have connection timeouts +- You need the tunnel to remain ready for bidirectional traffic + +A value of 25 seconds is recommended for most scenarios. Omit this setting +for peers with public static IPs that initiate connections. + +> [!NOTE] +> Only the peer behind NAT needs `persistent-keepalive` configured. The +> peer with a public IP learns the NAT endpoint from incoming packets. + +### IPv6 Endpoints + +WireGuard fully supports IPv6 for tunnel endpoints: + +``` +admin@example:/> configure +admin@example:/config/> edit interface wg0 +admin@example:/config/interface/wg0/> set wireguard listen-port 51820 +admin@example:/config/interface/wg0/> set wireguard private-key wg-key +admin@example:/config/interface/wg0/> set ipv4 address 10.0.0.1 prefix-length 24 +admin@example:/config/interface/wg0/> set ipv6 address fd00::1 prefix-length 64 +admin@example:/config/interface/wg0/> edit wireguard peer wg-peers remote +admin@example:/config/interface/wg0/wireguard/peer/…/> set endpoint 2001:db8::2 +admin@example:/config/interface/wg0/wireguard/peer/…/> set endpoint-port 51820 +admin@example:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 10.0.0.2/32 +admin@example:/config/interface/wg0/wireguard/peer/…/> set allowed-ips fd00::2/128 +admin@example:/config/interface/wg0/wireguard/peer/…/> leave +admin@example:/> +``` + +WireGuard can carry both IPv4 and IPv6 traffic regardless of whether the +tunnel endpoints use IPv4 or IPv6. + +### Dynamic Endpoints (Road Warriors) + +For mobile clients or peers without fixed IPs, omit the `endpoint` setting. +WireGuard learns the peer's endpoint from authenticated incoming packets: + +``` +admin@hub:/> configure +admin@hub:/config/> edit interface wg0 wireguard peer wg-peers mobile-client +admin@hub:/config/interface/wg0/wireguard/peer/…/> set allowed-ips 10.0.0.10/32 +admin@hub:/config/interface/wg0/wireguard/peer/…/> leave +admin@hub:/> +``` + +The mobile client configures the hub's endpoint normally. The hub learns +and tracks the mobile client's changing IP address automatically. + +### Monitoring WireGuard Status + +Check WireGuard interface status and peer connections: + +``` +admin@example:/> show interfaces +wg0 wireguard UP 2 peers (1 up) + ipv4 10.0.0.1/24 (static) + ipv6 fd00::1/64 (static) + +admin@example:/> show interfaces wg0 +name : wg0 +type : wireguard +index : 12 +operational status : up +peers : 2 + + Peer 1: + status : UP + endpoint : 203.0.113.2:51820 + latest handshake : 2025-12-09T10:23:45+0000 + transfer tx : 125648 bytes + transfer rx : 98432 bytes + + Peer 2: + status : DOWN + endpoint : 203.0.113.3:51820 + latest handshake : 2025-12-09T09:15:22+0000 + transfer tx : 45120 bytes + transfer rx : 32768 bytes +``` + +The connection status shows `UP` if a handshake occurred within the last 3 +minutes, indicating an active tunnel. The `latest handshake` timestamp shows +when the peers last successfully authenticated and exchanged keys. + +### Post-Quantum Security (Preshared Keys) + +For additional security against future quantum computers, WireGuard supports +preshared keys that provide post-quantum resistance. + +**Generate a preshared key using `wg genpsk`:** + +```bash +admin@example:~$ wg genpsk > preshared.key +admin@example:~$ cat preshared.key +cO2DxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2m= +``` + +**Import the preshared key into the keystore:** + +``` +admin@example:/> configure +admin@example:/config/> edit keystore symmetric-key wg-psk +admin@example:/config/keystore/symmetric-key/wg-psk/> set key-format wireguard-symmetric-key-format +admin@example:/config/keystore/symmetric-key/wg-psk/> set key cO2DxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2m= +admin@example:/config/keystore/symmetric-key/wg-psk/> end +admin@example:/config/> edit interface wg0 wireguard peer wg-peers remote +admin@example:/config/interface/wg0/wireguard/peer/…/> set preshared-key wg-psk +admin@example:/config/interface/wg0/wireguard/peer/…/> leave +admin@example:/> +``` + +The preshared key must be securely shared between both peers and configured +on both sides. This provides an additional layer of symmetric encryption +alongside the Curve25519 key exchange. + +> [!IMPORTANT] +> Preshared keys must be kept secret and exchanged through a secure channel, +> just like passwords. Delete the `preshared.key` file after importing it +> into both peer keystores. diff --git a/patches/libnetconf2/3.7.10/001-server-config-UPDATE-allow-unsupported-keys.patch b/patches/libnetconf2/3.7.10/001-server-config-UPDATE-allow-unsupported-keys.patch new file mode 100644 index 000000000..697e522d3 --- /dev/null +++ b/patches/libnetconf2/3.7.10/001-server-config-UPDATE-allow-unsupported-keys.patch @@ -0,0 +1,15 @@ +diff --git a/src/server_config.c b/src/server_config.c +index 905f6aa..a893eb5 100644 +--- a/src/server_config.c ++++ b/src/server_config.c +@@ -1851,8 +1851,8 @@ nc_server_config_private_key_format(const struct lyd_node *node, enum nc_operati + + privkey_type = nc_server_config_get_private_key_type(format); + if (privkey_type == NC_PRIVKEY_FORMAT_UNKNOWN) { +- ERR(NULL, "Unknown private key format."); +- ret = 1; ++ DBG(NULL, "Unknown private key format."); ++ ret = 0; + goto cleanup; + } + diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 8db05041e..eaee5a879 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -38,6 +38,7 @@ confd_plugin_la_SOURCES = \ if-gre.c \ if-vxlan.c \ if-wifi.c \ + if-wireguard.c \ keystore.c \ system.c \ syslog.c \ diff --git a/src/confd/src/if-wireguard.c b/src/confd/src/if-wireguard.c new file mode 100644 index 000000000..10562a93e --- /dev/null +++ b/src/confd/src/if-wireguard.c @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +#include + +#include "interfaces.h" + +#define WIREGUARD_CONFIG "/run/wireguard-%s.conf" + +int wireguard_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip, struct dagger *net) +{ + const char *ifname = lydx_get_cattr(cif, "name"); + const char *listen_port, *private_key_ref; + const char *private_key_data; + struct lyd_node *wg, *peer, *key_node; + FILE *wg_fp = NULL; + FILE *wg_sh = NULL; + + wg = lydx_get_child(cif, "wireguard"); + if (!wg) + return -EINVAL; + + listen_port = lydx_get_cattr(wg, "listen-port"); + if (!listen_port) + listen_port = "51820"; + + private_key_ref = lydx_get_cattr(wg, "private-key"); + + key_node = lydx_get_xpathf(cif, "../../keystore/asymmetric-keys/asymmetric-key[name='%s']", private_key_ref); + private_key_data = lydx_get_cattr(key_node, "cleartext-private-key"); + + fprintf(ip, "link add dev %s type wireguard\n", ifname); + wg_fp = fopenf("w", WIREGUARD_CONFIG, ifname); + if (!wg_fp) + return -errno; + + fprintf(wg_fp, "[Interface]\n"); + fprintf(wg_fp, "PrivateKey = %s\n", private_key_data); + fprintf(wg_fp, "ListenPort = %s\n", listen_port); + + LYX_LIST_FOR_EACH(lyd_child(wg), peer, "peer") { + const char *public_key_bag_ref, *public_key_ref, *preshared_key_ref; + const char *public_key_data, *preshared_key_data; + const char *endpoint, *endpoint_port; + const char *keepalive; + struct lyd_node *allowed_ip, *pub_key_node, *psk_node; + + fprintf(wg_fp, "\n[Peer]\n"); + + public_key_bag_ref = lydx_get_cattr(peer, "public-key-bag"); + public_key_ref = lydx_get_cattr(peer, "public-key"); + + pub_key_node = lydx_get_xpathf(cif, "../../truststore/public-key-bags/public-key-bag[name='%s']/public-key[name='%s']", + public_key_bag_ref, public_key_ref); + public_key_data = lydx_get_cattr(pub_key_node, "public-key"); + + fprintf(wg_fp, "PublicKey = %s\n", public_key_data); + + preshared_key_ref = lydx_get_cattr(peer, "preshared-key"); + if (preshared_key_ref) { + psk_node = lydx_get_xpathf(cif, "../../keystore/symmetric-keys/symmetric-key[name='%s']", + preshared_key_ref); + preshared_key_data = lydx_get_cattr(psk_node, "cleartext-key"); + if (preshared_key_data) + fprintf(wg_fp, "PresharedKey = %s\n", preshared_key_data); + } + + endpoint = lydx_get_cattr(peer, "endpoint"); + if (endpoint) { + endpoint_port = lydx_get_cattr(peer, "endpoint-port"); + if (!endpoint_port) + endpoint_port = "51820"; + fprintf(wg_fp, "Endpoint = %s:%s\n", endpoint, endpoint_port); + } + + /* Output all allowed IPs on a single line, comma-separated */ + { + int first = 1; + LYX_LIST_FOR_EACH(lyd_child(peer), allowed_ip, "allowed-ips") { + const char *ip_prefix = lyd_get_value(allowed_ip); + if (ip_prefix) { + fprintf(wg_fp, "%s%s", first ? "AllowedIPs = " : ", ", ip_prefix); + first = 0; + } + } + if (!first) + fprintf(wg_fp, "\n"); + } + + keepalive = lydx_get_cattr(peer, "persistent-keepalive"); + if (keepalive) + fprintf(wg_fp, "PersistentKeepalive = %s\n", keepalive); + } + + fclose(wg_fp); + + wg_sh = dagger_fopen_net_init(net, ifname, NETDAG_INIT_POST, "enable-wireguard.sh"); + + fprintf(wg_sh, "wg setconf %s ", ifname); + fprintf(wg_sh, WIREGUARD_CONFIG, ifname); + fprintf(wg_sh, "\n"); + + /* Remove wireguard config after tunnel is configured, to protect keys */ + fprintf(wg_sh, "rm -f "); + fprintf(wg_sh, WIREGUARD_CONFIG, ifname); + fprintf(wg_sh, "\n"); + fclose(wg_sh); + + return 0; +} diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index 0a0cc0326..edbf76393 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -108,6 +108,8 @@ static int ifchange_cand_infer_type(sr_session_ctx_t *session, const char *path) inferred.data.string_val = "infix-if-type:gretap"; else if (!fnmatch("vxlan+([0-9])", ifname, FNM_EXTMATCH)) inferred.data.string_val = "infix-if-type:vxlan"; + else if (!fnmatch("wg+([0-9])", ifname, FNM_EXTMATCH)) + inferred.data.string_val = "infix-if-type:wireguard"; free(ifname); @@ -419,6 +421,8 @@ static int netdag_gen_afspec_add(sr_session_ctx_t *session, struct dagger *net, return vxlan_gen(NULL, cif, ip); case IFT_WIFI: return wifi_gen(NULL, cif, net); + case IFT_WIREGUARD: + return wireguard_gen(NULL, cif, ip, net); case IFT_ETH: return netdag_gen_ethtool(net, cif, dif); case IFT_LO: @@ -454,6 +458,7 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net, case IFT_GRETAP: case IFT_VETH: case IFT_VXLAN: + case IFT_WIREGUARD: case IFT_LO: return 0; @@ -490,6 +495,8 @@ static bool netdag_must_del(struct lyd_node *dif, struct lyd_node *cif) return lydx_get_descendant(lyd_child(dif), "veth", NULL); case IFT_VXLAN: return lydx_get_descendant(lyd_child(dif), "vxlan", NULL); + case IFT_WIREGUARD: + return lydx_get_descendant(lyd_child(dif), "wireguard", NULL); case IFT_UNKNOWN: ERR_IFACE(cif, -EINVAL, "unsupported interface type \"%s\"", lydx_get_cattr(cif, "type")); @@ -576,6 +583,7 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif, case IFT_LAG: case IFT_VLAN: case IFT_VXLAN: + case IFT_WIREGUARD: case IFT_UNKNOWN: link_gen_del(dif, ip); break; @@ -742,6 +750,7 @@ static int netdag_init_iface(struct lyd_node *cif) case IFT_GRETAP: case IFT_LO: case IFT_VXLAN: + case IFT_WIREGUARD: case IFT_UNKNOWN: break; } diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h index aaa583688..8271337a7 100644 --- a/src/confd/src/interfaces.h +++ b/src/confd/src/interfaces.h @@ -25,7 +25,6 @@ _map(IFT_BRIDGE, "infix-if-type:bridge") \ _map(IFT_DUMMY, "infix-if-type:dummy") \ _map(IFT_ETH, "infix-if-type:ethernet") \ - _map(IFT_WIFI, "infix-if-type:wifi") \ _map(IFT_GRE, "infix-if-type:gre") \ _map(IFT_GRETAP, "infix-if-type:gretap") \ _map(IFT_LAG, "infix-if-type:lag") \ @@ -33,6 +32,8 @@ _map(IFT_VETH, "infix-if-type:veth") \ _map(IFT_VLAN, "infix-if-type:vlan") \ _map(IFT_VXLAN, "infix-if-type:vxlan") \ + _map(IFT_WIFI, "infix-if-type:wifi") \ + _map(IFT_WIREGUARD,"infix-if-type:wireguard") \ /* */ enum iftype { @@ -150,4 +151,7 @@ int ifchange_cand_infer_dhcp(sr_session_ctx_t *session, const char *path); /* if-vxlan.c */ int vxlan_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); +/* infix-if-wireguard */ +int wireguard_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip, struct dagger *net); + #endif /* CONFD_INTERFACES_H_ */ diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 67a282a27..1c2de2b2b 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -45,7 +45,7 @@ MODULES=( "infix-factory-default@2023-06-28.yang" "infix-interfaces@2025-11-06.yang -e vlan-filtering" "ietf-crypto-types -e cleartext-symmetric-keys" - "infix-crypto-types@2025-06-17.yang" + "infix-crypto-types@2025-11-09.yang" "ietf-keystore -e symmetric-keys" - "infix-keystore@2025-06-17.yang" + "infix-keystore@2025-11-09.yang" ) diff --git a/src/confd/yang/confd/infix-crypto-types.yang b/src/confd/yang/confd/infix-crypto-types.yang index 25851e673..f6b06e926 100644 --- a/src/confd/yang/confd/infix-crypto-types.yang +++ b/src/confd/yang/confd/infix-crypto-types.yang @@ -6,6 +6,9 @@ module infix-crypto-types { prefix ct; } + revision 2025-11-09 { + description "Add Wireguard public/private key and sha"; + } revision 2025-06-17 { description "Add Wi-Fi secret support."; } @@ -28,6 +31,7 @@ module infix-crypto-types { base public-key-format; base ct:ssh-public-key-format; } + identity symmetric-key-format { description "Base for symmetric key format"; @@ -38,4 +42,29 @@ module infix-crypto-types { description "WiFi secret key"; } + + identity x25519-public-key-format { + base public-key-format; + base ct:public-key-format; + description + "X25519 (Curve25519) public key format for Diffie-Hellman key exchange. + This is the format used by WireGuard."; + } + + identity x25519-private-key-format { + base private-key-format; + base ct:private-key-format; + description + "X25519 (Curve25519) private key format for Diffie-Hellman key exchange. + This is the format used by WireGuard."; + } + + identity wireguard-symmetric-key-format { + base ct:symmetric-key-format; + base symmetric-key-format; + description + "WireGuard pre-shared key format. + 32-byte base64-encoded key used as an optional additional layer + of symmetric encryption for post-quantum resistance."; + } } diff --git a/src/confd/yang/confd/infix-crypto-types@2025-06-17.yang b/src/confd/yang/confd/infix-crypto-types@2025-11-09.yang similarity index 100% rename from src/confd/yang/confd/infix-crypto-types@2025-06-17.yang rename to src/confd/yang/confd/infix-crypto-types@2025-11-09.yang diff --git a/src/confd/yang/confd/infix-if-bridge.yang b/src/confd/yang/confd/infix-if-bridge.yang index 72247ba61..4f3d0c74f 100644 --- a/src/confd/yang/confd/infix-if-bridge.yang +++ b/src/confd/yang/confd/infix-if-bridge.yang @@ -915,6 +915,7 @@ submodule infix-if-bridge { "derived-from-or-self(if:type,'ianaift:ilan') or "+ "derived-from-or-self(if:type,'infix-ift:gretap') or "+ "derived-from-or-self(if:type,'infix-ift:wifi') or "+ + "derived-from-or-self(if:type,'infix-ift:wireguard') or "+ "derived-from-or-self(if:type,'infix-ift:vxlan')" { description "Applies when a Bridge interface exists."; } diff --git a/src/confd/yang/confd/infix-if-type.yang b/src/confd/yang/confd/infix-if-type.yang index 8e2ea0d6f..7a21e1faf 100644 --- a/src/confd/yang/confd/infix-if-type.yang +++ b/src/confd/yang/confd/infix-if-type.yang @@ -81,6 +81,9 @@ module infix-if-type { identity vxlan { base infix-interface-type; } + identity wireguard { + base infix-interface-type; + } identity lag { base infix-interface-type; base ianaift:ieee8023adLag; diff --git a/src/confd/yang/confd/infix-if-wireguard.yang b/src/confd/yang/confd/infix-if-wireguard.yang new file mode 100644 index 000000000..d1bceeec3 --- /dev/null +++ b/src/confd/yang/confd/infix-if-wireguard.yang @@ -0,0 +1,258 @@ +submodule infix-if-wireguard { + yang-version 1.1; + belongs-to infix-interfaces { + prefix infix-if; + } + + import ietf-interfaces { + prefix if; + } + import ietf-inet-types { + prefix inet; + } + import infix-crypto-types { + prefix ixct; + } + import ietf-keystore { + prefix ks; + } + import infix-keystore { + prefix infix-ks; + } + import ietf-truststore { + prefix ts; + } + import infix-if-type { + prefix infixift; + } + import ietf-yang-types { + prefix yang; + } + + organization "KernelKit"; + contact "kernelkit@googlegroups.com"; + description "WireGuard VPN tunnel interface"; + + revision 2025-11-09 { + description "Initial revision"; + reference "internal"; + } + + typedef port { + type inet:port-number; + description + "WireGuard UDP port. Valid range: 0..65535."; + } + + typedef keepalive-interval { + type uint16 { + range "1..65535"; + } + units "seconds"; + description + "Persistent keepalive interval in seconds. + + A keepalive packet will be sent to the peer endpoint at this + interval if no traffic has been exchanged. This is useful for + traversing NAT and firewalls that may close the UDP connection + after a period of inactivity. + + Recommended value is 25 seconds for peers behind NAT."; + } + + augment "/if:interfaces/if:interface" { + when "derived-from-or-self(if:type, 'infixift:wireguard')" { + description "Only shown for if:type infixift:wireguard"; + } + container wireguard { + description "WireGuard VPN configuration"; + + leaf listen-port { + type port; + default 51820; + description "Local UDP port to listen on for incoming WireGuard traffic"; + } + + leaf private-key { + type ks:asymmetric-key-ref; + mandatory true; + description "Reference to WireGuard private key (X25519/Curve25519 format)"; + must "not(deref(.)/../ks:public-key-format) or " + + "(derived-from-or-self(deref(.)/../ks:public-key-format, 'ixct:x25519-public-key-format') and " + + "derived-from-or-self(deref(.)/../ks:private-key-format, 'ixct:x25519-private-key-format'))" { + error-message "Private key must be in WireGuard (X25519/Curve25519) format"; + } + } + + list peer { + key "public-key-bag public-key"; + description "WireGuard peer configuration"; + + leaf public-key-bag { + type ts:public-key-bag-ref; + description "Reference to public key bag containing peer's public key"; + } + + leaf public-key { + type ts:public-key-ref; + description "Reference to peer's WireGuard public key (X25519/Curve25519 format)"; + must "not(deref(.)/../ts:public-key-format) or " + + "(derived-from-or-self(deref(.)/../ts:public-key-format, 'ixct:x25519-public-key-format'))" { + error-message "Public key must be in WireGuard (X25519/Curve25519) format"; + } + } + + leaf preshared-key { + type ks:symmetric-key-ref; + description + "Optional preshared key for additional layer of symmetric encryption. + + This provides post-quantum resistance as an attacker would need + to break both the Curve25519 key exchange and this symmetric key."; + must "derived-from-or-self(deref(.)/../infix-ks:key-format, 'ixct:wireguard-symmetric-key-format')" { + error-message "Preshared key must be in wireguard-symmetric-key-format"; + } + } + + leaf endpoint { + type inet:host; + description + "Peer endpoint address (IP address or DNS hostname). + + If not specified, this peer can only be a responder and cannot + initiate connections. WireGuard will learn the endpoint from + incoming authenticated packets."; + } + + leaf endpoint-port { + type port; + default 51820; + description "Peer endpoint UDP port"; + } + + leaf-list allowed-ips { + type inet:ip-prefix; + min-elements 1; + description + "List of IP address ranges (in CIDR notation) that are allowed + to be used as source addresses inside the tunnel from this peer. + + This also controls which destination addresses will be routed + to this peer. For example: + - '10.0.0.2/32' allows only this single IP + - '10.0.0.0/24' allows the entire subnet + - '0.0.0.0/0, ::/0' routes all traffic through this peer + + WireGuard uses this as a cryptographic routing table."; + } + + leaf persistent-keepalive { + type keepalive-interval; + description + "Interval in seconds to send keepalive packets to this peer. + + If not specified (or set to 0), keepalive is disabled. + Use this when the peer is behind NAT or a firewall that may + close the UDP connection after inactivity."; + } + } + + container peer-status { + config false; + description "Operational state for WireGuard peers"; + + list peer { + key "public-key"; + description "Per-peer operational statistics and status"; + + leaf public-key { + type string { + pattern '[A-Za-z0-9+/]{43}='; + } + description + "WireGuard public key of the peer in base64 encoding. + + This is the actual key value, not a keystore reference."; + } + + leaf latest-handshake { + type yang:date-and-time; + description + "Timestamp of the most recent successful handshake with this peer. + + If no handshake has occurred yet, this leaf will not be present. + A successful handshake indicates that the peer is authenticated + and a secure session has been established."; + } + + leaf endpoint-address { + type inet:ip-address; + description + "The actual IP address from which packets were last received + from this peer. + + This may differ from the configured endpoint if: + - The peer is roaming (changed IP address) + - The configured endpoint is a DNS hostname + - No endpoint was configured (learned from incoming packets) + + If no packets have been received, this leaf will not be present."; + } + + leaf endpoint-port { + type port; + description + "The actual UDP port from which packets were last received + from this peer. + + If no packets have been received, this leaf will not be present."; + } + + container transfer { + description "Data transfer statistics for this peer"; + + leaf tx-bytes { + type yang:counter64; + units "bytes"; + description + "Total number of bytes transmitted (sent) to this peer. + + This counts encrypted payload bytes sent through the tunnel."; + } + + leaf rx-bytes { + type yang:counter64; + units "bytes"; + description + "Total number of bytes received from this peer. + + This counts decrypted payload bytes received through the tunnel."; + } + } + + leaf connection-status { + type enumeration { + enum down { + description + "No handshake has occurred, or the last handshake is too old. + + The peer is not considered connected."; + } + enum up { + description + "A recent handshake has occurred and the connection is active. + + Typically means a handshake within the last 2-3 minutes."; + } + } + description + "Current connection status with this peer. + + This is derived from the latest-handshake timestamp and indicates + whether the tunnel is currently operational."; + } + } + } + } + } +} diff --git a/src/confd/yang/confd/infix-if-wireguard@2025-11-09.yang b/src/confd/yang/confd/infix-if-wireguard@2025-11-09.yang new file mode 120000 index 000000000..4d9bf2eeb --- /dev/null +++ b/src/confd/yang/confd/infix-if-wireguard@2025-11-09.yang @@ -0,0 +1 @@ +infix-if-wireguard.yang \ No newline at end of file diff --git a/src/confd/yang/confd/infix-interfaces.yang b/src/confd/yang/confd/infix-interfaces.yang index 9fea0434e..048d90365 100644 --- a/src/confd/yang/confd/infix-interfaces.yang +++ b/src/confd/yang/confd/infix-interfaces.yang @@ -28,6 +28,7 @@ module infix-interfaces { include infix-if-gre; include infix-if-vxlan; include infix-if-wifi; + include infix-if-wireguard; organization "KernelKit"; contact "kernelkit@googlegroups.com"; diff --git a/src/confd/yang/confd/infix-keystore.yang b/src/confd/yang/confd/infix-keystore.yang index 97ae2b703..19878856c 100644 --- a/src/confd/yang/confd/infix-keystore.yang +++ b/src/confd/yang/confd/infix-keystore.yang @@ -11,6 +11,10 @@ module infix-keystore { import infix-crypto-types { prefix infix-ct; } + + revision 2025-11-09 { + description "Add WireGuard support"; + } revision 2025-06-17 { description "Add Wi-Fi secrets support"; } diff --git a/src/confd/yang/confd/infix-keystore@2025-06-17.yang b/src/confd/yang/confd/infix-keystore@2025-11-09.yang similarity index 100% rename from src/confd/yang/confd/infix-keystore@2025-06-17.yang rename to src/confd/yang/confd/infix-keystore@2025-11-09.yang diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index c85a1db8c..175e253d4 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -878,6 +878,7 @@ def __init__(self, data): self.gre = self.data.get('infix-interfaces:gre') self.vxlan = self.data.get('infix-interfaces:vxlan') self.wifi = self.data.get('infix-interfaces:wifi') + self.wireguard = self.data.get('infix-interfaces:wireguard') if self.data.get('infix-interfaces:vlan'): self.lower_if = self.data.get('infix-interfaces:vlan', None).get('lower-layer-if',None) @@ -912,6 +913,9 @@ def is_gre(self): def is_gretap(self): return self.data['type'] == "infix-if-type:gretap" + def is_wireguard(self): + return self.data['type'] == "infix-if-type:wireguard" + def oper(self, detail=False): """Remap in brief overview to fit column widths.""" if not detail and self.oper_status == "lower-layer-down": @@ -983,6 +987,23 @@ def pr_proto_vxlan(self, pipe=''): row = self._pr_proto_common("vxlan", True, pipe); print(row) + def pr_proto_wireguard(self, pipe=''): + row = self._pr_proto_common("wireguard", False, pipe) + + if self.wireguard: + peer_status = self.wireguard.get('peer-status', {}) + peers = peer_status.get('peer', []) + total_peers = len(peers) + up_peers = sum(1 for p in peers if p.get('connection-status') == 'up') + + if total_peers > 0: + row += f"{total_peers} peer" + if total_peers != 1: + row += "s" + row += f" ({up_peers} up)" + + print(row) + def pr_proto_loopack(self, pipe=''): row = self._pr_proto_common("loopback", False, pipe); print(row) @@ -1167,6 +1188,12 @@ def pr_vxlan(self): self.pr_proto_ipv4() self.pr_proto_ipv6() + def pr_wireguard(self): + self.pr_name(pipe="") + self.pr_proto_wireguard() + self.pr_proto_ipv4() + self.pr_proto_ipv6() + def pr_wifi(self): self.pr_name(pipe="") self.pr_proto_wifi() @@ -1307,6 +1334,43 @@ def pr_iface(self): print(f"{'remote address':<{20}}: {self.vxlan['remote']}") print(f"{'VxLAN id':<{20}}: {self.vxlan['vni']}") + if self.wireguard: + peer_status = self.wireguard.get('peer-status', {}) + peers = peer_status.get('peer', []) + if peers: + print(f"{'peers':<{20}}: {len(peers)}") + for idx, peer in enumerate(peers, 1): + print(f"\n Peer {idx}:") + + # Public key (always 44 chars: 43 + '=') + if pubkey := peer.get('public-key'): + print(f" {'public key':<{18}}: {pubkey}") + + # Connection status with color + status = peer.get('connection-status', 'unknown') + if status == 'up': + status_str = Decore.green(status.upper()) + else: + status_str = Decore.red(status.upper()) + print(f" {'status':<{18}}: {status_str}") + + # Endpoint information + if endpoint := peer.get('endpoint-address'): + port = peer.get('endpoint-port', '') + endpoint_str = f"{endpoint}:{port}" if port else endpoint + print(f" {'endpoint':<{18}}: {endpoint_str}") + + # Latest handshake + if handshake := peer.get('latest-handshake'): + print(f" {'latest handshake':<{18}}: {handshake}") + + # Transfer statistics + if transfer := peer.get('transfer'): + tx = transfer.get('tx-bytes', '0') + rx = transfer.get('rx-bytes', '0') + print(f" {'transfer tx':<{18}}: {tx} bytes") + print(f" {'transfer rx':<{18}}: {rx} bytes") + if self.in_octets and self.out_octets: print(f"{'in-octets':<{20}}: {self.in_octets}") print(f"{'out-octets':<{20}}: {self.out_octets}") @@ -1477,6 +1541,10 @@ def pr_interface_list(json): iface.pr_vxlan() continue + if iface.is_wireguard(): + iface.pr_wireguard() + continue + if iface.is_wifi(): iface.pr_wifi() continue diff --git a/src/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py index 9cd321689..60665e0ce 100644 --- a/src/statd/python/yanger/ietf_interfaces/link.py +++ b/src/statd/python/yanger/ietf_interfaces/link.py @@ -9,6 +9,7 @@ from . import veth from . import vlan from . import wifi +from . import wireguard def statistics(iplink): @@ -27,6 +28,7 @@ def statistics(iplink): def iplink2yang_type(iplink): ifname=iplink["ifname"] + match iplink["link_type"]: case "loopback": return "infix-if-type:loopback" @@ -36,6 +38,8 @@ def iplink2yang_type(iplink): data = HOST.run(tuple(f"ls /sys/class/net/{ifname}/wireless/".split()), default="no") if data != "no": return "infix-if-type:wifi" + case "none": + pass # WireGuard interfaces is for some reason link_type none case _: return "infix-if-type:other" @@ -54,6 +58,8 @@ def iplink2yang_type(iplink): return "infix-if-type:veth" case "vlan": return "infix-if-type:vlan" + case "wireguard": + return "infix-if-type:wireguard" return "infix-if-type:ethernet" @@ -141,6 +147,9 @@ def interface(iplink, ipaddr): case "infix-if-type:wifi": if w := wifi.wifi(iplink["ifname"]): interface["infix-interfaces:wifi"] = w + case "infix-if-type:wireguard": + if wg := wireguard.wireguard(iplink): + interface["infix-interfaces:wireguard"] = wg match iplink2yang_lower(iplink): case "infix-interfaces:bridge-port": diff --git a/src/statd/python/yanger/ietf_interfaces/wireguard.py b/src/statd/python/yanger/ietf_interfaces/wireguard.py new file mode 100644 index 000000000..901386b2f --- /dev/null +++ b/src/statd/python/yanger/ietf_interfaces/wireguard.py @@ -0,0 +1,127 @@ +import subprocess +import json +from datetime import datetime, timezone + +from ..host import HOST + + +def _parse_wg_show(ifname): + """Parse `wg show dump` output into structured data""" + try: + result = HOST.run(("wg", "show", ifname, "dump"), default="") + if not result: + return None + + lines = result.strip().split('\n') + if len(lines) < 2: # Need at least interface line + one peer + return None + + peers = [] + # Skip first line (interface info), process peer lines + for line in lines[1:]: + parts = line.split('\t') + if len(parts) < 8: + continue + + public_key, preshared_key, endpoint, allowed_ips, \ + latest_handshake, rx_bytes, tx_bytes, persistent_keepalive = parts + + peer = { + "public_key": public_key, + "endpoint": endpoint if endpoint != "(none)" else None, + "allowed_ips": allowed_ips.split(',') if allowed_ips else [], + "latest_handshake": int(latest_handshake) if latest_handshake != "0" else None, + "rx_bytes": int(rx_bytes), + "tx_bytes": int(tx_bytes), + } + peers.append(peer) + + return peers + except Exception: + return None + + +def _format_timestamp(epoch_seconds): + """Convert Unix timestamp to YANG date-and-time format""" + if not epoch_seconds: + return None + dt = datetime.fromtimestamp(epoch_seconds, tz=timezone.utc) + # YANG date-and-time requires timezone with colon: +00:00 not +0000 + timestamp = dt.strftime("%Y-%m-%dT%H:%M:%S%z") + # Insert colon in timezone offset: +0000 -> +00:00 + return timestamp[:-2] + ':' + timestamp[-2:] + + +def _parse_endpoint(endpoint_str): + """Parse endpoint string like '192.168.1.1:51820' or '[2001:db8::1]:51820'""" + if not endpoint_str or endpoint_str == "(none)": + return None, None + + # Handle IPv6 with brackets + if endpoint_str.startswith('['): + addr_end = endpoint_str.find(']') + if addr_end == -1: + return None, None + addr = endpoint_str[1:addr_end] + port_part = endpoint_str[addr_end+1:] + port = int(port_part.lstrip(':')) if ':' in port_part else None + return addr, port + + # Handle IPv4 + parts = endpoint_str.rsplit(':', 1) + if len(parts) == 2: + return parts[0], int(parts[1]) + return parts[0], None + + +def _connection_status(latest_handshake_epoch): + """Determine connection status based on handshake time""" + if not latest_handshake_epoch: + return "down" + + # Consider connection up if handshake within last 3 minutes + age = datetime.now(timezone.utc).timestamp() - latest_handshake_epoch + return "up" if age < 180 else "down" + + +def wireguard(iplink): + """Get WireGuard operational state data""" + ifname = iplink.get("ifname") + if not ifname: + return None + + peers_data = _parse_wg_show(ifname) + if not peers_data: + return None + + peers = [] + for peer_data in peers_data: + peer = { + "public-key": peer_data["public_key"] + } + + # Connection status (always include) + if peer_data["latest_handshake"]: + peer["latest-handshake"] = _format_timestamp(peer_data["latest_handshake"]) + peer["connection-status"] = _connection_status(peer_data["latest_handshake"]) + else: + peer["connection-status"] = "down" + + # Parse endpoint + if peer_data["endpoint"]: + addr, port = _parse_endpoint(peer_data["endpoint"]) + if addr: + peer["endpoint-address"] = addr + if port: + peer["endpoint-port"] = port + + # Transfer statistics + if peer_data["tx_bytes"] or peer_data["rx_bytes"]: + peer["transfer"] = { + "tx-bytes": str(peer_data["tx_bytes"]), + "rx-bytes": str(peer_data["rx_bytes"]), + } + + peers.append(peer) + + return {"peer-status": {"peer": peers}} if peers else None diff --git a/test/case/interfaces/tunnels.yaml b/test/case/interfaces/tunnels.yaml index 5886bc190..fed1c857d 100644 --- a/test/case/interfaces/tunnels.yaml +++ b/test/case/interfaces/tunnels.yaml @@ -7,3 +7,9 @@ - name: Tunnel TTL verification suite: tunnel_ttl/test.yaml + +- name: Wireguard p2p + case: wireguard_p2p/test.py + +- name: WireGuard multipoint + case: wireguard_multipoint/test.py diff --git a/test/case/interfaces/wireguard_multipoint/Readme.adoc b/test/case/interfaces/wireguard_multipoint/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/wireguard_multipoint/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/wireguard_multipoint/test.adoc b/test/case/interfaces/wireguard_multipoint/test.adoc new file mode 100644 index 000000000..89189c92d --- /dev/null +++ b/test/case/interfaces/wireguard_multipoint/test.adoc @@ -0,0 +1,62 @@ +=== WireGuard multipoint + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wireguard_multipoint] + +==== Description + +Set up a WireGuard hub-and-spoke topology with one server (hub) and two +clients (spokes). The server acts as a central point through which clients +can communicate. Host namespaces are connected behind the server and client1 +to test routing through the WireGuard mesh. + +This test verifies: + +- WireGuard hub-and-spoke topology with multiple peers +- Mixed IPv4/IPv6 tunnel endpoints (client1 uses IPv4, client2 uses IPv6) +- Dual-stack WireGuard tunnels carrying both IPv4 and IPv6 traffic +- Advanced key management with preshared keys for post-quantum resistance +- Persistent keepalive configuration for NAT traversal +- Different listen ports on server and clients +- Multiple allowed-ips per peer for routing multiple subnets +- Static routes for subnet reachability through WireGuard +- Security boundaries enforced by allowed-ips (client2 isolated from server subnet) +- IPv4 and IPv6 connectivity through the encrypted tunnel mesh +- Proper routing between all nodes in the WireGuard network + +Topology: + +Network layout: +- Server (192.168.0.2, 2001:db8:3c4d:02::2) with nsserver namespace (192.168.0.1, 2001:db8:3c4d:02::100) +- Client1 (192.168.1.2, 2001:db8:3c4d:01::2) with nsclient1 namespace (192.168.1.1, 2001:db8:3c4d:01::100) +- Client2 (no data subnet) + +WireGuard hub-and-spoke: + server:wg0 (10.0.0.1, fd00:0::1) + | + +----------------+----------------+ + | | + client1:wg0 client2:wg0 + (10.0.0.2, fd00:0::2) (10.0.0.3, fd00:0::3) + via IPv4 endpoint via IPv6 endpoint + 192.168.10.x 2001:db8:3c4d:20::x + +Security boundaries: +- nsclient1 can reach all WireGuard IPs (10.0.0.1, .2, .3 and fd00:0::1, ::2, ::3) +- nsserver can reach server and client1 WireGuard IPs, but NOT client2 (blocked by allowed-ips) + +==== Topology + +image::topology.svg[WireGuard multipoint topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUTs +. Configure server, client1 and client2 +. Verify IPv4 connectivity from host to server through client1 +. Verify IPv4 connectivity from host to client2 through WireGuard mesh +. Verify host:data2 can not ping 10.0.0.3 +. Verify IPv6 connectivity from host to server through client1 +. Verify IPv6 connectivity from host to client2 through WireGuard mesh +. Verify host:data2 can not ping fd00:0::3 + + diff --git a/test/case/interfaces/wireguard_multipoint/test.py b/test/case/interfaces/wireguard_multipoint/test.py new file mode 100755 index 000000000..c3f4d015e --- /dev/null +++ b/test/case/interfaces/wireguard_multipoint/test.py @@ -0,0 +1,518 @@ +#!/usr/bin/env python3 +r""" +Advanced WireGuard multipoint hub-and-spoke tunnel test + +Set up a WireGuard hub-and-spoke topology with one server (hub) and two +clients (spokes). The server acts as a central point through which clients +can communicate. Host namespaces are connected behind the server and client1 +to test routing through the WireGuard mesh. + +This test verifies: + +- WireGuard hub-and-spoke topology with multiple peers +- Mixed IPv4/IPv6 tunnel endpoints (client1 uses IPv4, client2 uses IPv6) +- Dual-stack WireGuard tunnels carrying both IPv4 and IPv6 traffic +- Advanced key management with preshared keys for post-quantum resistance +- Persistent keepalive configuration for NAT traversal +- Different listen ports on server and clients +- Multiple allowed-ips per peer for routing multiple subnets +- Static routes for subnet reachability through WireGuard +- Security boundaries enforced by allowed-ips (client2 isolated from server subnet) +- IPv4 and IPv6 connectivity through the encrypted tunnel mesh +- Proper routing between all nodes in the WireGuard network + +Topology: + +Network layout: +- Server (192.168.0.2, 2001:db8:3c4d:02::2) with nsserver namespace (192.168.0.1, 2001:db8:3c4d:02::100) +- Client1 (192.168.1.2, 2001:db8:3c4d:01::2) with nsclient1 namespace (192.168.1.1, 2001:db8:3c4d:01::100) +- Client2 (no data subnet) + +WireGuard hub-and-spoke: + server:wg0 (10.0.0.1, fd00:0::1) + | + +----------------+----------------+ + | | + client1:wg0 client2:wg0 + (10.0.0.2, fd00:0::2) (10.0.0.3, fd00:0::3) + via IPv4 endpoint via IPv6 endpoint + 192.168.10.x 2001:db8:3c4d:20::x + +Security boundaries: +- nsclient1 can reach all WireGuard IPs (10.0.0.1, .2, .3 and fd00:0::1, ::2, ::3) +- nsserver can reach server and client1 WireGuard IPs, but NOT client2 (blocked by allowed-ips) + +""" + +import infamy +import infamy.util as util + +# Server keys +server_private_key = "uIUL4AnD5QaVrwHDPHJzQ7sIQ+Q3zDdflnvfd59qa28=" +server_public_key = "qGVmu5UbNtMuZs2t9wFoOoHlvgmV+A1SyQacVb/bEV0=" + +# Client1 keys +client1_private_key = "kNmkNlSkSh9+Va2tmFv9Va8TBCZlTBF0fKAGJf8vomo=" +client1_public_key = "ROaZyvJc5DzA2XUAAeTj2YlwDsy2w0lr3t+rWj2imAk=" + +# Client2 keys +client2_private_key = "OPT7v/l5zICEmFIrO0U+YwA+w07l8Xo2Dp38hjGOHGY=" +client2_public_key = "Om9CPLYdK3l93GauKrq5WXo/gbcD+1CeqFpobRLLkB4=" + +# Preshared keys (256-bit symmetric keys, base64-encoded) +psk_client1 = "zYr83O4Ykj9i1gN+/aaosJxQxCzvXv1EYOj0MX9H2K4=" +psk_client2 = "A4Gf6KCp+CL+tH2TUd9cyARpBZAH8e+9QXiPJ0t+4So=" + + +def configure_server(dut): + dut.put_config_dicts({ + "ietf-keystore": { + "keystore": { + "asymmetric-keys": { + "asymmetric-key": [{ + "name": "server-wg-key", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": server_public_key, + "private-key-format": "infix-crypto-types:x25519-private-key-format", + "cleartext-private-key": server_private_key + }] + }, + "symmetric-keys": { + "symmetric-key": [{ + "name": "psk-client1", + "key-format": "infix-crypto-types:wireguard-symmetric-key-format", + "cleartext-key": psk_client1 + }, { + "name": "psk-client2", + "key-format": "infix-crypto-types:wireguard-symmetric-key-format", + "cleartext-key": psk_client2 + }] + } + } + }, + "ietf-truststore": { + "truststore": { + "public-key-bags": { + "public-key-bag": [{ + "name": "wireguard-clients", + "public-key": [{ + "name": "client1-wg-pubkey", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": client1_public_key + }, { + "name": "client2-wg-pubkey", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": client2_public_key + }] + }] + } + } + }, + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": server["link1"], + "ipv4": { + "address": [{ + "ip": "192.168.10.1", + "prefix-length": 24 + }] + } + }, { + "name": server["link2"], + "ipv6": { + "address": [{ + "ip": "2001:db8:3c4d:20::1", + "prefix-length": 64 + }] + } + }, { + "name": server["data"], + "ipv4": { + "address": [{ + "ip": "192.168.0.2", + "prefix-length": 24 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "2001:db8:3c4d:02::2", + "prefix-length": 64 + }], + "forwarding": True + } + }, + { + "name": "wg0", + "type": "infix-if-type:wireguard", + "ipv4": { + "address": [{ + "ip": "10.0.0.1", + "prefix-length": 24 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "fd00:0::1", + "prefix-length": 64 + }], + "forwarding": True + }, + "wireguard": { + "listen-port": 51820, + "private-key": "server-wg-key", + "peer": [{ + "public-key-bag": "wireguard-clients", + "public-key": "client1-wg-pubkey", + "preshared-key": "psk-client1", + "endpoint": "192.168.10.2", + "endpoint-port": 51821, + "allowed-ips": ["10.0.0.2/32", "192.168.1.0/24", "fd00:0::2/128", "2001:db8:3c4d:01::/64"], + "persistent-keepalive": 25 + }, { + "public-key-bag": "wireguard-clients", + "public-key": "client2-wg-pubkey", + "preshared-key": "psk-client2", + "endpoint": "2001:db8:3c4d:20::2", + "endpoint-port": 51822, + "allowed-ips": ["10.0.0.3/32", "fd00:0::3/128"], + "persistent-keepalive": 30 + }] + } + }] + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:static", + "name": "default", + "static-routes": { + "ipv4": { + "route": [{ + "destination-prefix": "192.168.1.0/24", + "next-hop": { + "next-hop-address": "10.0.0.2" + } + }] + }, + "ipv6": { + "route": [{ + "destination-prefix": "2001:db8:3c4d:01::/64", + "next-hop": { + "next-hop-address": "fd00:0::2" + } + }] + } + } + }] + } + } + } + }) + +def configure_client1(dut): + dut.put_config_dicts({ + "ietf-keystore": { + "keystore": { + "asymmetric-keys": { + "asymmetric-key": [{ + "name": "client1-wg-key", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": client1_public_key, + "private-key-format": "infix-crypto-types:x25519-private-key-format", + "cleartext-private-key": client1_private_key + }] + }, + "symmetric-keys": { + "symmetric-key": [{ + "name": "psk-server", + "key-format": "infix-crypto-types:wireguard-symmetric-key-format", + "cleartext-key": psk_client1 + }] + } + } + }, + "ietf-truststore": { + "truststore": { + "public-key-bags": { + "public-key-bag": [{ + "name": "wireguard-server", + "public-key": [{ + "name": "server-wg-pubkey", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": server_public_key + }] + }] + } + } + }, + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": client1["link"], + "ipv4": { + "address": [{ + "ip": "192.168.10.2", + "prefix-length": 24 + }] + } + }, { + "name": client1["data"], + "ipv4": { + "address": [{ + "ip": "192.168.1.2", + "prefix-length": 24 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "2001:db8:3c4d:01::2", + "prefix-length": 64 + }], + "forwarding": True + } + }, { + "name": "wg0", + "type": "infix-if-type:wireguard", + "ipv4": { + "address": [{ + "ip": "10.0.0.2", + "prefix-length": 24 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "fd00:0::2", + "prefix-length": 64 + }], + "forwarding": True + }, + "wireguard": { + "listen-port": 51821, + "private-key": "client1-wg-key", + "peer": [{ + "public-key-bag": "wireguard-server", + "public-key": "server-wg-pubkey", + "preshared-key": "psk-server", + "endpoint": "192.168.10.1", + "endpoint-port": 51820, + "allowed-ips": ["10.0.0.1/32", "10.0.0.3/32", "192.168.0.0/24", "fd00:0::1/128", "fd00:0::3/128", "2001:db8:3c4d:02::/64"], + "persistent-keepalive": 25 + }] + } + }] + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:static", + "name": "default", + "static-routes": { + "ipv4": { + "route": [{ + "destination-prefix": "10.0.0.0/24", + "next-hop": { + "next-hop-address": "10.0.0.1" + } + }, { + "destination-prefix": "192.168.0.0/16", + "next-hop": { + "next-hop-address": "10.0.0.1" + } + }] + }, + "ipv6": { + "route": [{ + "destination-prefix": "fd00::/64", + "next-hop": { + "next-hop-address": "fd00::1" + } + }, { + "destination-prefix": "2001:db8:3c4d:02::/64", + "next-hop": { + "next-hop-address": "fd00::1" + } + }] + } + } + }] + } + } + } + }) + +def configure_client2(dut): + dut.put_config_dicts({ + "ietf-keystore": { + "keystore": { + "asymmetric-keys": { + "asymmetric-key": [{ + "name": "client2-wg-key", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": client2_public_key, + "private-key-format": "infix-crypto-types:x25519-private-key-format", + "cleartext-private-key": client2_private_key + }] + }, + "symmetric-keys": { + "symmetric-key": [{ + "name": "psk-server", + "key-format": "infix-crypto-types:wireguard-symmetric-key-format", + "cleartext-key": psk_client2 + }] + } + } + }, + "ietf-truststore": { + "truststore": { + "public-key-bags": { + "public-key-bag": [{ + "name": "wireguard-server", + "public-key": [{ + "name": "server-wg-pubkey", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": server_public_key + }] + }] + } + } + }, + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": client2["link"], + "ipv6": { + "address": [{ + "ip": "2001:db8:3c4d:20::2", + "prefix-length": 64 + }] + } + }, { + "name": "wg0", + "type": "infix-if-type:wireguard", + "ipv4": { + "address": [{ + "ip": "10.0.0.3", + "prefix-length": 24 + }] + }, + "ipv6": { + "address": [{ + "ip": "fd00:0::3", + "prefix-length": 64 + }] + }, + + "wireguard": { + "listen-port": 51822, + "private-key": "client2-wg-key", + "peer": [{ + "public-key-bag": "wireguard-server", + "public-key": "server-wg-pubkey", + "preshared-key": "psk-server", + "endpoint": "2001:db8:3c4d:20::1", + "endpoint-port": 51820, + "allowed-ips": ["10.0.0.1/32", "10.0.0.2/32", "192.168.1.0/24", "fd00:0::1/128", "fd00:0::2/128", "2001:db8:3c4d:01::/64"], + "persistent-keepalive": 30 + }] + } + }] + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:static", + "name": "default", + "static-routes": { + "ipv4": { + "route": [{ + "destination-prefix": "10.0.0.0/24", + "next-hop": { + "next-hop-address": "10.0.0.1" + } + }, { + "destination-prefix": "192.168.0.0/16", + "next-hop": { + "next-hop-address": "10.0.0.1" + } + }] + }, + "ipv6": { + "route": [{ + "destination-prefix": "fd00::/64", + "next-hop": { + "next-hop-address": "fd00::1" + } + }, { + "destination-prefix": "2001:db8:3c4d:01::/64", + "next-hop": { + "next-hop-address": "fd00::1" + } + }] + } + } + }] + } + } + } + }) + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUTs"): + env = infamy.Env() + server = env.attach("server", "mgmt") + client1 = env.attach("client1", "mgmt") + client2 = env.attach("client2", "mgmt") + + _, hclient1 = env.ltop.xlate("host", "data1") + _, hserver = env.ltop.xlate("host", "data2") + + with test.step("Configure server, client1 and client2"): + util.parallel(configure_server(server), configure_client1(client1), configure_client2(client2)) + + with infamy.IsolatedMacVlan(hserver) as nsserver, infamy.IsolatedMacVlan(hclient1) as nsclient1: + nsserver.addip("192.168.0.1") + nsserver.addroute("default", "192.168.0.2"); + nsclient1.addip("192.168.1.1") + nsclient1.addroute("default", "192.168.1.2"); + nsserver.addip("2001:db8:3c4d:02::100", prefix_length=64, proto="ipv6") + nsserver.addroute("default", "2001:db8:3c4d:02::2", proto="ipv6") + nsclient1.addip("2001:db8:3c4d:01::100", prefix_length=64, proto="ipv6") + nsclient1.addroute("default", "2001:db8:3c4d:01::2", proto="ipv6") + + with test.step("Verify IPv4 connectivity from host to server through client1"): + util.parallel(nsclient1.must_reach("10.0.0.1"), + nsclient1.must_reach("10.0.0.2"), + nsclient1.must_reach("10.0.0.3")) + + with test.step("Verify IPv4 connectivity from host to client2 through WireGuard mesh"): + util.parallel(nsserver.must_reach("10.0.0.1"), + nsserver.must_reach("10.0.0.2")) + + with test.step("Verify host:data2 can not ping 10.0.0.3"): + nsserver.must_not_reach("10.0.0.3") # Not in allowed IPs + + with test.step("Verify IPv6 connectivity from host to server through client1"): + util.parallel(nsclient1.must_reach("fd00:0::1"), + nsclient1.must_reach("fd00:0::2"), + nsclient1.must_reach("fd00:0::3")) + + with test.step("Verify IPv6 connectivity from host to client2 through WireGuard mesh"): + util.parallel(nsserver.must_reach("fd00:0::1"), + nsserver.must_reach("fd00:0::2")) + + with test.step("Verify host:data2 can not ping fd00:0::3"): + nsserver.must_not_reach("fd00:0::3") # Not in allowed IPs + + + test.succeed() diff --git a/test/case/interfaces/wireguard_multipoint/topology.dot b/test/case/interfaces/wireguard_multipoint/topology.dot new file mode 100644 index 000000000..003992349 --- /dev/null +++ b/test/case/interfaces/wireguard_multipoint/topology.dot @@ -0,0 +1,43 @@ +graph "wireguard-multipoint" { + layout="neato"; + overlap="false"; + esep="+40"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt1 } | { data1 } | { data2 } | { mgmt2 } | { mgmt3 }" + pos="6,0!", + requires="controller", + ]; + + server [ + label="{ server } | { mgmt } | { data } | { link1 } | { link2 }" + pos="6, -6!", + requires="infix", + ]; + + client1 [ + label="{ link } | { data } | { mgmt } | { client1 }", + pos="0, -6!", + requires="infix", + ]; + + client2 [ + label="{ link } | { data } | { mgmt } | { client2 }", + pos="6,-12!", + requires="infix", + ]; + + host:mgmt1 -- server:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- client1:mgmt [requires="mgmt", color="lightgray"] + host:mgmt3 -- client2:mgmt [requires="mgmt", color="lightgray"] + + host:data1 -- client1:data [headlabel=".1", label="192.168.1.0/24" taillabel=".2 ", labeldistance=1, fontcolor="black", color="black"] + host:data2 -- server:data [headlabel=".1", label="192.168.0.0/24" taillabel=".2 ", labeldistance=1, fontcolor="black", color="black"] + + server:link1 -- client1:link [headlabel=".1", label="192.168.10.0/24", taillabel=".10 ", labeldistance=1, fontcolor="black", color="black"] + server:link2 -- client2:link [headlabel=".2\n\n", label="192.168.20.0/24", taillabel="\n.20", labeldistance=1, fontcolor="black", color="black"] + +} diff --git a/test/case/interfaces/wireguard_multipoint/topology.svg b/test/case/interfaces/wireguard_multipoint/topology.svg new file mode 100644 index 000000000..9086870a4 --- /dev/null +++ b/test/case/interfaces/wireguard_multipoint/topology.svg @@ -0,0 +1,113 @@ + + + + + + +wireguard-multipoint + + + +host + +host + +mgmt1 + +data1 + +data2 + +mgmt2 + +mgmt3 + + + +server + +server + +mgmt + +data + +link1 + +link2 + + + +host:mgmt1--server:mgmt + + + + +host:data2--server:data + +192.168.0.0/24 +.1 +.2  + + + +client1 + +link + +data + +mgmt + +client1 + + + +host:mgmt2--client1:mgmt + + + + +host:data1--client1:data + +192.168.1.0/24 +.1 +.2  + + + +client2 + +link + +data + +mgmt + +client2 + + + +host:mgmt3--client2:mgmt + + + + +server:link1--client1:link + +192.168.10.0/24 +.1 +.10  + + + +server:link2--client2:link + +192.168.20.0/24 +.2 +.20 + + + diff --git a/test/case/interfaces/wireguard_p2p/Readme.adoc b/test/case/interfaces/wireguard_p2p/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/wireguard_p2p/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/wireguard_p2p/test.adoc b/test/case/interfaces/wireguard_p2p/test.adoc new file mode 100644 index 000000000..7a8aa03ea --- /dev/null +++ b/test/case/interfaces/wireguard_p2p/test.adoc @@ -0,0 +1,47 @@ +=== Wireguard p2p + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wireguard_p2p] + +==== Description + +Set up a WireGuard tunnel between two DUTs with a host connected to the first +DUT. Enable IP forwarding on first DUT's interface to host and the WireGuard +tunnel interface to the second DUT. On host, add route to IP network of second +DUT and verify connectivity with the second DUT through the WireGuard tunnel. + +This test verifies: + +- WireGuard tunnel establishment between two peers +- Key management via ietf-keystore with X25519 keypairs +- IPv4 and IPv6 connectivity through the encrypted tunnel +- Proper routing through the WireGuard tunnel + +Topology: +.... + 192.168.50.0/24 + host:data ---- left:data left:link ---- right:link + 192.168.10.2/24 192.168.10.1 192.168.50.1 192.168.50.2 + \\ / + \\ / + \\ WireGuard + \\ Tunnel / + \\ / + left:wg0 right:wg0 + 10.0.0.1/32 10.0.0.2/32 + +.... + +==== Topology + +image::topology.svg[Wireguard p2p topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUTs +. Generate WireGuard keys +. Configure WireGuard tunnel on left DUT +. Configure WireGuard tunnel on right DUT +. Verify IPv4 connectivity through WireGuard tunnel +. Verify IPv6 connectivity through WireGuard tunnel + + diff --git a/test/case/interfaces/wireguard_p2p/test.py b/test/case/interfaces/wireguard_p2p/test.py new file mode 100755 index 000000000..f773f17c2 --- /dev/null +++ b/test/case/interfaces/wireguard_p2p/test.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +r""" +Basic WireGuard point-to-point tunnel test + +Set up a WireGuard tunnel between two DUTs with a host connected to the first +DUT. Enable IP forwarding on first DUT's interface to host and the WireGuard +tunnel interface to the second DUT. On host, add route to IP network of second +DUT and verify connectivity with the second DUT through the WireGuard tunnel. + +This test verifies: + +- WireGuard tunnel establishment between two peers +- Key management via ietf-keystore with X25519 keypairs +- IPv4 and IPv6 connectivity through the encrypted tunnel +- Proper routing through the WireGuard tunnel + +Topology: +.... + 192.168.50.0/24 + host:data ---- left:data left:link ---- right:link + 192.168.10.2/24 192.168.10.1 192.168.50.1 192.168.50.2 + \\ / + \\ / + \\ WireGuard + \\ Tunnel / + \\ / + left:wg0 right:wg0 + 10.0.0.1/32 10.0.0.2/32 + +.... + +""" + +import infamy + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUTs"): + env = infamy.Env() + left = env.attach("left", "mgmt") + right = env.attach("right", "mgmt") + + with test.step("Generate WireGuard keys"): + # WireGuard X25519 keys (base64-encoded 32-byte keys) + # These are example keys - in production, use `wg genkey` to generate + left_private_key = "EJPoi0BnccsfjEhKk0IWwNzJKXZKgS6XaKt+InYITVA=" + left_public_key = "xWVOEFUZZ5VI6t1fhZeISNyw7Ma/bY8INzIoaSSLlz8=" + + right_private_key = "UEaX13FTGhiIrnnKRd20KWh/vG6zqRIMSTzOP3hNs2s=" + right_public_key = "2pytpunN+e3V9e5asMXP+UqKoerFm08KWzcFYoWP41k=" + + with test.step("Configure WireGuard tunnel on left DUT"): + left.put_config_dicts({ + "ietf-keystore": { + "keystore": { + "asymmetric-keys": { + "asymmetric-key": [{ + "name": "left-wg-key", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": left_public_key, + "private-key-format": "infix-crypto-types:x25519-private-key-format", + "cleartext-private-key": left_private_key + }] + } + } + }, + "ietf-truststore": { + "truststore": { + "public-key-bags": { + "public-key-bag": [{ + "name": "wireguard-peers", + "public-key": [{ + "name": "right-wg-pubkey", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": right_public_key + }] + }] + } + } + }, + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": left["link"], + "ipv4": { + "address": [{ + "ip": "192.168.50.1", + "prefix-length": 24 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "2001:db8:3c4d:50::1", + "prefix-length": 64 + }], + "forwarding": True + } + }, { + "name": left["data"], + "ipv4": { + "address": [{ + "ip": "192.168.10.1", + "prefix-length": 24 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "2001:db8:3c4d:10::1", + "prefix-length": 64 + }], + "forwarding": True + } + }, { + "name": "wg0", + "type": "infix-if-type:wireguard", + "ipv4": { + "address": [{ + "ip": "10.0.0.1", + "prefix-length": 32 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "fd00::1", + "prefix-length": 64 + }], + "forwarding": True + }, + "wireguard": { + "listen-port": 51820, + "private-key": "left-wg-key", + "peer": [{ + "public-key-bag": "wireguard-peers", + "public-key": "right-wg-pubkey", + "endpoint": "192.168.50.2", + "endpoint-port": 51820, + "allowed-ips": ["10.0.0.0/24", "fd00::/64"] + }] + } + }] + } + } + }) + + with test.step("Configure WireGuard tunnel on right DUT"): + right.put_config_dicts({ + "ietf-keystore": { + "keystore": { + "asymmetric-keys": { + "asymmetric-key": [{ + "name": "right-wg-key", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": right_public_key, + "private-key-format": "infix-crypto-types:x25519-private-key-format", + "cleartext-private-key": right_private_key + }] + } + } + }, + "ietf-truststore": { + "truststore": { + "public-key-bags": { + "public-key-bag": [{ + "name": "wireguard-peers", + "public-key": [{ + "name": "left-wg-pubkey", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": left_public_key + }] + }] + } + } + }, + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": right["link"], + "ipv4": { + "address": [{ + "ip": "192.168.50.2", + "prefix-length": 24 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "2001:db8:3c4d:50::2", + "prefix-length": 64 + }] + } + }, { + "name": "wg0", + "type": "infix-if-type:wireguard", + "ipv4": { + "address": [{ + "ip": "10.0.0.2", + "prefix-length": 32 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "fd00::2", + "prefix-length": 64 + }] + }, + "wireguard": { + "listen-port": 51820, + "private-key": "right-wg-key", + "peer": [{ + "public-key-bag": "wireguard-peers", + "public-key": "left-wg-pubkey", + "endpoint": "192.168.50.1", + "endpoint-port": 51820, + "allowed-ips": ["10.0.0.0/24", "fd00::/64", "2001:db8:3c4d:10::/64", "192.168.10.0/24"] + }] + } + }] + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:static", + "name": "default", + "static-routes": { + "ipv4": { + "route": [{ + "destination-prefix": "192.168.10.0/24", + "next-hop": { + "next-hop-address": "10.0.0.1" + } + }] + }, + "ipv6": { + "route": [{ + "destination-prefix": "2001:db8:3c4d:10::/64", + "next-hop": { + "next-hop-address": "fd00::1" + } + }] + } + } + }] + } + } + } + }) + + _, hport = env.ltop.xlate("host", "data") + with test.step("Verify IPv4 connectivity through WireGuard tunnel"): + with infamy.IsolatedMacVlan(hport) as ns0: + ns0.addip("192.168.10.2") + ns0.addroute("10.0.0.0/24", "192.168.10.1") + ns0.must_reach("10.0.0.2") + + with test.step("Verify IPv6 connectivity through WireGuard tunnel"): + with infamy.IsolatedMacVlan(hport) as ns0: + ns0.addip("2001:db8:3c4d:10::2", prefix_length=64, proto="ipv6") + ns0.addroute("fd00::/64", "2001:db8:3c4d:10::1", proto="ipv6") + ns0.must_reach("fd00::2") + + test.succeed() diff --git a/test/case/interfaces/wireguard_p2p/topology.dot b/test/case/interfaces/wireguard_p2p/topology.dot new file mode 100644 index 000000000..3658b97db --- /dev/null +++ b/test/case/interfaces/wireguard_p2p/topology.dot @@ -0,0 +1,34 @@ +graph "wireguard-basic" { + layout="neato"; + overlap="false"; + esep="+40"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt1 } | { data } | { mgmt2 }" + pos="3,0!", + requires="controller", + ]; + + left [ + label="{ left } | { mgmt } | { data } | { link }", + pos="0, -3!", + + requires="infix", + ]; + + right [ + label="{ link } | { mgmt } | { right }", + pos="8,-3!", + + requires="infix", + ]; + + host:mgmt1 -- left:mgmt [requires="mgmt", color="lightgray"] + host:data -- left:data [headlabel=".1", label="192.168.10.0/24" taillabel=".2 ", labeldistance=1, fontcolor="black", color="black"] + host:mgmt2 -- right:mgmt [requires="mgmt", color="lightgray"] + + left:link -- right:link [headlabel=".1\n\n", label="192.168.50.0/24", taillabel="\n.2", labeldistance=1, fontcolor="black", color="black"] +} diff --git a/test/case/interfaces/wireguard_p2p/topology.svg b/test/case/interfaces/wireguard_p2p/topology.svg new file mode 100644 index 000000000..5b951047b --- /dev/null +++ b/test/case/interfaces/wireguard_p2p/topology.svg @@ -0,0 +1,72 @@ + + + + + + +wireguard-basic + + + +host + +host + +mgmt1 + +data + +mgmt2 + + + +left + +left + +mgmt + +data + +link + + + +host:mgmt1--left:mgmt + + + + +host:data--left:data + +192.168.10.0/24 +.1 +.2  + + + +right + +link + +mgmt + +right + + + +host:mgmt2--right:mgmt + + + + +left:link--right:link + +192.168.50.0/24 +.1 +.2 + + +