From 3f01ea5ee65fa35f5692fe1ca37f9c7203ae36d7 Mon Sep 17 00:00:00 2001
From: Subho
Date: Tue, 5 Sep 2023 10:20:48 +0530
Subject: [PATCH] networking: Add support for WireGuard
---
pkg/networkmanager/dialogs-common.jsx | 53 +++-
pkg/networkmanager/interfaces.js | 38 +++
pkg/networkmanager/network-interface.jsx | 19 +-
pkg/networkmanager/network-main.jsx | 1 +
pkg/networkmanager/wireguard.jsx | 353 +++++++++++++++++++++
pkg/networkmanager/wireguard.scss | 38 +++
test/run | 2 +-
test/verify/check-networkmanager-wireguard | 194 +++++++++++
8 files changed, 683 insertions(+), 15 deletions(-)
create mode 100644 pkg/networkmanager/wireguard.jsx
create mode 100644 pkg/networkmanager/wireguard.scss
create mode 100755 test/verify/check-networkmanager-wireguard
diff --git a/pkg/networkmanager/dialogs-common.jsx b/pkg/networkmanager/dialogs-common.jsx
index 519ec18cb264..bad4c2d812eb 100644
--- a/pkg/networkmanager/dialogs-common.jsx
+++ b/pkg/networkmanager/dialogs-common.jsx
@@ -19,6 +19,7 @@
import React, { useContext, useEffect, useState } from 'react';
import cockpit from 'cockpit';
+import * as packagekit from 'packagekit.js';
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
@@ -35,11 +36,14 @@ import { IpSettingsDialog } from './ip-settings.jsx';
import { TeamDialog, getGhostSettings as getTeamGhostSettings } from './team.jsx';
import { TeamPortDialog } from './teamport.jsx';
import { VlanDialog, getGhostSettings as getVlanGhostSettings } from './vlan.jsx';
+import { WireGuardDialog, getWireGuardGhostSettings } from './wireguard.jsx';
import { MtuDialog } from './mtu.jsx';
import { MacDialog } from './mac.jsx';
import { ModalError } from 'cockpit-components-inline-notification.jsx';
import { ModelContext } from './model-context.jsx';
import { useDialogs } from "dialogs.jsx";
+import { install_dialog } from "cockpit-components-install-dialog.jsx";
+import { read_os_release } from "os-release.js";
import {
apply_group_member,
@@ -141,7 +145,7 @@ export const Name = ({ idPrefix, iface, setIface }) => {
);
};
-export const NetworkModal = ({ dialogError, help, idPrefix, title, onSubmit, children, isFormHorizontal, isCreateDialog }) => {
+export const NetworkModal = ({ dialogError, help, idPrefix, title, onSubmit, children, isFormHorizontal, isCreateDialog, submitDisabled = false }) => {
const Dialogs = useDialogs();
return (
@@ -152,7 +156,7 @@ export const NetworkModal = ({ dialogError, help, idPrefix, title, onSubmit, chi
title={title}
footer={
<>
-
+ }
+ footerContent={
+ {_("Endpoint acting as a \"server\" need to be specified as host:port, otherwise it can be left empty.")}
+ }
+ >
+
+
+ setPeers(peers => [...peers, { publicKey: '', endpoint: '', allowedIps: '' }])}
+ >
+ {_("Add peer")}
+
+ >
+ }
+ />
+ }
+ className='dynamic-form-group'
+ >
+ {(peers.length !== 0)
+ ? peers.map((peer, i) => (
+
+
+ {
+ setPeers(peers => peers.map((peer, index) => i === index ? { ...peer, publicKey: val } : peer));
+ }}
+ id={idPrefix + '-publickey-peer-' + i}
+ />
+
+
+ {
+ setPeers(peers => peers.map((peer, index) => i === index ? { ...peer, endpoint: val } : peer));
+ }}
+ id={idPrefix + '-endpoint-peer-' + i}
+ />
+
+
+ {
+ setPeers(peers => peers.map((peer, index) => i === index ? { ...peer, allowedIps: val } : peer));
+ }}
+ id={idPrefix + '-allowedips-peer-' + i}
+ />
+
+
+ {
+ setPeers(peers => peers.filter((_, index) => i !== index));
+ }}
+ >
+
+
+
+
+ ))
+ :
+ {_("No peers added.")}
+
+ }
+
+
+ );
+}
+
+export function getWireGuardGhostSettings({ newIfaceName }) {
+ return {
+ connection: {
+ id: `con-${newIfaceName}`,
+ interface_name: newIfaceName
+ },
+ wireguard: {
+ listen_port: 0,
+ private_key: "",
+ peers: []
+ },
+ ipv4: {
+ addresses: []
+ }
+ };
+}
diff --git a/pkg/networkmanager/wireguard.scss b/pkg/networkmanager/wireguard.scss
new file mode 100644
index 000000000000..23f4fb37257d
--- /dev/null
+++ b/pkg/networkmanager/wireguard.scss
@@ -0,0 +1,38 @@
+@import "global-variables.scss";
+@import "@patternfly/patternfly/utilities/Text/text.scss";
+@import "@patternfly/patternfly/utilities/Spacing/spacing.scss";
+@import "@patternfly/patternfly/utilities/Sizing/sizing.scss";
+
+.placeholder-text {
+ color: var(--pf-v5-global--Color--200);
+ block-size: 2.25rem;
+}
+
+.dynamic-form-group {
+ .pf-v5-c-empty-state {
+ padding: 0;
+ }
+
+ .pf-v5-c-form__field-group-body {
+ .pf-v5-c-form__group {
+ display: block;
+ }
+
+ .remove-button-group {
+ // Move 'Remove' button the the end of the row
+ grid-column: -1;
+ // Move 'Remove' button to the bottom of the line so as to align with the other form fields
+ display: flex;
+ align-items: flex-end;
+ }
+ }
+
+ // We use FormFieldGroup PF component for the nested look and for ability to add buttons to the header
+ // However we want to save space and not add indent to the left so we need to override it
+ .pf-v5-c-form__field-group-body {
+ // Stretch content fully
+ --pf-v5-c-form__field-group-body--GridColumn: 1 / -1;
+ // Reduce padding at the top
+ --pf-v5-c-form__field-group-body--PaddingTop: var(--pf-v5-global--spacer--xs);
+ }
+}
diff --git a/test/run b/test/run
index a2e6839ada19..8beee5eaa8a7 100755
--- a/test/run
+++ b/test/run
@@ -12,7 +12,7 @@ PREPARE_OPTS=""
RUN_OPTS=""
ALL_TESTS="$(test/common/run-tests --test-dir test/verify -l)"
-RE_NETWORKING='Networking|Bonding|TestBridge|Firewall|Team|IPA|AD'
+RE_NETWORKING='Networking|Bonding|TestBridge|WireGuard|Firewall|Team|IPA|AD'
RE_STORAGE='Storage'
RE_EXPENSIVE='HostSwitching|MultiMachine|Updates|Superuser|Kdump|Pages'
diff --git a/test/verify/check-networkmanager-wireguard b/test/verify/check-networkmanager-wireguard
new file mode 100755
index 000000000000..ba6c6481bc90
--- /dev/null
+++ b/test/verify/check-networkmanager-wireguard
@@ -0,0 +1,194 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+import subprocess
+import sys
+
+import netlib
+import packagelib
+import testlib
+
+
+class TestWireGuard(packagelib.PackageCase, netlib.NetworkCase):
+ provision = {
+ "machine1": {"address": "192.168.100.11/24", "restrict": False},
+ "machine2": {"address": "192.168.100.12/24", "restrict": False},
+ }
+
+ def testVPN(self):
+ m1 = self.machines["machine1"]
+ m2 = self.machines["machine2"]
+ b = self.browser
+
+ self.login_and_go("/network")
+
+ # Peer 1 (client)
+ m1_port = 51820
+ m1_ip4 = "10.0.0.1"
+ m1_ip6 = "2001::1"
+ b.click("button:contains('Add VPN')")
+ b.wait_visible("#network-wireguard-settings-dialog")
+ iface_name = b.val("#network-wireguard-settings-interface-name-input")
+ b.wait_visible("button:contains('Save'):disabled")
+ if m1.image in ["rhel4edge", "centos-8-stream"] or m1.image.startswith("rhel-8"):
+ b.wait_visible(".pf-v5-c-alert:contains('wireguard-tools package is not installed')")
+ b.click("button:contains('Cancel')")
+ b.wait_not_present("#network-ip-settings-dialog")
+ b.wait_visible("#networking")
+ b.wait_not_present(f"#networking-interfaces th:contains('{iface_name}')")
+ # Skip the rest of the tests for images without wireguard-tools
+ # As without it private/public key, connection over IPv4/IPv6 etc can't be tested
+ return
+
+ # Peer 2 (server)
+ m2_port = 51820
+ m2_ip4 = "10.0.0.2"
+ m2_ip6 = "2001::2"
+ if not m2.ostree_image:
+ m2.execute(f"firewall-cmd --add-port={m2_port}/udp")
+ m2.execute("wg genkey > private")
+ m2_pubkey = m2.execute("wg pubkey < private").strip()
+ m2.execute("ip link add dev wg0 type wireguard")
+ m2.execute(f"ip addr add {m2_ip4}/24 dev wg0")
+ m2.execute("wg set wg0 private-key ./private")
+ m2.execute(f"wg set wg0 listen-port {m2_port}")
+ m2.execute("ip link set wg0 up")
+
+ # Validate each field, enter the right value, and then proceed to the next field
+ #
+ # check private-key
+ b.click("#network-wireguard-settings-paste-key")
+ b.set_input_text("#network-wireguard-settings-private-key-input", "incorrect key")
+ b.set_input_text("#network-wireguard-settings-addresses-input", m1_ip4)
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('key must be 32 bytes base64 encoded')")
+ b.click("#network-wireguard-settings-generated-key")
+
+ # check public-key
+ b.wait_not_val("#network-wireguard-settings-public-key input", "")
+ m1_pubkey = b.val("#network-wireguard-settings-public-key input")
+
+ # check listen-port
+ b.set_input_text("#network-wireguard-settings-listen-port-input", "66000")
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('out of range')")
+ b.set_input_text("#network-wireguard-settings-listen-port-input", "sometext")
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('Listen port must be a number')")
+ b.set_input_text("#network-wireguard-settings-listen-port-input", str(m1_port))
+
+ # check ip addresses
+ b.set_input_text("#network-wireguard-settings-addresses-input", "10.0.0.1/24/56")
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('Addresses are not formatted correctly')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "10.0.0")
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('Invalid address 10.0.0')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "ten.one")
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('Invalid address ten.one')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "10 1")
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('Invalid address 10')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "1.2.3.4/")
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('Invalid prefix or netmask')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "1.2.3.4 , 5.6.7.8 1.2.3.4.5")
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('Invalid address 1.2.3.4.5')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", f"{m1_ip4}/24 1.2.3.4")
+
+ # peer
+ b.click("button:contains('Add peer')")
+ b.wait_visible("#network-wireguard-settings-peer-0")
+ b.set_input_text("#network-wireguard-settings-publickey-peer-0", m2_pubkey)
+ b.set_input_text("#network-wireguard-settings-endpoint-peer-0", f"192.168.100.12")
+ b.set_input_text("#network-wireguard-settings-allowedips-peer-0", m2_ip4)
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('Peer #1 has invalid endpoint. It must be specified as host:port, e.g. 1.2.3.4:51820 or example.com:51820')")
+ b.set_input_text("#network-wireguard-settings-endpoint-peer-0", f"192.168.100.12:somestring")
+ b.click("button:contains('Save')")
+ b.wait_visible(".pf-v5-c-alert:contains('Peer #1 has invalid endpoint port. Port must be a number.')")
+ b.click("button:contains('Add peer')")
+ b.wait_visible("#network-wireguard-settings-peer-1")
+ b.set_input_text("#network-wireguard-settings-publickey-peer-1", m2_pubkey)
+ b.set_input_text("#network-wireguard-settings-endpoint-peer-1", f"192.168.100.12:{m2_port}")
+ b.set_input_text("#network-wireguard-settings-allowedips-peer-1", m2_ip4)
+ b.click("button#network-wireguard-settings-btn-close-peer-0")
+ b.wait_not_present("#network-wireguard-settings-peer-1")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#network-wireguard-settings-dialog")
+ b.wait_in_text(f"#networking-interfaces th:contains('{iface_name}') + td", f"1.2.3.4/32, {m1_ip4}/24")
+
+ # endpoint and port is not necessary for a peer if that peer estalishes the connectio first (i.e. the client)
+ m2.execute(f"wg set wg0 peer {m1_pubkey} allowed-ips {m1_ip4}/32")
+
+ # check connection over ipv4
+ try:
+ m1.execute(f"ping -c 5 {m2_ip4}")
+ except (subprocess.CalledProcessError, testlib.Error):
+ print("-------- status on m1 ----------", file=sys.stderr)
+ m1.execute("set -x; ip a >&2; ip route >&2; nmcli c >&2; wg >&2")
+ print("-------- status on m2 ----------", file=sys.stderr)
+ m2.execute("set -x; ip a >&2; ip route >&2; nmcli c >&2; wg >&2")
+ raise
+
+ # check connection over ipv6
+ m2.execute(f"ip addr add {m2_ip6}/64 dev wg0")
+ m2.execute(f"wg set wg0 peer {m1_pubkey} allowed-ips {m1_ip4}/32,{m1_ip6}")
+
+ b.click(f"#networking-interfaces button:contains('{iface_name}')")
+ b.click("#networking-edit-wg")
+ b.wait_visible("#network-wireguard-settings-dialog")
+ b.set_input_text("#network-wireguard-settings-allowedips-peer-0", f"{m2_ip4}/32,{m2_ip6}")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#network-wireguard-settings-dialog")
+
+ m1.execute("until wg show wg0 | grep -q 'allowed ips.*2001::2/128'; do sleep 1; done")
+
+ b.click("#networking-edit-ipv6")
+ b.wait_visible("#network-ip-settings-dialog")
+ b.select_from_dropdown("#network-ip-settings-select-method", "manual")
+ b.set_input_text("#network-ip-settings-address-0", m1_ip6)
+ b.set_input_text("#network-ip-settings-netmask-0", "64")
+ b.set_input_text("#network-ip-settings-gateway-0", "::")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#network-ip-settings-dialog")
+ self.wait_for_iface_setting("IPv6", "Address 2001:0:0:0:0:0:0:1/64")
+
+ m1.execute(f"until ip a show dev {iface_name} | grep -q 'inet6 {m1_ip6}/64 scope global'; do sleep 0.3; done", timeout=10)
+
+ try:
+ m1.execute(f"ping -6 -c 5 {m2_ip6}")
+ except (subprocess.CalledProcessError, testlib.Error):
+ print("-------- status on m1 ----------", file=sys.stderr)
+ m1.execute("set -x; ip a >&2; ip -6 route >&2; nmcli c >&2; wg >&2")
+ print("-------- status on m2 ----------", file=sys.stderr)
+ m2.execute("set -x; ip a >&2; ip -6 route >&2; nmcli c >&2; wg >&2")
+ raise
+
+ # install wireguard-tools from the install dialog
+ if not m1.ostree_image:
+ b.go("/network")
+ m1.execute("pkcon remove -y wireguard-tools")
+ self.createPackage("wireguard-tools", "1", "1")
+ self.enableRepo()
+ # HACK: Packagekit on Arch Linux does not deal well with detecting new repositories
+ if m1.image == "arch":
+ m1.execute("systemctl restart packagekit")
+ m1.execute("pkcon refresh force")
+ b.click("button:contains('Add VPN')")
+ b.wait_visible("#dialog button:contains('Install'):enabled")
+ b.click("#dialog button:contains('Install')")
+ b.wait_not_present("#dialog")
+ b.wait_visible("#network-wireguard-settings-dialog")
+ b.wait_visible(".pf-v5-c-alert")
+ b.click("button:contains('Cancel')")
+
+ # lastly delete the interface
+ b.click(f"#networking-interfaces button:contains('{iface_name}')")
+ b.click("#network-interface-delete")
+ b.wait_not_present(f"#networking-interfaces th:contains('{iface_name}')")
+
+
+if __name__ == "__main__":
+ testlib.test_main()