diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig index cd4a84c1f..67252d707 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 ffd4b045d..4451a6834 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/aarch32_defconfig b/configs/aarch32_defconfig index 029e15f44..b2f60003c 100644 --- a/configs/aarch32_defconfig +++ b/configs/aarch32_defconfig @@ -29,7 +29,7 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh" BR2_LINUX_KERNEL=y BR2_LINUX_KERNEL_CUSTOM_VERSION=y -BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.58" +BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.64" BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch32/linux_defconfig" BR2_LINUX_KERNEL_INSTALL_TARGET=y @@ -82,6 +82,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/configs/aarch32_minimal_defconfig b/configs/aarch32_minimal_defconfig index 4410a9137..51577a67b 100644 --- a/configs/aarch32_minimal_defconfig +++ b/configs/aarch32_minimal_defconfig @@ -82,6 +82,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/configs/aarch64_defconfig b/configs/aarch64_defconfig index e83ba3bfe..028f00250 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 b9d83f4ef..98c7a492f 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 ab35d43ae..f218cbd27 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 d97366a04..b72818583 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/ChangeLog.md b/doc/ChangeLog.md index db71a077e..d649d874f 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -19,6 +19,7 @@ All notable changes to the project are documented in this file. > > - WiFi Access Point (AP) mode support with multi-SSID capability > - RIPv2 routing support +> - WireGuard support ### Changes @@ -47,6 +48,7 @@ All notable changes to the project are documented in this file. moved to `wifi/station` container. Existing Wi-Fi interfaces will be removed during upgrade (for the rest of the configuration to apply) and you need to reconfigure them again. See [wifi.md](wifi.md) for details +- Add support for WireGuard VPN tunnels. ### Fixes diff --git a/doc/img/vpn-hub-spoke.svg b/doc/img/vpn-hub-spoke.svg new file mode 100644 index 000000000..59d51ea3a --- /dev/null +++ b/doc/img/vpn-hub-spoke.svg @@ -0,0 +1,983 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Headquarters + + + + HUB + + + + + + + + + + 192.168.0.0/24 + + + + + + + + Branch A + + + + Spoke + + + + + + + + + + 192.168.1.0/24 + + + + + + + + Branch B + + + + Spoke + + + + + + + + + + 192.168.2.0/24 + + + + + + + + Branch C + + + + Spoke + + + + + + + + + + 192.168.3.0/24 + + + + + + + Encrypted VPN + + + + + + Encrypted VPN + + + + + + Encrypted VPN + diff --git a/doc/img/vpn-roadwarrior.svg b/doc/img/vpn-roadwarrior.svg new file mode 100644 index 000000000..f0eefbd95 --- /dev/null +++ b/doc/img/vpn-roadwarrior.svg @@ -0,0 +1,689 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Unsecure + + Unsecure + + Unsecure + + Unsecure + + + + + + + + + + + + + + + + + + + + + + Encrypted VPN + + + + + + + Laptop + Remote Worker + + + + + + + Mobile + On-the-go + + + + + + + Tablet + Traveling + + + + + + + + Corporate + + 192.168.0.0/24 + + + + VPN Gateway + + + Servers + Files + Applications + + + + + + + diff --git a/doc/img/vpn-site-to-site.svg b/doc/img/vpn-site-to-site.svg new file mode 100644 index 000000000..ad8f14203 --- /dev/null +++ b/doc/img/vpn-site-to-site.svg @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Unsecure + + Unsecure + + + + + + + + + + + + Encrypted VPN Tunnel + + + + + + + Main Office + + 192.168.1.0/24 + + + + VPN Gateway + + + + + + + + + + 192.168.1.0/24 + + + + + + + + Branch Office + + 192.168.2.0/24 + + + + VPN Gateway + + + + + + + + + + 192.168.2.0/24 + + + + diff --git a/doc/img/wireguard-allowed-ips.svg b/doc/img/wireguard-allowed-ips.svg new file mode 100644 index 000000000..ed8d6e229 --- /dev/null +++ b/doc/img/wireguard-allowed-ips.svg @@ -0,0 +1,785 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Understanding WireGuard: Keys & allowed-ips + + + + + + + + Network A + 192.168.1.0/24 + Devices: .1, .2, .3... + + + + + + + WireGuard Peer A + + + wg0: 10.0.0.1/24 + + + PrivateKey: aaa...= + PublicKey: AAA...= + + [Peer] # Peer B + + PublicKey = BBB...= + allowed-ips = 10.0.0.2/32, + 192.168.2.0/24 + + + + + + + + + + Network B + 192.168.2.0/24 + Devices: .1, .2, .3... + + + + + + + WireGuard Peer B + + + wg0: 10.0.0.2/24 + + + PrivateKey: bbb...= + PublicKey: BBB...= + + [Peer] # Peer A + + PublicKey = AAA...= + allowed-ips = 10.0.0.1/32, + 192.168.1.0/24 + + + + + A's public key (AAA...=) + + B's public key (BBB...=) + + + + + + + WireGuard Tunnel + + + + + + to 192.168.2.x + + to 192.168.1.x + + diff --git a/doc/vpn-wireguard.md b/doc/vpn-wireguard.md new file mode 100644 index 000000000..1b8a8e2c9 --- /dev/null +++ b/doc/vpn-wireguard.md @@ -0,0 +1,437 @@ +# WireGuard VPN + +> [!NOTE] +> For a general introduction to VPN concepts and deployment models, see +> [VPN Configuration](vpn.md). + +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 + +> [!IMPORTANT] +> When issuing `leave` to activate your changes, remember to also save +> your settings, `copy running-config startup-config`. See the [CLI +> Introduction](cli/introduction.md) for a background. + +## 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 + +> [!TIP] +> If you name your WireGuard interface `wgN`, where `N` is a number, the +> CLI infers the interface type automatically. + +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/…/wg-peers/peer/peer-b/> set endpoint 203.0.113.2 +admin@siteA:/config/interface/…/wg-peers/peer/peer-b/> set endpoint-port 51820 +admin@siteA:/config/interface/…/wg-peers/peer/peer-b/> set allowed-ips 10.0.0.2/32 +admin@siteA:/config/interface/…/wg-peers/peer/peer-b/> set persistent-keepalive 25 +admin@siteA:/config/interface/…/wg-peers/peer/peer-b/> 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/…/wg-peers/peer/peer-a/> set endpoint 203.0.113.1 +admin@siteB:/config/interface/…/wg-peers/peer/peer-a/> set endpoint-port 51820 +admin@siteB:/config/interface/…/wg-peers/peer/peer-a/> set allowed-ips 10.0.0.1/32 +admin@siteB:/config/interface/…/wg-peers/peer/peer-a/> set persistent-keepalive 25 +admin@siteB:/config/interface/…/wg-peers/peer/peer-a/> 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. + +![WireGuard Keys and Allowed IPs](img/wireguard-allowed-ips.svg) +*Figure: WireGuard key exchange and allowed-ips configuration between two peers* + +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@example:/config/interface/…/wg-peers/peer/peer-a/> set allowed-ips 10.0.0.2/32 +admin@example:/config/interface/…/wg-peers/peer/peer-a/> 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. + +## Peer Configuration and Key Bags + +WireGuard peer configuration supports a two-level hierarchy that allows +efficient management of multiple peers with shared settings. + +**Public Key Bags** group related peers together (e.g., all mobile clients, +all branch offices) and allow you to configure default settings that apply +to all peers in the bag. Individual peers can then override these defaults +when needed. + +Settings that support bag-level defaults and per-peer overrides: + +- `endpoint` - Remote peer's IP address +- `endpoint-port` - Remote peer's UDP port (defaults to 51820 at bag level) +- `persistent-keepalive` - Keepalive interval in seconds +- `preshared-key` - Optional pre-shared key for additional quantum resistance +- `allowed-ips` - IP addresses allowed to/from this peer + +> [!IMPORTANT] +> **Key Bag Configuration Rules:** +> - **Single key in bag:** You can use bag-level settings (endpoint, allowed-ips, etc.) +> without specifying individual peer configurations. All settings apply to that one peer. +> - **Multiple keys in bag:** You MUST provide individual `peer` configuration for +> each key in the bag. Settings like `endpoint` and `allowed-ips` must be unique +> per peer and cannot be shared at the bag level when multiple peers exist. +> +> This prevents configuration errors where multiple peers would incorrectly share +> the same endpoint address or allowed-ips ranges. + +**Example with bag-level defaults:** + +``` +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 + +# Configure defaults for all peers in the 'branch-offices' bag +admin@example:/config/interface/wg0/> edit wireguard peers branch-offices +admin@example:/config/interface/…/wireguard/peers/branch-offices/> set endpoint-port 51820 +admin@example:/config/interface/…/wireguard/peers/branch-offices/> set persistent-keepalive 25 +admin@example:/config/interface/…/wireguard/peers/branch-offices/> end + +# Configure peer-specific settings (inherits endpoint-port and keepalive from bag) +admin@example:/config/interface/…/wireguard/peers/branch-offices/> edit peer office-east +admin@example:/config/interface/…/branch-offices/peer/office-east/> set endpoint 203.0.113.10 +admin@example:/config/interface/…/branch-offices/peer/office-east/> set allowed-ips 10.0.0.10/32 +admin@example:/config/interface/…/branch-offices/peer/office-east/> set allowed-ips 192.168.10.0/24 +admin@example:/config/interface/…/branch-offices/peer/office-east/> end + +# Another peer with an override for persistent-keepalive +admin@example:/config/interface/…/wireguard/peers/branch-offices/> edit peer office-west +admin@example:/config/interface/…/branch-offices/peer/office-west/> set endpoint 203.0.113.20 +admin@example:/config/interface/…/branch-offices/peer/office-west/> set allowed-ips 10.0.0.20/32 +admin@example:/config/interface/…/branch-offices/peer/office-west/> set allowed-ips 192.168.20.0/24 +admin@example:/config/interface/…/branch-offices/peer/office-west/> set persistent-keepalive 10 +admin@example:/config/interface/…/branch-offices/peer/office-west/> leave +admin@example:/> +``` + +In this example: +- Both peers inherit `endpoint-port 51820` and `persistent-keepalive 25` from the bag +- `office-west` overrides the keepalive to 10 seconds while `office-east` uses the default 25 +- Each peer has its own `endpoint` and `allowed-ips` configuration + +This approach simplifies management when you have many peers with similar +configurations - set the common defaults once at the bag level, then only +specify per-peer differences. + +## Site-to-Site VPN + +![Site-to-Site VPN Topology](img/vpn-site-to-site.svg) +*Figure: Site-to-Site VPN connecting two office networks* + +A site-to-site VPN connects entire networks across locations, creating a unified +private network over the internet. This allows devices in one location to +seamlessly access resources in another as if they were on the same local network. + +This is the point-to-point configuration shown earlier, extended with routing +to allow access to networks behind each peer. Configure the WireGuard tunnel +as shown in [Point-to-Point Configuration](#point-to-point-configuration), +then add the remote network to `allowed-ips` and configure static routes. + +## Road Warrior VPN + +![Road Warrior VPN Topology](img/vpn-roadwarrior.svg) +*Figure: Mobile clients connecting to corporate network* + +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 peers wg-peers peer mobile-client +admin@hub:/config/interface/…/wg-peers/peer/mobile-client/> set allowed-ips 10.0.0.10/32 +admin@hub:/config/interface/…/wg-peers/peer/mobile-client/> 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. + +## Hub-and-Spoke Topology + +![Hub-and-Spoke VPN Topology](img/vpn-hub-spoke.svg) +*Figure: Hub-and-Spoke topology with central hub routing traffic between spokes* + +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 ipv4 address 10.0.0.1 prefix-length 24 +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/> edit wireguard peers wg-peers + +# Spoke 1 +admin@hub:/config/interface/…/wireguard/peers/wg-peers/> edit peer spoke1 +admin@hub:/config/interface/…/wg-peers/peer/spoke1/> set allowed-ips 10.0.0.2/32 +admin@hub:/config/interface/…/wg-peers/peer/spoke1/> set allowed-ips 192.168.1.0/24 +admin@hub:/config/interface/…/wg-peers/peer/spoke1/> end + +# Spoke 2 +admin@hub:/config/interface/…/wireguard/peers/wg-peers/> edit peer spoke2 +admin@hub:/config/interface/…/wg-peers/peer/spoke2/> set allowed-ips 10.0.0.3/32 +admin@hub:/config/interface/…/wg-peers/peer/spoke2/> set allowed-ips 192.168.2.0/24 +admin@hub:/config/interface/…/wg-peers/peer/spoke2/> leave + +# Add routes for spoke networks +admin@hub:/> configure +admin@hub:/config/> edit routing control-plane-protocol static name default +admin@hub:/config/routing/…/static/name/default/> set ipv4 route 192.168.1.0/24 wg0 +admin@hub:/config/routing/…/static/name/default/> set ipv4 route 192.168.2.0/24 wg0 +admin@hub:/config/routing/…/static/name/default/> 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 peers wg-peers peer hub +admin@spoke1:/config/interface/…/wg-peers/peer/hub/> set endpoint 203.0.113.1 +admin@spoke1:/config/interface/…/wg-peers/peer/hub/> set endpoint-port 51820 +admin@spoke1:/config/interface/…/wg-peers/peer/hub/> set allowed-ips 10.0.0.1/32 +admin@spoke1:/config/interface/…/wg-peers/peer/hub/> set allowed-ips 10.0.0.3/32 +admin@spoke1:/config/interface/…/wg-peers/peer/hub/> set allowed-ips 192.168.0.0/24 +admin@spoke1:/config/interface/…/wg-peers/peer/hub/> set allowed-ips 192.168.2.0/24 +admin@spoke1:/config/interface/…/wg-peers/peer/hub/> set persistent-keepalive 25 +admin@spoke1:/config/interface/…/wg-peers/peer/hub/> leave +admin@spoke1:/> configure +admin@spoke1:/config/> edit routing control-plane-protocol static name default +admin@spoke1:/config/routing/…/static/name/default/> set ipv4 route 192.168.0.0/24 wg0 +admin@spoke1:/config/routing/…/static/name/default/> set ipv4 route 192.168.2.0/24 wg0 +admin@spoke1:/config/routing/…/static/name/default/> 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/…/wg-peers/peer/hub/> 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 peers wg-peers peer remote +admin@example:/config/interface/…/wg-peers/peer/remote/> set endpoint 2001:db8::2 +admin@example:/config/interface/…/wg-peers/peer/remote/> set endpoint-port 51820 +admin@example:/config/interface/…/wg-peers/peer/remote/> set allowed-ips 10.0.0.2/32 +admin@example:/config/interface/…/wg-peers/peer/remote/> set allowed-ips fd00::2/128 +admin@example:/config/interface/…/wg-peers/peer/remote/> leave +admin@example:/> +``` + +WireGuard can carry both IPv4 and IPv6 traffic regardless of whether the +tunnel endpoints use IPv4 or IPv6. + +## 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) + +WireGuard supports optional preshared keys (PSK) that add an extra layer of +symmetric encryption alongside Curve25519. This provides defense-in-depth +against future quantum computers that might break elliptic curve cryptography. + +PSKs protect your data from "harvest now, decrypt later" attacks - adversaries +recording traffic today would still need the PSK even if they break Curve25519 +later. However, peer authentication still relies on Curve25519, so PSKs don't +provide complete post-quantum security. + +**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/interface/wg0/> edit wireguard peers wg-peers peer remote +admin@example:/config/interface/…/wg-peers/peer/remote/> set preshared-key wg-psk +admin@example:/config/interface/…/wg-peers/peer/remote/> leave +admin@example:/> +``` + +The preshared key must be securely shared between both peers and configured +on both sides. + +> [!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/doc/vpn.md b/doc/vpn.md new file mode 100644 index 000000000..d17456c2e --- /dev/null +++ b/doc/vpn.md @@ -0,0 +1,83 @@ +# VPN Configuration + +A Virtual Private Network (VPN) creates encrypted tunnels over public networks, +enabling secure communication between remote locations or users. Unlike plain +tunnels (GRE, VXLAN) that only provide encapsulation, VPNs add authentication +and encryption to protect data confidentiality and integrity. + + +## Configuring VPN + +For detailed configuration instructions and examples, see: + +- **[WireGuard VPN](vpn-wireguard.md)** - Complete guide to configuring + WireGuard tunnels, including site-to-site, road warrior, and hub-and-spoke + topologies. + +## Understanding VPN Tunnels + +VPN tunnels establish secure connections across untrusted networks by: + +- **Authentication:** Verifying the identity of tunnel endpoints using + cryptographic keys or certificates +- **Encryption:** Protecting data confidentiality with strong ciphers +- **Integrity:** Detecting tampering through message authentication codes + +This makes VPNs essential for connecting sites over the internet, enabling +remote access for mobile users, and securing traffic in untrusted environments. + +### VPN Deployment Models + +VPNs are typically deployed in one of several models: + +**Site-to-Site VPN** + +![Site-to-Site VPN Topology](img/vpn-site-to-site.svg) +*Figure: Site-to-Site VPN connecting two office networks* + +Connects entire networks across locations, creating a unified private network +over the internet. Routers or firewalls at each site maintain persistent +tunnels, allowing seamless access between locations. + +- Use case: Connecting branch offices to headquarters +- Characteristics: Always-on, connects networks not individual devices +- Example: Main office (192.168.1.0/24) ↔ Branch office (192.168.2.0/24) + +**Remote Access VPN (Road Warrior)** + +![Road Warrior VPN Topology](img/vpn-roadwarrior.svg) +*Figure: Mobile clients connecting to corporate network* + +Enables individual users to securely access a private network from remote +locations. Clients initiate connections as needed from dynamic IP addresses. + +- Use case: Remote employees accessing corporate resources +- Characteristics: On-demand, handles dynamic endpoints and roaming +- Example: Mobile laptop ↔ Corporate network + +**Hub-and-Spoke VPN** + +![Hub-and-Spoke VPN Topology](img/vpn-hub-spoke.svg) +*Figure: Hub-and-Spoke topology with central hub routing traffic between spokes* + +A central hub connects to multiple remote sites (spokes), routing traffic +between them. Spokes don't connect directly to each other but communicate +through the hub. + +- Use case: Central office connecting multiple remote locations +- Characteristics: Centralized control, simplified management +- Example: HQ ↔ (Branch A, Branch B, Branch C) + +### VPN Protocol Comparison + +Different VPN protocols offer varying trade-offs between security, performance, +and complexity: + +| Protocol | Complexity | Performance | Use Case | +|------------|------------|-------------|---------------------------------| +| WireGuard | Simple | Very High | Modern deployments, all models | +| IPsec | Complex | High | Legacy systems, compliance reqs | +| OpenVPN | Moderate | Moderate | Maximum compatibility | + +Infix supports WireGuard as its primary VPN solution, offering the best +balance of simplicity, security, and performance for modern networks. diff --git a/doc/wifi.md b/doc/wifi.md index 155b484eb..ddb838025 100644 --- a/doc/wifi.md +++ b/doc/wifi.md @@ -185,33 +185,6 @@ In the CLI, signal strength is reported as: excellent, good, fair or bad. For precise RSSI values in dBm, use NETCONF or RESTCONF to access the operational datastore directly. -### Connect to a Network - -Once you've identified the desired network from the scan results, configure -station mode with the SSID and credentials. First, store your WiFi password -in the keystore: - -``` -admin@example:/> configure -admin@example:/config/> edit keystore symmetric-key my-wifi-key -admin@example:/config/keystore/…/my-wifi-key/> set key-format wifi-preshared-key-format -admin@example:/config/keystore/…/my-wifi-key/> set symmetric-key YourWiFiPassword -admin@example:/config/keystore/…/my-wifi-key/> leave -``` - -Then configure the WiFi interface for station mode: - -``` -admin@example:/> configure -admin@example:/config/> edit interface wifi0 -admin@example:/config/interface/wifi0/> set wifi station ssid MyNetwork -admin@example:/config/interface/wifi0/> set wifi station security secret my-wifi-key -admin@example:/config/interface/wifi0/> leave -``` - -The interface will transition from scan-only mode to station mode and -attempt to connect to the specified network. - ## Station Mode (Client) Station mode connects to an existing Wi-Fi network. Before configuring station diff --git a/mkdocs.yml b/mkdocs.yml index 6aae8a6f0..dd8e5c2c5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - Quality of Service: qos.md - RMON Counters: eth-counters.md - Tunneling (L2/L3): tunnels.md + - VPN Tunnels: vpn.md - Wireless LAN (WiFi): wifi.md - Services: - Device Discovery: discovery.md diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 1d379e698..08bf1cb73 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 \ ntp.c \ diff --git a/src/confd/src/if-wireguard.c b/src/confd/src/if-wireguard.c new file mode 100644 index 000000000..7552885aa --- /dev/null +++ b/src/confd/src/if-wireguard.c @@ -0,0 +1,161 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +#include + +#include "interfaces.h" + +#define WIREGUARD_CONFIG "/run/wireguard-%s.conf" + +/* Helper to get a peer setting with override logic: + * 1. Check peer-specific override + * 2. Fall back to key-bag level default + */ +static const char *get_peer_setting(struct lyd_node *peer_override, struct lyd_node *bag_peer, + const char *setting_name) +{ + const char *value = NULL; + + if (peer_override) + value = lydx_get_cattr(peer_override, setting_name); + if (!value) + value = lydx_get_cattr(bag_peer, setting_name); + + return value; +} + +static void write_allowed_ips(FILE *wg_fp, struct lyd_node *peer_override, struct lyd_node *bag_peer) +{ + struct lyd_node *settings_node, *allowed_ip, *check_ip; + int first = 1; + + /* Determine which node has the allowed-ips to use: + * If peer override exists and has any allowed-ips, use those. + * Otherwise use bag-level allowed-ips. + */ + settings_node = bag_peer; /* Default to bag level */ + if (peer_override && lyd_child(peer_override)) { + LYX_LIST_FOR_EACH(lyd_child(peer_override), check_ip, "allowed-ips") { + settings_node = peer_override; /* Override has IPs, use them */ + break; + } + } + + LYX_LIST_FOR_EACH(lyd_child(settings_node), 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"); +} + +static void write_peer(FILE *wg_fp, struct lyd_node *cif, struct lyd_node *bag_peer, + struct lyd_node *peer_override, const char *public_key_data) +{ + const char *preshared_key_ref, *preshared_key_data; + const char *endpoint, *endpoint_port, *keepalive; + struct lyd_node *psk_node; + + fprintf(wg_fp, "\n[Peer]\n"); + fprintf(wg_fp, "PublicKey = %s\n", public_key_data); + + preshared_key_ref = get_peer_setting(peer_override, bag_peer, "preshared-key"); + endpoint = get_peer_setting(peer_override, bag_peer, "endpoint"); + endpoint_port = get_peer_setting(peer_override, bag_peer, "endpoint-port"); + keepalive = get_peer_setting(peer_override, bag_peer, "persistent-keepalive"); + + if (preshared_key_ref) { + psk_node = lydx_get_xpathf(cif, "../../keystore/symmetric-keys/symmetric-key[name='%s']", + preshared_key_ref); + if (psk_node) { + preshared_key_data = lydx_get_cattr(psk_node, "cleartext-symmetric-key"); + if (preshared_key_data) + fprintf(wg_fp, "PresharedKey = %s\n", preshared_key_data); + } + } + + if (endpoint) { + if (!endpoint_port) + endpoint_port = "51820"; /* Default port */ + fprintf(wg_fp, "Endpoint = %s:%s\n", endpoint, endpoint_port); + } + + write_allowed_ips(wg_fp, peer_override, bag_peer); + + if (keepalive) + fprintf(wg_fp, "PersistentKeepalive = %s\n", keepalive); +} + + +int wireguard_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip, struct dagger *net) +{ + const char *ifname, *listen_port, *private_key_ref, *private_key_data; + struct lyd_node *wg, *key_node, *bag_peer, *pub_key_bag; + FILE *wg_fp = NULL; + FILE *wg_sh = NULL; + mode_t old_umask; + + ifname = lydx_get_cattr(cif, "name"); + wg = lydx_get_child(cif, "wireguard"); + if (!wg) + return -EINVAL; + + listen_port = lydx_get_cattr(wg, "listen-port"); + 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); + + /* Set umask to create config file with limited permissions (0600) */ + old_umask = umask(0177); + wg_fp = fopenf("w", WIREGUARD_CONFIG, ifname); + umask(old_umask); + 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), bag_peer, "peers") { + const char *public_key_bag_ref = lydx_get_cattr(bag_peer, "public-key-bag"); + + pub_key_bag = lydx_get_xpathf(cif, "../../truststore/public-key-bags/public-key-bag[name='%s']", + public_key_bag_ref); + if (pub_key_bag) { + struct lyd_node *pub_key, *peer_override; + const char *public_key_name, *public_key_data; + + LYX_LIST_FOR_EACH(lyd_child(pub_key_bag), pub_key, "public-key") { + public_key_name = lydx_get_cattr(pub_key, "name"); + public_key_data = lydx_get_cattr(pub_key, "public-key"); + + if (!public_key_data) + continue; + + /* Check if there's a peer-specific override for this key */ + peer_override = lydx_get_xpathf(bag_peer, "peer[public-key='%s']", public_key_name); + + write_peer(wg_fp, cif, bag_peer, peer_override, public_key_data); + } + } + } + + fclose(wg_fp); + + /* Create activation script */ + 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"); + + fclose(wg_sh); + + return 0; +} diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index 6c33659bd..7df74980f 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_add_iface(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: @@ -457,6 +461,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; @@ -494,6 +499,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")); @@ -579,6 +586,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; @@ -744,6 +752,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 c26010c16..3e7bcfd6e 100644 --- a/src/confd/src/interfaces.h +++ b/src/confd/src/interfaces.h @@ -33,6 +33,7 @@ _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 { @@ -152,4 +153,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 bb136003d..c8941cbe5 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -30,7 +30,7 @@ MODULES=( "infix-hardware@2025-12-04.yang" "ieee802-dot1q-types@2022-10-29.yang" "infix-ip@2025-11-02.yang" - "infix-if-type@2025-02-12.yang" + "infix-if-type@2026-01-07.yang" "infix-routing@2025-12-02.yang" "ieee802-dot1ab-lldp@2022-03-15.yang" "infix-lldp@2025-05-05.yang" @@ -49,8 +49,8 @@ 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-12-10.yang" "infix-ntp@2025-12-03.yang" + "infix-keystore@2025-12-17.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-type.yang b/src/confd/yang/confd/infix-if-type.yang index dd937609b..052e6220c 100644 --- a/src/confd/yang/confd/infix-if-type.yang +++ b/src/confd/yang/confd/infix-if-type.yang @@ -11,6 +11,11 @@ module infix-if-type { contact "kernelkit@googlegroups.com"; description "Infix extensions to IANA interfaces types"; + revision 2026-01-07 { + description "Add interface type wifi and wireguard"; + reference "internal"; + } + revision 2025-02-12 { description "Remove interface type etherlike."; reference "internal"; @@ -81,6 +86,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-type@2025-02-12.yang b/src/confd/yang/confd/infix-if-type@2026-01-07.yang similarity index 100% rename from src/confd/yang/confd/infix-if-type@2025-02-12.yang rename to src/confd/yang/confd/infix-if-type@2026-01-07.yang 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..6afb3c17c --- /dev/null +++ b/src/confd/yang/confd/infix-if-wireguard.yang @@ -0,0 +1,309 @@ +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."; + } + + grouping peer-settings { + description + "Common WireGuard peer configuration settings. + + These settings can be applied at the key-bag level (affecting all + keys in the bag) or at individual peer level (overriding key-bag + settings for specific keys)."; + + leaf preshared-key { + type ks:central-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(.)/../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; + description "Peer endpoint UDP port"; + } + + leaf-list allowed-ips { + type inet:ip-prefix; + 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. + + IMPORTANT: Allowed-IPs must be unique across all peers. If + multiple peers have overlapping allowed-ips, only the last + peer will be used for routing. When configuring at the + key-bag level, only use allowed-ips if all peers genuinely + need identical routing (rare). For typical hub-and-spoke + scenarios, use per-peer configuration with unique allowed-ips + for each client (e.g., 10.0.0.2/32, 10.0.0.3/32, etc.)."; + } + + 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."; + } + } + + 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:central-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 peers { + key "public-key-bag"; + description + "Peers from a public key bag with common settings. + + If the bag contains a single key, bag-level settings (endpoint, + allowed-ips, etc.) apply to that peer. If the bag contains multiple + keys, individual peer configuration is required for each key - you + must provide a 'peer' entry for each public key in the bag with + unique settings like endpoint and allowed-ips."; + + must "count(../../../../ts:truststore/ts:public-key-bags/ts:public-key-bag[ts:name=current()/public-key-bag]/ts:public-key) = 1 or " + + "count(./peer) = count(../../../../ts:truststore/ts:public-key-bags/ts:public-key-bag[ts:name=current()/public-key-bag]/ts:public-key)" { + error-message "When a key-bag contains multiple keys, individual peer " + + "configuration is required for each key. Either use a " + + "key-bag with a single key for shared settings, or provide " + + "a 'peer' entry for each key in the bag with unique settings."; + } + + leaf public-key-bag { + type ts:central-public-key-bag-ref; + 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"; + } + description "Reference to public key bag containing peer public keys"; + } + + uses peer-settings { + refine endpoint-port { + default 51820; + } + } + + list peer { + key "public-key"; + description + "Individual peer with override settings. + + Settings specified here override the key-bag level settings + for this specific peer."; + + leaf public-key { + type leafref { + path "../../../../../../ts:truststore/ts:public-key-bags/" + + "ts:public-key-bag[ts:name=current()/../../public-key-bag]/" + + "ts:public-key/ts:name"; + } + description "Name of the peer's public key within the referenced key-bag"; + } + + uses peer-settings; + } + } + + 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 405cf8326..097b581dc 100644 --- a/src/confd/yang/confd/infix-interfaces.yang +++ b/src/confd/yang/confd/infix-interfaces.yang @@ -34,6 +34,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 f94a362c3..ec7536d6f 100644 --- a/src/confd/yang/confd/infix-keystore.yang +++ b/src/confd/yang/confd/infix-keystore.yang @@ -12,6 +12,9 @@ module infix-keystore { prefix infix-ct; } + revision 2025-12-17 { + description "Add WireGuard support"; + } revision 2025-12-10 { description "Adapt to changes in final version of ietf-keystore"; } @@ -36,18 +39,10 @@ module infix-keystore { } } deviation "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-format" { - deviate not-supported; - } - augment "/ks:keystore/ks:symmetric-keys/ks:symmetric-key" { - leaf key-format { + deviate replace { type identityref { base infix-ct:symmetric-key-format; } - description - "Identifies the symmetric key's format - - Valid symmetric key formats are: - wifi-preshared-key-format - WiFi preshared key"; } } deviation "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-type/ks:cleartext-symmetric-key" { @@ -61,7 +56,16 @@ module infix-keystore { "(string-length(.) >= 8 and string-length(.) <= 63)" { error-message "WiFi pre-shared key must be 8-63 characters long"; } - description "WiFi pre-shared key: 8-63 printable ASCII characters"; + must "../infix-ks:key-format != 'infix-ct:wireguard-symmetric-key-format' or " + + "string-length(.) = 44" { + error-message "WireGuard pre-shared key must be 44 characters (32-byte base64-encoded)"; + } + description + "Cleartext symmetric key value. + + Format depends on key-format: + - WiFi pre-shared key: 8-63 printable ASCII characters + - WireGuard pre-shared key: 32-byte base64-encoded key (44 chars with padding)"; } } diff --git a/src/confd/yang/confd/infix-keystore@2025-12-10.yang b/src/confd/yang/confd/infix-keystore@2025-12-17.yang similarity index 100% rename from src/confd/yang/confd/infix-keystore@2025-12-10.yang rename to src/confd/yang/confd/infix-keystore@2025-12-17.yang diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 80e3379e1..b1e25cd6e 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -1114,6 +1114,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) @@ -1148,6 +1149,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": @@ -1219,6 +1223,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) @@ -1471,6 +1492,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() @@ -1598,9 +1625,6 @@ def pr_iface(self): print(f"{'in-octets':<{20}}: {self.in_octets}") print(f"{'out-octets':<{20}}: {self.out_octets}") - frame = get_json_data([], self.data,'ieee802-ethernet-interface:ethernet', - 'statistics', 'frame') - if self.wifi: # Detect mode: AP has "stations", Station has "rssi" or "scan-results" ap = self.wifi.get('access-point') @@ -1635,6 +1659,46 @@ 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") + + + frame = get_json_data([], self.data,'ieee802-ethernet-interface:ethernet', + 'statistics', 'frame') if frame: print("") for key, val in frame.items(): @@ -1799,6 +1863,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_hardware.py b/src/statd/python/yanger/ietf_hardware.py index 75e530791..74bf46cca 100644 --- a/src/statd/python/yanger/ietf_hardware.py +++ b/src/statd/python/yanger/ietf_hardware.py @@ -668,7 +668,7 @@ def wifi_radio_components(): def operational(): - systemjson = HOST.read_json("/run/system.json") + systemjson = HOST.read_json("/run/system.json", {}) return { "ietf-hardware:hardware": { diff --git a/src/statd/python/yanger/ietf_interfaces/ethernet.py b/src/statd/python/yanger/ietf_interfaces/ethernet.py index 8e3c04045..c2b3821c8 100644 --- a/src/statd/python/yanger/ietf_interfaces/ethernet.py +++ b/src/statd/python/yanger/ietf_interfaces/ethernet.py @@ -89,6 +89,9 @@ def link(ifname): def ethernet(iplink): eth = link(iplink["ifname"]) + if eth is None: + eth = {} + if stats := statistics(iplink["ifname"]): eth["statistics"] = stats 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..270b57ecc 100644 --- a/test/case/interfaces/tunnels.yaml +++ b/test/case/interfaces/tunnels.yaml @@ -7,3 +7,12 @@ - name: Tunnel TTL verification suite: tunnel_ttl/test.yaml + +- name: WireGuard Point-to-Point + case: wireguard_p2p/test.py + +- name: WireGuard multipoint + case: wireguard_multipoint/test.py + +- name: WireGuard Roadwarrior + case: wireguard_roadwarrior/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..265ac3fe3 --- /dev/null +++ b/test/case/interfaces/wireguard_multipoint/test.adoc @@ -0,0 +1,57 @@ +=== 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 + +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: +- host:data1 can reach all WireGuard IPs (10.0.0.1, .2, .3 and fd00:0::1, ::2, ::3) +- host:data2 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 with ping 10.0.0.1, 10.0.0.2 and 10.0.0.3 from host:data1 +. Verify IPv4 connectivity with ping 10.0.0.1 and 10.0.0.2 from host:data2 +. Verify host:data2 can not ping 10.0.0.3 +. Verify IPv6 connectivity with ping fd00:0::1, fd00:0::2 and fd00:0::3 from host:data1 +. Verify IPv6 connectivity with ping fd00:0:1 and fd00:0:2 from host:data2 +. 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..bb9e86645 --- /dev/null +++ b/test/case/interfaces/wireguard_multipoint/test.py @@ -0,0 +1,512 @@ +#!/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 + +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: +- host:data1 can reach all WireGuard IPs (10.0.0.1, .2, .3 and fd00:0::1, ::2, ::3) +- host:data2 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", + "infix-keystore:symmetric-key": psk_client1, + "key-format": "infix-crypto-types:wireguard-symmetric-key-format" + }, { + "name": "psk-client2", + "infix-keystore:symmetric-key": psk_client2, + "key-format": "infix-crypto-types:wireguard-symmetric-key-format" + }] + } + } + }, + "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", + "peers": [{ + "public-key-bag": "wireguard-clients", + "persistent-keepalive": 3, + "peer": [{ + "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"] + }, { + "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": 5 + }] + }] + } + }] + } + }, + "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", + "infix-keystore:symmetric-key": psk_client1, + "key-format": "infix-crypto-types:wireguard-symmetric-key-format" + }] + } + } + }, + "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", + "peers": [{ + "public-key-bag": "wireguard-server", + "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": 3 + }] + } + }] + } + }, + "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", + "infix-keystore:symmetric-key": psk_client2, + "key-format": "infix-crypto-types:wireguard-symmetric-key-format" + }] + } + } + }, + "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", + "peers": [{ + "public-key-bag": "wireguard-server", + "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": 5 + }] + } + }] + } + }, + "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 with ping 10.0.0.1, 10.0.0.2 and 10.0.0.3 from host:data1"): + 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 with ping 10.0.0.1 and 10.0.0.2 from host:data2"): + 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 with ping fd00:0::1, fd00:0::2 and fd00:0::3 from host:data1"): + 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 with ping fd00:0:1 and fd00:0:2 from host:data2"): + 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..006ffeb45 --- /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=" { data1 } | { mgmt2 } | { mgmt1 } | host | { data2 } | { mgmt3 }" + pos="6,0!", + requires="controller", + ]; + + server [ + label=" { link1 } | { server } | { mgmt } | { data } | { link2 }" + pos="6, -6!", + requires="infix", + ]; + + client1 [ + label="{ { data } | { mgmt } | { client1 } | link } ", + pos="0, -6!", + requires="infix", + ]; + + client2 [ + label="{ { data } | { mgmt } | { client2 } | link } ", + pos="12,-6!", + 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..06e0c0318 --- /dev/null +++ b/test/case/interfaces/wireguard_multipoint/topology.svg @@ -0,0 +1,113 @@ + + + + + + +wireguard-multipoint + + + +host + +data1 + +mgmt2 + +mgmt1 + +host + +data2 + +mgmt3 + + + +server + +link1 + +server + +mgmt + +data + +link2 + + + +host:mgmt1--server:mgmt + + + + +host:data2--server:data + +192.168.0.0/24 +.1 +.2  + + + +client1 + +data + +mgmt + +client1 + +link + + + +host:mgmt2--client1:mgmt + + + + +host:data1--client1:data + +192.168.1.0/24 +.1 +.2  + + + +client2 + +data + +mgmt + +client2 + +link + + + +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..4cff4afc1 --- /dev/null +++ b/test/case/interfaces/wireguard_p2p/test.adoc @@ -0,0 +1,45 @@ +=== WireGuard Point-to-Point + +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 Point-to-Point topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUTs +. Configure WireGuard tunnel on DUTs +. Verify IPv4 connectivity with ping 10.0.0.2 from host:data +. Verify IPv6 connectivity with ping fd00::2 from host:data + + diff --git a/test/case/interfaces/wireguard_p2p/test.py b/test/case/interfaces/wireguard_p2p/test.py new file mode 100755 index 000000000..33081e288 --- /dev/null +++ b/test/case/interfaces/wireguard_p2p/test.py @@ -0,0 +1,260 @@ +#!/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 +import infamy.util as util + +left_private_key = "EJPoi0BnccsfjEhKk0IWwNzJKXZKgS6XaKt+InYITVA=" +left_public_key = "xWVOEFUZZ5VI6t1fhZeISNyw7Ma/bY8INzIoaSSLlz8=" + +right_private_key = "UEaX13FTGhiIrnnKRd20KWh/vG6zqRIMSTzOP3hNs2s=" +right_public_key = "2pytpunN+e3V9e5asMXP+UqKoerFm08KWzcFYoWP41k=" + +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("Configure WireGuard tunnel on DUTs"): + util.parallel(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": 24 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "fd00::1", + "prefix-length": 64 + }], + "forwarding": True + }, + "wireguard": { + "listen-port": 51820, + "private-key": "left-wg-key", + "peers": [{ + "public-key-bag": "wireguard-peers", + "endpoint": "192.168.50.2", + "endpoint-port": 51820, + "allowed-ips": ["10.0.0.0/24", "fd00::/64"] + }] + } + }] + } + } + }), + 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": 24 + }], + "forwarding": True + }, + "ipv6": { + "address": [{ + "ip": "fd00::2", + "prefix-length": 64 + }] + }, + "wireguard": { + "listen-port": 51820, + "private-key": "right-wg-key", + "peers": [{ + "public-key-bag": "wireguard-peers", + "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 with ping 10.0.0.2 from host:data"): + 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 with ping fd00::2 from host:data"): + 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 + + + diff --git a/test/case/interfaces/wireguard_roadwarrior/Readme.adoc b/test/case/interfaces/wireguard_roadwarrior/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/wireguard_roadwarrior/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/wireguard_roadwarrior/test.adoc b/test/case/interfaces/wireguard_roadwarrior/test.adoc new file mode 100644 index 000000000..21052b91b --- /dev/null +++ b/test/case/interfaces/wireguard_roadwarrior/test.adoc @@ -0,0 +1,56 @@ +=== WireGuard Roadwarrior + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wireguard_roadwarrior] + +==== Description + +This test demonstrates a realistic roadwarrior scenario where clients +connect through an internet router to reach a VPN server. + +Set up a WireGuard server with 2 roadwarrior clients connecting through +a router that simulates the internet. Each client is on a different subnet +behind the router, and has a private network that should be accessible +through the VPN tunnel. + +This test verifies: + +- WireGuard tunnel establishment through an intermediate router +- Roadwarrior clients on different subnets +- Access to server's private network through the VPN tunnel +- Routing between client networks and server network via WireGuard + +Topology: +.... + Server ---- Router ---- Client1 + (VPN) (Internet) \---- Client2 + + WAN: 192.168.100.0/24 + Server: 192.168.100.1 + Router: 192.168.100.2 + + Client1 link: 192.168.50.0/24 (Router LAN1) + Client2 link: 192.168.51.0/24 (Router LAN2) + + WireGuard tunnel: 10.0.0.0/24 + Server: 10.0.0.1/24 + Client1: 10.0.0.2/24 + Client2: 10.0.0.3/24 + + Backend networks: + Server data: 192.168.0.0/24 + Client1 data: 192.168.1.0/24 + Client2 data: 192.168.2.0/24 +.... + +==== Topology + +image::topology.svg[WireGuard Roadwarrior topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUTs +. Configure DUTs +. Check on the server that both clients is connected +. Verify IPv4 connectivity with ping 192.168.0.2 from host:data1 and host:data2 + + diff --git a/test/case/interfaces/wireguard_roadwarrior/test.py b/test/case/interfaces/wireguard_roadwarrior/test.py new file mode 100755 index 000000000..4ed4b7e0d --- /dev/null +++ b/test/case/interfaces/wireguard_roadwarrior/test.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +r""" +WireGuard roadwarrior test with router simulating internet + +This test demonstrates a realistic roadwarrior scenario where clients +connect through an internet router to reach a VPN server. + +Set up a WireGuard server with 2 roadwarrior clients connecting through +a router that simulates the internet. Each client is on a different subnet +behind the router, and has a private network that should be accessible +through the VPN tunnel. + +This test verifies: + +- WireGuard tunnel establishment through an intermediate router +- Roadwarrior clients on different subnets +- Access to server's private network through the VPN tunnel +- Routing between client networks and server network via WireGuard + +Topology: +.... + Server ---- Router ---- Client1 + (VPN) (Internet) \---- Client2 + + WAN: 192.168.100.0/24 + Server: 192.168.100.1 + Router: 192.168.100.2 + + Client1 link: 192.168.50.0/24 (Router LAN1) + Client2 link: 192.168.51.0/24 (Router LAN2) + + WireGuard tunnel: 10.0.0.0/24 + Server: 10.0.0.1/24 + Client1: 10.0.0.2/24 + Client2: 10.0.0.3/24 + + Backend networks: + Server data: 192.168.0.0/24 + Client1 data: 192.168.1.0/24 + Client2 data: 192.168.2.0/24 +.... + +""" + +import infamy +import infamy.util as util +import infamy.iface as iface +import infamy.wireguard as wg + +def configure_server(server, server_public_key, server_private_key, client1_public_key, client2_public_key): + """Configure WireGuard server with key-bag level settings for all clients""" + server.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 + }] + } + } + }, + "ietf-truststore": { + "truststore": { + "public-key-bags": { + "public-key-bag": [{ + "name": "roadwarriors", + "public-key": [{ + "name": "client1-key", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": client1_public_key, + }, { + "name": "client2-key", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": client2_public_key, + }] + }] + } + } + }, + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": server["wan"], + "ipv4": { + "address": [{ + "ip": "192.168.100.1", + "prefix-length": 24 + }], + "forwarding": True + } + }, { + "name": server["data"], + "ipv4": { + "address": [{ + "ip": "192.168.0.1", + "prefix-length": 24 + }], + "forwarding": True + } + }, + { + "name": "wg0", + "type": "infix-if-type:wireguard", + "ipv4": { + "address": [{ + "ip": "10.0.0.1", + "prefix-length": 24 + }], + "forwarding": True + }, + "wireguard": { + "private-key": "server-wg-key", + "peers": [{ + "public-key-bag": "roadwarriors", + "persistent-keepalive": 3, + "peer": [{ + "public-key": "client1-key", + "allowed-ips": ["10.0.0.2/32", "192.168.1.0/24"] + }, { + "public-key": "client2-key", + "allowed-ips": ["10.0.0.3/32", "192.168.2.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.1.0/24", + "next-hop": { + "next-hop-address": "10.0.0.2" + } + }, { + "destination-prefix": "192.168.2.0/24", + "next-hop": { + "next-hop-address": "10.0.0.3" + } + }, { + "destination-prefix": "192.168.50.0/24", + "next-hop": { + "next-hop-address": "192.168.100.2" + } + }, { + "destination-prefix": "192.168.51.0/24", + "next-hop": { + "next-hop-address": "192.168.100.2" + } + }] + } + } + }] + } + } + } + }) + +def configure_router(router): + """Configure router to forward traffic between server and clients""" + router.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": router["wan"], + "ipv4": { + "address": [{ + "ip": "192.168.100.2", + "prefix-length": 24 + }], + "forwarding": True + } + }, { + "name": router["lan1"], + "ipv4": { + "address": [{ + "ip": "192.168.50.1", + "prefix-length": 24 + }], + "forwarding": True + } + }, { + "name": router["lan2"], + "ipv4": { + "address": [{ + "ip": "192.168.51.1", + "prefix-length": 24 + }], + "forwarding": True + } + }] + } + } + }) + +def configure_client(client, client_num, private_key, public_key, server_pubkey): + """Configure a WireGuard roadwarrior client""" + # Client1 is on 192.168.50.0/24, Client2 is on 192.168.51.0/24 + link_subnet = 49 + client_num + + client.put_config_dicts({ + "ietf-keystore": { + "keystore": { + "asymmetric-keys": { + "asymmetric-key": [{ + "name": f"client{client_num}-wg-key", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": public_key, + "private-key-format": "infix-crypto-types:x25519-private-key-format", + "cleartext-private-key": private_key + }] + } + } + }, + "ietf-truststore": { + "truststore": { + "public-key-bags": { + "public-key-bag": [{ + "name": "server", + "public-key": [{ + "name": "server-key", + "public-key-format": "infix-crypto-types:x25519-public-key-format", + "public-key": server_pubkey + }] + }] + } + } + }, + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": client["link"], + "ipv4": { + "address": [{ + "ip": f"192.168.{link_subnet}.2", + "prefix-length": 24 + }] + } + }, { + "name": client["data"], + "ipv4": { + "address": [{ + "ip": f"192.168.{client_num}.1", + "prefix-length": 24 + }], + "forwarding": True + } + }, { + "name": "wg0", + "type": "infix-if-type:wireguard", + "ipv4": { + "address": [{ + "ip": f"10.0.0.{client_num + 1}", + "prefix-length": 24 + }], + "forwarding": True + }, + "wireguard": { + "private-key": f"client{client_num}-wg-key", + "peers": [{ + "public-key-bag": "server", + "endpoint": "192.168.100.1", + "endpoint-port": 51820, + "allowed-ips": ["10.0.0.0/24", "192.168.0.0/24"], + "persistent-keepalive": 3 + }] + } + }] + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:static", + "name": "default", + "static-routes": { + "ipv4": { + "route": [{ + "destination-prefix": "192.168.0.0/24", + "next-hop": { + "next-hop-address": "10.0.0.1" + } + }, { + "destination-prefix": "0.0.0.0/0", + "next-hop": { + "next-hop-address": f"192.168.{link_subnet}.1" + } + }] + } + } + }] + } + } + } + }) +server_private_key = "uIUL4AnD5QaVrwHDPHJzQ7sIQ+Q3zDdflnvfd59qa28=" +server_public_key = "qGVmu5UbNtMuZs2t9wFoOoHlvgmV+A1SyQacVb/bEV0=" + +client1_private_key = "kNmkNlSkSh9+Va2tmFv9Va8TBCZlTBF0fKAGJf8vomo=" +client1_public_key = "ROaZyvJc5DzA2XUAAeTj2YlwDsy2w0lr3t+rWj2imAk=" + +client2_private_key = "OPT7v/l5zICEmFIrO0U+YwA+w07l8Xo2Dp38hjGOHGY=" +client2_public_key = "Om9CPLYdK3l93GauKrq5WXo/gbcD+1CeqFpobRLLkB4=" + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUTs"): + env = infamy.Env() + server = env.attach("server", "mgmt") + router = env.attach("router", "mgmt") + client1 = env.attach("client1", "mgmt") + client2 = env.attach("client2", "mgmt") + + _, hport_server = env.ltop.xlate("host", "data") + _, hport_client1 = env.ltop.xlate("host", "data1") + _, hport_client2 = env.ltop.xlate("host", "data2") + + with test.step("Configure DUTs"): + util.parallel( + lambda: configure_server(server, server_public_key, server_private_key, client1_public_key, client2_public_key), + lambda: configure_router(router), + lambda: configure_client(client1, 1, client1_private_key, client1_public_key, server_public_key), + lambda: configure_client(client2, 2, client2_private_key, client2_public_key, server_public_key) + ) + + with test.step("Check on the server that both clients is connected"): + util.parallel(lambda: util.until(lambda: wg.is_peer_up(server, "wg0", client1_public_key)), + lambda: util.until(lambda: wg.is_peer_up(server, "wg0", client2_public_key))) + + with infamy.IsolatedMacVlan(hport_server) as ns_server, \ + infamy.IsolatedMacVlan(hport_client1) as ns_client1, \ + infamy.IsolatedMacVlan(hport_client2) as ns_client2: + ns_server.addip("192.168.0.2") + ns_server.addroute("default", "192.168.0.1") + + ns_client1.addip("192.168.1.2") + ns_client1.addroute("default", "192.168.1.1") + + ns_client2.addip("192.168.2.2") + ns_client2.addroute("default", "192.168.2.1") + + with test.step("Verify IPv4 connectivity with ping 192.168.0.2 from host:data1 and host:data2"): + util.parallel( + lambda: ns_client1.must_reach("192.168.0.2"), + lambda: ns_client2.must_reach("192.168.0.2") + ) + + test.succeed() diff --git a/test/case/interfaces/wireguard_roadwarrior/topology.dot b/test/case/interfaces/wireguard_roadwarrior/topology.dot new file mode 100644 index 000000000..3faa38297 --- /dev/null +++ b/test/case/interfaces/wireguard_roadwarrior/topology.dot @@ -0,0 +1,52 @@ +graph "roadwarrior" { + layout="neato"; + overlap=false; + esep="+20"; + splines=true; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | data } | host | { mgmt2 } | { data2 | mgmt3 | data1 | mgmt4 }", + pos="200,250!", + requires="controller", + ]; + + server [ + label="{ data | mgmt } | { server | 192.168.100.1 } | wan", + pos="50,150!", + requires="infix", + ]; + + router [ + label=" wan | { mgmt | router | (internet) } | { lan1 | lan2 }", + pos="200,150!", + requires="infix", + ]; + + client1 [ + label=" link | { data | mgmt } | { client1 | 192.168.50.2 }", + pos="325,175!", + requires="infix", + ]; + + client2 [ + label=" link | { mgmt | data } | { client2 | 192.168.51.2 }", + pos="325,125!", + requires="infix", + ]; + + host:mgmt1 -- server:mgmt [requires="mgmt", color="lightgray"]; + host:mgmt2 -- router:mgmt [requires="mgmt", color="lightgray"]; + host:mgmt3 -- client1:mgmt [requires="mgmt", color="lightgray"]; + host:mgmt4 -- client2:mgmt [requires="mgmt", color="lightgray"]; + + server:wan -- router:wan [label="192.168.100.0/24", headlabel=".2", taillabel=".1", fontcolor="black", color="black"]; + router:lan1 -- client1:link [label="192.168.50.0/24", headlabel=".2", taillabel=".1", fontcolor="black", color="black"]; + router:lan2 -- client2:link [label="192.168.51.0/24", headlabel=".2", taillabel=".1", fontcolor="black", color="black"]; + + host:data -- server:data [headlabel=".1", label="192.168.0.0/24", taillabel=".2", labeldistance=1, fontcolor="black", color="black"]; + host:data1 -- client1:data [headlabel=".1", label="192.168.1.0/24", taillabel=".2", labeldistance=1, fontcolor="black", color="black"]; + host:data2 -- client2:data [headlabel=".1", label="192.168.2.0/24", taillabel=".2", labeldistance=1, fontcolor="black", color="black"]; +} diff --git a/test/case/interfaces/wireguard_roadwarrior/topology.svg b/test/case/interfaces/wireguard_roadwarrior/topology.svg new file mode 100644 index 000000000..73f5e21ba --- /dev/null +++ b/test/case/interfaces/wireguard_roadwarrior/topology.svg @@ -0,0 +1,158 @@ + + + + + + +roadwarrior + + + +host + +mgmt1 + +data + +host + +mgmt2 + +data2 + +mgmt3 + +data1 + +mgmt4 + + + +server + +data + +mgmt + +server + +192.168.100.1 + +wan + + + +host:mgmt1--server:mgmt + + + + +host:data--server:data + +192.168.0.0/24 +.1 +.2 + + + +router + +wan + +mgmt + +router + +(internet) + +lan1 + +lan2 + + + +host:mgmt2--router:mgmt + + + + +client1 + +link + +data + +mgmt + +client1 + +192.168.50.2 + + + +host:mgmt3--client1:mgmt + + + + +host:data1--client1:data + +192.168.1.0/24 +.1 +.2 + + + +client2 + +link + +mgmt + +data + +client2 + +192.168.51.2 + + + +host:mgmt4--client2:mgmt + + + + +host:data2--client2:data + +192.168.2.0/24 +.1 +.2 + + + +server:wan--router:wan + +192.168.100.0/24 +.2 +.1 + + + +router:lan1--client1:link + +192.168.50.0/24 +.2 +.1 + + + +router:lan2--client2:link + +192.168.51.0/24 +.2 +.1 + + + diff --git a/test/case/ntp/client_stratum_selection/test.adoc b/test/case/ntp/client_stratum_selection/test.adoc index 9c458894f..56f5ebc52 100644 --- a/test/case/ntp/client_stratum_selection/test.adoc +++ b/test/case/ntp/client_stratum_selection/test.adoc @@ -29,6 +29,7 @@ image::topology.svg[NTP client stratum selection topology, align=center, scaledw . Wait for srv2 to sync from srv1 . Configure client to sync from both servers . Wait for client to see both servers +. Wait for srv2 stratum to stabilize . Verify client selects srv1 (lower stratum) diff --git a/test/case/routing/ospf_debug/test.adoc b/test/case/routing/ospf_debug/test.adoc new file mode 100644 index 000000000..b42f26f87 --- /dev/null +++ b/test/case/routing/ospf_debug/test.adoc @@ -0,0 +1,33 @@ +=== OSPF Debug Logging + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/ospf_debug] + +==== Description + +Verifies OSPF debug logging by configuring two routers (R1 and R2) with +OSPF on their interconnecting link. The test enables specific OSPF debug +categories and verifies that appropriate debug messages appear in +/var/log/debug. + +This test specifically validates: +- Debug messages appear when debug options are enabled +- No excessive debug messages when debug options are disabled +- Individual categories (ism, nsm, packet) can be toggled independently + +==== Topology + +image::topology.svg[OSPF Debug Logging topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUTs +. Clean up old log files from previous test runs +. Configure R1 and R2 without debug enabled +. Wait for OSPF adjacency to form +. Enable OSPF debug logging on R1 +. Verify OSPF debug messages appear in log file +. Remove log file before disabling debug +. Disable OSPF debug logging on R1 +. Verify no OSPF debug messages when disabled + + diff --git a/test/case/routing/ospf_debug/topology.svg b/test/case/routing/ospf_debug/topology.svg new file mode 100644 index 000000000..8128a4ae3 --- /dev/null +++ b/test/case/routing/ospf_debug/topology.svg @@ -0,0 +1,63 @@ + + + + + + +ospf_debug + + + +PC + +PC + +mgmt1 + +mgmt2 + + + +R1 + +mgmt + +link + +R1 + 192.168.100.1/32 +(lo) + + + +PC:mgmt1--R1:mgmt + + + + +R2 + +link + +mgmt + +R2 + 192.168.200.1/32 +(lo) + + + +PC:mgmt2--R2:mgmt + + + + +R1:link--R2:link + +192.168.50.2/24 +192.168.50.1/24 + + + diff --git a/test/case/routing/rip_multihop/test.adoc b/test/case/routing/rip_multihop/test.adoc new file mode 100644 index 000000000..82b27fa95 --- /dev/null +++ b/test/case/routing/rip_multihop/test.adoc @@ -0,0 +1,29 @@ +=== RIP Multi-hop + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/rip_multihop] + +==== Description + +Verifies RIP functionality across multiple hops with three routers in a line +topology (R1 -- R2 -- R3). This test ensures: +- RIP routes propagate through multiple hops +- R2 (middle router) has two RIP neighbors +- End-to-end connectivity works across the RIP network + +Topology: + PC:data1 -- R1 -- R2 -- R3 -- PC:data2 + +==== Topology + +image::topology.svg[RIP Multi-hop topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUTs +. Configure routers +. Wait for RIP routes to be exchanged +. Verify R2 has two RIP neighbors +. Test end-to-end connectivity PC:data1 to R3 loopback +. Test end-to-end connectivity PC:data2 to R1 loopback + + diff --git a/test/infamy/iface.py b/test/infamy/iface.py index 4b872ad33..2831bb680 100644 --- a/test/infamy/iface.py +++ b/test/infamy/iface.py @@ -98,6 +98,16 @@ def get_phys_address(target, iface): return get_param(target, iface, "phys-address") +def get_oper_status(target, iface): + """Get interface operational status (up/down/etc)""" + return get_param(target, iface, "oper-status") + + +def is_oper_up(target, iface): + """Check if interface operational status is 'up'""" + return get_oper_status(target, iface) == "up" + + def exist_bridge_multicast_filter(target, group, iface, bridge): """Check if a bridge has a multicast filter for group with iface""" # The interface array is different in restconf/netconf, netconf has diff --git a/test/infamy/wireguard.py b/test/infamy/wireguard.py new file mode 100644 index 000000000..ba17c6b90 --- /dev/null +++ b/test/infamy/wireguard.py @@ -0,0 +1,90 @@ +""" +Fetch WireGuard interface status from remote device. +""" +import json + + +def get_peers(target, iface): + """Get all WireGuard peers from interface as a list""" + interface = target.get_iface(iface) + if interface is None: + return [] + + wg = interface.get("wireguard") or interface.get("infix-if-wireguard:wireguard") + if wg is None: + return [] + + # Peers are under 'peer-status' in operational state + peer_status = wg.get('peer-status') + if peer_status is None: + return [] + + # The peer-status container has a 'peer' list inside it + if isinstance(peer_status, dict): + peer_list = peer_status.get('peer', []) + if isinstance(peer_list, list): + return peer_list + # Single peer might not be in a list + if peer_list: + return [peer_list] + return [] + + # If peer_status is already a list + if isinstance(peer_status, list): + return peer_status + + return [] + + +def get_peer_by_pubkey(target, iface, public_key): + """Get specific WireGuard peer by public key""" + peers = get_peers(target, iface) + + for peer in peers: + if peer.get('public-key') == public_key: + return peer + + return None + + +def is_peer_up(target, iface, public_key): + """Check if WireGuard peer connection status is 'up'""" + peer = get_peer_by_pubkey(target, iface, public_key) + if peer is None: + return False + + status = peer.get('connection-status') + if status is None: + return False + + return status.lower() == 'up' + + +def get_peer_endpoint(target, iface, public_key): + """Get WireGuard peer endpoint""" + peer = get_peer_by_pubkey(target, iface, public_key) + if peer is None: + return None + + return peer.get('endpoint') + + +def get_peer_transfer(target, iface, public_key): + """Get WireGuard peer transfer statistics (tx, rx in bytes)""" + peer = get_peer_by_pubkey(target, iface, public_key) + if peer is None: + return None, None + + tx = peer.get('transfer-tx') + rx = peer.get('transfer-rx') + + return tx, rx + + +def get_peer_handshake(target, iface, public_key): + """Get WireGuard peer latest handshake timestamp""" + peer = get_peer_by_pubkey(target, iface, public_key) + if peer is None: + return None + + return peer.get('latest-handshake')