Skip to content

Commit

Permalink
network: wireguard WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
subhoghoshX committed Jun 27, 2023
1 parent 755575b commit be63190
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 2 deletions.
6 changes: 5 additions & 1 deletion pkg/networkmanager/dialogs-common.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ 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, getGhostSettings as getWireGuardGhostSettings } from './wireguard.jsx';
import { MtuDialog } from './mtu.jsx';
import { MacDialog } from './mac.jsx';
import { ModalError } from 'cockpit-components-inline-notification.jsx';
Expand Down Expand Up @@ -183,7 +184,7 @@ export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) =
let name;
// Find the first free interface name
for (let i = 0; i < 100; i++) {
name = type + i;
name = (type == 'wireguard' ? 'wg' : type) + i;
if (!model.find_interface(name))
break;
}
Expand All @@ -198,6 +199,7 @@ export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) =
if (type == 'vlan') settings = getVlanGhostSettings();
if (type == 'team') settings = getTeamGhostSettings({ newIfaceName });
if (type == 'bridge') settings = getBridgeGhostSettings({ newIfaceName });
if (type == 'wireguard') settings = getWireGuardGhostSettings({ newIfaceName });
}

const properties = { connection: con, dev, settings };
Expand All @@ -212,6 +214,8 @@ export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) =
dlg = <TeamDialog {...properties} />;
else if (type == 'bridge')
dlg = <BridgeDialog {...properties} />;
else if (type == 'wireguard')
dlg = <WireGuardDialog settings={settings} />;
else if (type == 'mtu')
dlg = <MtuDialog {...properties} />;
else if (type == 'mac')
Expand Down
27 changes: 27 additions & 0 deletions pkg/networkmanager/interfaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,33 @@ export function NetworkManagerModel() {
} else
delete result["802-3-ethernet"];

if (settings.wireguard) {
set("wireguard", "private-key", "s", settings.wireguard.private_key);
set("wireguard", "listen-port", "u", settings.wireguard.listen_port);
set("wireguard", "peers", "aa{sv}", settings.wireguard.peers.map(peer => {
return {
"public-key": {
t: "s",
v: peer.publicKey
},
...peer.endpoint
? {
endpoint: {
t: "s",
v: peer.endpoint
}
}
: {},
"allowed-ips": {
t: "as",
v: [peer.allowedIps]
}
};
}));
} else {
delete result.wireguard;
}

return result;
}

