Skip to content

Commit

Permalink
network: wireguard WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
subhoghoshX committed Jul 3, 2023
1 parent 755575b commit 301cee2
Show file tree
Hide file tree
Showing 7 changed files with 382 additions and 15 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 {...properties} />;
else if (type == 'mtu')
dlg = <MtuDialog {...properties} />;
else if (type == 'mac')
Expand Down
73 changes: 60 additions & 13 deletions pkg/networkmanager/interfaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,18 @@ export function NetworkManagerModel() {
};
}

if (settings.wireguard) {
result.wireguard = {
listen_port: get("wireguard", "listen-port", ""),
peers: get("wireguard", "peers", []).map(peer => ({
publicKey: peer['public-key'].v,
endpoint: peer.endpoint?.v ?? "", // enpoint of a peer is optional
allowedIps: peer['allowed-ips'].v
})),
private_key: get("wireguard", "private-key")
};
}

return result;
}

Expand Down Expand Up @@ -699,6 +711,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 Expand Up @@ -785,18 +824,25 @@ export function NetworkManagerModel() {
connections_by_uuid[settings.connection.uuid] = obj;
}

function refresh_settings(obj) {
async function refresh_settings(obj) {
push_refresh();
client.call(objpath(obj), "org.freedesktop.NetworkManager.Settings.Connection", "GetSettings")
.then(function(reply) {
const result = reply[0];
if (result) {
priv(obj).orig = result;
set_settings(obj, settings_from_nm(result));
}
})
.catch(complain)
.finally(pop_refresh);

try {
const reply = await client.call(objpath(obj), "org.freedesktop.NetworkManager.Settings.Connection", "GetSettings");
const result = reply[0];
if (result) {
if (result.wireguard) {
const secrets = await client.call(objpath(obj), "org.freedesktop.NetworkManager.Settings.Connection", "GetSecrets", ["wireguard"]);
result.wireguard['private-key'] = secrets[0].wireguard['private-key'];
}
priv(obj).orig = result;
set_settings(obj, settings_from_nm(result));
}
} catch (e) {
complain(e);
} finally {
pop_refresh();
}
}

function refresh_udev(obj) {
Expand Down Expand Up @@ -829,8 +875,9 @@ export function NetworkManagerModel() {
.finally(pop_refresh);
}

function handle_updated(obj) {
refresh_settings(obj);
function handle_updated(obj, things) {
if (things.length > 0)
refresh_settings(obj);
}

/* NetworkManager specific object types, used by the generic D-Bus
Expand Down
16 changes: 15 additions & 1 deletion pkg/networkmanager/network-interface.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,19 @@ export const NetworkInterfacePage = ({
return renderSettingsRow(_("VLAN"), rows, configure);
}

function renderWireGuardSettingsRow() {
const rows = [];
const options = settings.wireguard;

if (!options) {
return null;
}

const configure = <NetworkAction type="wireguard" iface={iface} connectionSettings={settings} />;

return renderSettingsRow(_("WireGuard"), rows, configure);
}

return [
render_group(),
renderAutoconnectRow(),
Expand All @@ -573,7 +586,8 @@ export const NetworkInterfacePage = ({
renderBridgePortSettingsRow(),
renderBondSettingsRow(),
renderTeamSettingsRow(),
renderTeamPortSettingsRow()
renderTeamPortSettingsRow(),
renderWireGuardSettingsRow(),
];
}

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
221 changes: 221 additions & 0 deletions pkg/networkmanager/wireguard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* 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;

function addressesToString(addresses) {
return addresses.map(address => address[0] + "/" + address[1]).join(",");
}

function stringToAddresses(str) {
return str.split(",").map(strAddress => {
const [ip, prefix] = strAddress.split("/");
const defaultPrefix = 32;
// The gateway may be left as 0 if no gateway exists for that subnet.
const gateway = "0.0.0.0";
return [ip, prefix ?? defaultPrefix, gateway];
});
}

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

const [iface, setIface] = useState(settings.connection.interface_name);
const [privateKey, setPrivateKey] = useState(settings.wireguard.private_key);
const [publicKey, setPublicKey] = useState('');
const [listenPort, setListenPort] = useState(settings.wireguard.listen_port);
const [addresses, setAddresses] = useState(addressesToString(settings.ipv4.addresses));
const [dialogError, setDialogError] = useState('');
const [peers, setPeers] = useState(settings.wireguard.peers.map(peer => ({ ...peer, allowedIps: peer.allowedIps.join(',') })));

useEffect(() => {
async function getPublicKey() {
try {
const key = await cockpit.spawn(["wg", "pubkey"], { err: 'message' }).input(privateKey.trim());
setPublicKey(key.trim());
} catch (e) {
console.error(e.message);
setPublicKey('');
}
}

getPublicKey();
}, [privateKey]);

function onSubmit() {
function createSettingsObj() {
return {
...settings,
connection: {
...settings.connection,
id: `con-${iface}`,
interface_name: iface,
type: 'wireguard'
},
wireguard: {
private_key: privateKey.trim(),
listen_port: +listenPort,
peers: peers.map(peer => ({ ...peer, allowedIps: [peer.allowedIps] })),
},
ipv4: {
addresses: stringToAddresses(addresses),
method: 'manual',
dns: [],
dns_search: []
}
};
}

dialogSave({
connection,
dev,
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.spawn(["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' value={listenPort} onChange={(_, val) => { setListenPort(val) }} />
</FormGroup>
<FormGroup label={_("Addresses")} fieldId={idPrefix + '-addresses-input'}>
<TextInput id={idPrefix + '-addresses-input'} value={addresses} 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
},
wireguard: {
listen_port: "",
private_key: "",
peers: []
},
ipv4: {
addresses: []
}
};
}
Loading

0 comments on commit 301cee2

Please sign in to comment.