Expand Down
1 change: 1 addition & 0 deletions pkg/networkmanager/network-main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export const NetworkPage = ({ privileged, operationInProgress, usage_monitor, pl

const actions = privileged && (
<>
<NetworkAction buttonText={_("Add WireGuard")} type='wireguard' />
<NetworkAction buttonText={_("Add bond")} type='bond' />
<NetworkAction buttonText={_("Add team")} type='team' />
<NetworkAction buttonText={_("Add bridge")} type='bridge' />
Expand Down
2 changes: 1 addition & 1 deletion pkg/networkmanager/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function ip4_to_text(num, zero_is_empty) {
}

export function ip4_from_text(text, empty_is_zero) {
function invalid() {
function invalid(id) {
throw cockpit.format(_("Invalid address $0"), text);
}

Expand Down
192 changes: 192 additions & 0 deletions pkg/networkmanager/wireguard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2023 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/

import React, { useContext, useEffect, useState } from 'react';
import cockpit from 'cockpit';
import { Button } from '@patternfly/react-core/dist/esm/components/Button/index.js';
import { ClipboardCopy } from '@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js';
import { EmptyState, EmptyStateBody } from '@patternfly/react-core/dist/esm/components/EmptyState/index.js';
import { FormGroup, FormFieldGroup, FormFieldGroupHeader } from '@patternfly/react-core/dist/esm/components/Form/index.js';
import { Grid } from '@patternfly/react-core/dist/esm/layouts/Grid/index.js';
import { InputGroup } from '@patternfly/react-core/dist/esm/components/InputGroup/index.js';
import { MinusIcon } from '@patternfly/react-icons';
import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput/index.js';

import { Name, NetworkModal, dialogSave } from "./dialogs-common";
import { ModelContext } from './model-context';
import { useDialogs } from 'dialogs.jsx';

import './wireguard.scss';

const _ = cockpit.gettext;

export function WireGuardDialog({ settings }) {
const Dialogs = useDialogs();
const idPrefix = 'network-wireguard-settings';
const model = useContext(ModelContext);

const [iface, setIface] = useState(settings.connection.interface_name);
const [privateKey, setPrivateKey] = useState('');
const [publicKey, setPublicKey] = useState('');
const [listenPort, setListenPort] = useState('');
const [addresses, setAddresses] = useState('');
const [dialogError, setDialogError] = useState('');
const [peers, setPeers] = useState([]);

useEffect(() => {
async function getPublicKey() {
try {
const key = await cockpit.script(`echo ${privateKey.trim()} | wg pubkey`, { err: 'message' });
setPublicKey(key.trim());
} catch (e) {
setPublicKey('');
}
}

getPublicKey();
}, [privateKey]);

function onSubmit() {
function createSettingsObj() {
return {
connection: {
id: `con-${iface}`,
interface_name: iface,
type: 'wireguard'
},
wireguard: {
private_key: privateKey.trim(),
listen_port: +listenPort,
peers,
},
ipv4: {
addresses: [[addresses.split('/')[0], addresses.split('/')[1], '0.0.0.0']], // TODO: fix gateway
method: 'manual'
}
};
}

dialogSave({
model,
settings: createSettingsObj(),
onClose: Dialogs.close,
setDialogError
});
}

return (
<NetworkModal title={_("WireGuard settings")} onSubmit={onSubmit} dialogError={dialogError} idPrefix={idPrefix}>
<Name idPrefix={idPrefix} iface={iface} setIface={setIface} />
<FormGroup label={_('Private key')} fieldId={idPrefix + '-private-key-input'}>
<InputGroup>
<TextInput id={idPrefix + '-private-key-input'} value={privateKey} onChange={(_, val) => setPrivateKey(val)} />
<Button variant='secondary'
onClick={async () => {
const key = await cockpit.script("wg genkey");
setPrivateKey(key.trim());
}}
>
Generate private key
</Button>
</InputGroup>
</FormGroup>
<FormGroup label={_('Public key')}>
<ClipboardCopy isReadOnly>{publicKey}</ClipboardCopy>
</FormGroup>
<FormGroup label={_('Listen port')} fieldId={idPrefix + '-listen-port-input'}>
<TextInput id={idPrefix + '-listen-port-input'} placeholder='Random port selected when unspecified' type='number' onChange={(_, val) => { setListenPort(val) }} />
</FormGroup>
<FormGroup label={_('Addresses')} fieldId={idPrefix + '-addresses-input'}>
<TextInput id={idPrefix + '-addresses-input'} onChange={(_, val) => { setAddresses(val) }} placeholder='E.g. 10.0.0.1/24' />
</FormGroup>
<FormFieldGroup
header={
<FormFieldGroupHeader
titleText={{ text: _("Peers") }}
actions={
<Button
variant='secondary'
onClick={() => setPeers(peers => [...peers, { publicKey: '', endpoint: '', allowedIps: '' }])}
>
{_("Add peer")}
</Button>
}
/>
}
className='dynamic-form-group'
>
{(peers.length !== 0)
? peers.map((peer, i) => (
<Grid key={i} hasGutter>
<FormGroup className='pf-m-6-col-on-md' label={_("Public key")} fieldId={idPrefix + '-publickey-peer-' + i}>
<TextInput
value={peer.publicKey}
onChange={(_, val) => {
setPeers(peers => peers.map((peer, index) => i === index ? { ...peer, publicKey: val } : peer));
}}
id={idPrefix + '-publickey-peer-' + i}
/>
</FormGroup>
<FormGroup className='pf-m-3-col-on-md' label={_("Endpoint")} fieldId={idPrefix + '-endpoint-peer-' + i}>
<TextInput
value={peer.endpoint}
onChange={(_, val) => {
setPeers(peers => peers.map((peer, index) => i === index ? { ...peer, endpoint: val } : peer));
}}
id={idPrefix + '-endpoint-peer-' + i}
/>
</FormGroup>
<FormGroup className='pf-m-3-col-on-md' label={_("Allowed IPs")} fieldId={idPrefix + '-allowedips-peer-' + i}>
<TextInput
value={peer.allowedIps}
onChange={(_, val) => {
setPeers(peers => peers.map((peer, index) => i === index ? { ...peer, allowedIps: val } : peer));
}}
id={idPrefix + '-allowedips-peer-' + i}
/>
</FormGroup>
<FormGroup className='pf-m-1-col-on-md remove-button-group'>
<Button
variant='secondary'
onClick={() => {
setPeers(peers => peers.filter((_, index) => i !== index));
}}
>
<MinusIcon />
</Button>
</FormGroup>
</Grid>
))
: <EmptyState>
<EmptyStateBody>{_("No peers added.")}</EmptyStateBody>
</EmptyState>
}
</FormFieldGroup>
</NetworkModal>
);
}

export function getGhostSettings({ newIfaceName }) {
return {
connection: {
id: `con-${newIfaceName}`,
interface_name: newIfaceName
}
};
}
28 changes: 28 additions & 0 deletions pkg/networkmanager/wireguard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.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);
}
}
67 changes: 67 additions & 0 deletions test/verify/check-vpn
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)

from testlib import MachineCase, test_main
import configparser
import io

class TestVPNPlayground(MachineCase):
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 2 (server)
m2.execute("systemctl stop firewalld") # TODO: Fix it
m2.execute("wg genkey > private")
m2_port = 51820
m2_pubkey = m2.execute("wg pubkey < private").strip()

m2.execute("ip link add dev wg0 type wireguard")
m2.execute("ip addr add 10.0.0.2/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")

# Peer 1 (client)
m1.execute("systemctl stop firewalld") # TODO: Fix it
b.click("button:contains('Add WireGuard')")
b.wait_visible("#network-wireguard-settings-dialog")
b.click("button:contains('Generate private key')")
b.wait_not_val("#network-wireguard-settings-dialog .pf-v5-c-clipboard-copy input", "")
m1_pubkey = b.eval_js("document.querySelector('#network-wireguard-settings-dialog .pf-v5-c-clipboard-copy input').value")
b.set_input_text("#network-wireguard-settings-listen-port-input", "51820")
b.set_input_text("#network-wireguard-settings-addresses-input", "10.0.0.1/24")
b.click("button:contains('Add peer')")
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:{m2_port}")
b.set_input_text("#network-wireguard-settings-allowedips-peer-0", "10.0.0.2")
b.click("button:contains('Save')")

m2.execute(f"wg set wg0 peer {m1_pubkey} allowed-ips 10.0.0.1/32") # endpoint and port is not necessary for a peer if that peer estalishes the connectio first (i.e. the client)

m1.execute("ping -c 1 10.0.0.2")

# INI -> Python Dictionary
def convertToDict(string_config):
buf = io.StringIO(string_config)
config_object = configparser.ConfigParser()
config_object.read_file(buf)

output_dic = dict()
sections = config_object.sections()
for section in sections:
items = config_object.items(section)
output_dic[section] = dict(items)

return output_dic


if __name__ == "__main__":
test_main()

0 comments on commit be63190

Please sign in to comment.