Skip to content

Commit

Permalink
networking: Add support for OpenVPN
Browse files Browse the repository at this point in the history
  • Loading branch information
subhoghoshX committed Sep 28, 2023
1 parent 515b6bc commit ca446c9
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 2 deletions.
4 changes: 4 additions & 0 deletions pkg/networkmanager/dialogs-common.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { BondDialog, getGhostSettings as getBondGhostSettings } from './bond.jsx
import { BridgeDialog, getGhostSettings as getBridgeGhostSettings } from './bridge.jsx';
import { BridgePortDialog } from './bridgeport.jsx';
import { IpSettingsDialog } from './ip-settings.jsx';
import { OpenVPNDialog, getOpenVPNGhostSettings } from './openvpn.jsx';
import { TeamDialog, getGhostSettings as getTeamGhostSettings } from './team.jsx';
import { TeamPortDialog } from './teamport.jsx';
import { VlanDialog, getGhostSettings as getVlanGhostSettings } from './vlan.jsx';
Expand Down Expand Up @@ -203,6 +204,7 @@ export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) =
if (type == 'team') settings = getTeamGhostSettings({ newIfaceName });
if (type == 'bridge') settings = getBridgeGhostSettings({ newIfaceName });
if (type == 'wg') settings = getWireGuardGhostSettings({ newIfaceName });
if (type == 'openvpn') settings = getOpenVPNGhostSettings({ newIfaceName });
}

const properties = { connection: con, dev, settings };
Expand Down Expand Up @@ -234,6 +236,8 @@ export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) =
dlg = <BridgeDialog {...properties} />;
else if (type == 'wg')
dlg = <WireGuardDialog {...properties} />;
else if (type == 'openvpn')
dlg = <OpenVPNDialog {...properties} />;
else if (type == 'mtu')
dlg = <MtuDialog {...properties} />;
else if (type == 'mac')
Expand Down
6 changes: 5 additions & 1 deletion pkg/networkmanager/interfaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -733,8 +733,12 @@ export function NetworkManagerModel() {
}
};
}));
} else {
} else
delete result.wireguard;

if (settings.vpn) {
set("vpn", "service-type", "s", settings.vpn['service-type']);
set("vpn", "data", "a{ss}", settings.vpn.data);
}

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 @@ -139,6 +139,7 @@ export const NetworkPage = ({ privileged, operationInProgress, usage_monitor, pl
const actions = privileged && (
<>
<NetworkAction buttonText={_("Add VPN")} type='wg' />
<NetworkAction buttonText={_("Add OpenVPN")} type='openvpn' />
<NetworkAction buttonText={_("Add bond")} type='bond' />
<NetworkAction buttonText={_("Add team")} type='team' />
<NetworkAction buttonText={_("Add bridge")} type='bridge' />
Expand Down
124 changes: 124 additions & 0 deletions pkg/networkmanager/openvpn.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, { useContext, useState } from 'react';
import { Name, NetworkModal, dialogSave } from "./dialogs-common";
import { FileUpload } from '@patternfly/react-core/dist/esm/components/FileUpload/index.js';
import { FormGroup } from '@patternfly/react-core/dist/esm/components/Form/index.js';
import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput/index.js';
import cockpit from 'cockpit';
import { ModelContext } from './model-context';
import { useDialogs } from 'dialogs.jsx';

const _ = cockpit.gettext;

export function OpenVPNDialog({ settings, connection, dev }) {
const Dialogs = useDialogs();
const idPrefix = "network-openvpn-settings";
const model = useContext(ModelContext);

const [iface, setIface] = useState(settings.connection.interface_name);
const [remote, setRemote] = useState("");
const [caCertName, setCaCertName] = useState("");
const [caCertVal, setCaCertVal] = useState("");
const [userCertName, setUserCertName] = useState("");
const [userCertVal, setUserCertVal] = useState("");
const [userKeyName, setUserKeyName] = useState("");
const [userPrivateKey, setUserPrivateKey] = useState("");
const [dialogError, setDialogError] = useState("");

async function onSubmit() {
const user = await cockpit.user();
const caPath = `${user.home}/.cert/ca-${caCertName}.crt`;
const userCertPath = `${user.home}/.cert/cert-${userCertName}`;
const userKeyPath = `${user.home}/.cert/key-${userKeyName}.key`;

try {
// check if remote or certificates are empty
if (!remote.trim())
throw new Error(_("Remote cannot be empty."));
if (!caCertVal.trim())
throw new Error(_("CA certificate is empty."));
if (!userCertVal.trim())
throw new Error(_("User certificate is empty."));
if (!userPrivateKey.trim())
throw new Error(_("User private key is empty."));

await cockpit.script(`mkdir -p ${user.home}/.cert`);
await cockpit.script(`touch ${caPath} ${userCertPath} ${userKeyPath}`);
await cockpit.file(caPath).replace(caCertVal);
await cockpit.file(userCertPath).replace(userCertVal);
await cockpit.file(userKeyPath).replace(userPrivateKey);
} catch (e) {
setDialogError(e.message);
return;
}

function createSettingsObject() {
return {
...settings,
connection: {
...settings.connection,
type: 'vpn',
},
vpn: {
data: {
remote,
ca: caPath,
cert: userCertPath,
key: userKeyPath,
// hardcoding the bellow properties until we have a design for advanced dialog
'cert-pass-flags': '0',
'connection-type': 'tls',
dev: 'tun',
'push-peer-info': 'yes',
'remote-cert-tls': 'server'
},
'service-type': 'org.freedesktop.NetworkManager.openvpn'
}
};
}

dialogSave({
connection,
dev,
model,
settings: createSettingsObject(),
onClose: Dialogs.close,
setDialogError,
});
}

return (
<NetworkModal
title={!connection ? _("Add OpenVPN") : _("Edit OpenVPN settings")}
isCreateDialog={!connection}
onSubmit={onSubmit}
dialogError={dialogError}
idPrefix={idPrefix}
>
<Name idPrefix={idPrefix} iface={iface} setIface={setIface} />
<FormGroup label={_("Remote")}>
<TextInput id={idPrefix + '-remote-input'} value={remote} onChange={(_, val) => setRemote(val)} />
</FormGroup>
<FormGroup label={_("CA certificate")} id={idPrefix + '-ca-group'}>
<FileUpload id={idPrefix + '-ca'} filename={caCertName} onFileInputChange={(_, file) => setCaCertName(file.name)} type='text' onDataChange={(_, val) => setCaCertVal(val)} hideDefaultPreview />
</FormGroup>
<FormGroup label={_("User certificate")} id={idPrefix + '-user-cert-group'}>
<FileUpload id={idPrefix + '-user-cert'} filename={userCertName} onFileInputChange={(_, file) => setUserCertName(file.name)} type='text' onDataChange={(_, val) => setUserCertVal(val)} hideDefaultPreview />
</FormGroup>
<FormGroup label={_("User private key")} id={idPrefix + '-private-key-group'}>
<FileUpload id={idPrefix + '-user-key'} filename={userKeyName} onFileInputChange={(_, file) => setUserKeyName(file.name)} type='text' onDataChange={(_, val) => setUserPrivateKey(val)} hideDefaultPreview />
</FormGroup>
<FormGroup label={_("Test")}>
<input type='file' id={idPrefix + '-test-upload'} />
</FormGroup>
</NetworkModal>
);
}

export function getOpenVPNGhostSettings({ newIfaceName }) {
return {
connection: {
id: `con-${newIfaceName}`,
interface_name: newIfaceName,
}
};
}
2 changes: 1 addition & 1 deletion test/run
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ PREPARE_OPTS=""
RUN_OPTS=""
ALL_TESTS="$(test/common/run-tests --test-dir test/verify -l)"

RE_NETWORKING='Networking|Bonding|TestBridge|WireGuard|Firewall|Team|IPA|AD'
RE_NETWORKING='Networking|Bonding|TestBridge|WireGuard|OpenVPN|Firewall|Team|IPA|AD'
RE_STORAGE='Storage'
RE_EXPENSIVE='HostSwitching|MultiMachine|Updates|Superuser|Kdump|Pages'

Expand Down
191 changes: 191 additions & 0 deletions test/verify/check-networkmanager-openvpn
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)

import tempfile

import testlib


# Deps: openvpn, network-manager-openvpn, easy-rsa (optional)
class TestOpenVPN(testlib.MachineCase):
provision = {
"machine1": {"address": "192.168.100.11/24", "restrict": False},
"machine2": {"address": "192.168.100.12/24", "restrict": False},
}

def saveDialog(self):
b = self.browser
b.click("#network-openvpn-settings-save")

def testOpenvpn(self):
m1 = self.machines["machine1"]
m2 = self.machines["machine2"]
b = self.browser

# increasing productivity
keys_dir = "/etc/openvpn/server"
m1.execute("touch .hushlogin")
m2.execute("touch .hushlogin")

###############################################################
# SERVER #
###############################################################
m2.execute("openssl genrsa -out ca.key")
m2.execute("openssl req -x509 -new -sha512 -nodes -key ca.key -days 7307 -out ca.crt -subj '/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname'")
m2.execute(f"openssl dhparam -out {keys_dir}/dh2048.pem 2048")
m2.execute("openssl genrsa -out server.key")
host_conf = """ "
[req]
default_md = sha512
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
[req]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[req_distinguished_name]
C = AU
ST = Victoria
L = Melbourne
O = My Company
OU = My Division
CN = testing.com
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1 = testing.com
DNS.2 = *.testing.com
" """
m2.execute(f"echo {host_conf} >> host.conf")
m2.execute("openssl req -new -sha512 -nodes -key server.key -out server.csr -config host.conf")
host_ext_conf = """ "
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "My First Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = testing.com
DNS.2 = *.testing.com
" """
m2.execute(f"echo {host_ext_conf} >> host-ext.conf")
m2.execute("openssl x509 -req -sha512 -days 45 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extfile host-ext.conf")

m2.execute("openssl genrsa -out client.key")
m2.execute("openssl req -new -sha512 -nodes -key client.key -out client.csr -config host.conf")
m2.execute("openssl x509 -req -sha512 -days 3650 -CA ca.crt -CAkey ca.key -in client.csr -set_serial 01 -out client.crt")
m2.execute(f"mv -t {keys_dir} ca.key ca.crt server.key server.crt client.key client.crt")
server_conf = """ "
port 1194
proto udp
dev tun
ca ca.crt
cert server.crt
key server.key
dh dh2048.pem
server 10.8.0.0 255.255.255.0
keepalive 10 120
data-ciphers-fallback AES-256-CBC
persist-key
persist-tun
" """
m2.execute(f"echo {server_conf} >> {keys_dir}/server.conf")
if 'fedora' in m2.image:
# TODO: in fedora systemd starting openvpn leads to permission errors
m2.execute("setenforce 0")
m2.execute("systemctl enable --now openvpn-server@server")
m2.execute("firewall-cmd --add-port=1194/udp")

# create .ovpn file for client
ovpn_conf = """ "# start
client
dev tun
proto udp
remote 192.168.100.12 1194 udp
resolv-retry infinite
persist-key
persist-tun
remote-cert-tls server
data-ciphers-fallback AES-256-CBC
" """
m2.execute(f"echo {ovpn_conf} >> {keys_dir}/test.ovpn")
m2.execute(f"echo '<ca>' >> {keys_dir}/test.ovpn")
m2.execute(f"cat {keys_dir}/ca.crt >> {keys_dir}/test.ovpn")
m2.execute(f"echo '</ca>' >> {keys_dir}/test.ovpn")

m2.execute(f"echo '<cert>' >> {keys_dir}/test.ovpn")
m2.execute(f"cat {keys_dir}/client.crt >> {keys_dir}/test.ovpn")
m2.execute(f"echo '</cert>' >> {keys_dir}/test.ovpn")

m2.execute(f"echo '<key>' >> {keys_dir}/test.ovpn")
m2.execute(f"cat {keys_dir}/client.key >> {keys_dir}/test.ovpn")
m2.execute(f"echo '</key>' >> {keys_dir}/test.ovpn")

###############################################################
# CLIENT #
###############################################################

# download the .ovpn file from the server to client
m1.execute("ssh-keygen -t ed25519 -C '<comment>' -f '/root/.ssh/id_ed25519' -N ''")
pubkey = m1.execute("cat .ssh/id_ed25519.pub")
m2.execute(f"echo '{pubkey.strip()}' >> .ssh/authorized_keys")
m1.execute(f"scp -o StrictHostKeyChecking=accept-new 192.168.100.12:{keys_dir}/test.ovpn .")
m1.execute(f"scp -o StrictHostKeyChecking=accept-new 192.168.100.12:{keys_dir}/client.* .")
m1.execute(f"scp -o StrictHostKeyChecking=accept-new 192.168.100.12:{keys_dir}/ca.crt .")

# FIX: the client also doesn't work when selinux is enforcing
m1.execute("setenforce 0")

self.login_and_go("/network")
b.click("#networking-add-openvpn")
b.wait_visible("#network-openvpn-settings-dialog")
iface_name = b.val("#network-openvpn-settings-interface-name-input")

b.set_input_text("#network-openvpn-settings-remote-input", "192.168.100.12:1194:udp")
ca = m1.execute("cat ca.crt")
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tf_ca:
b.upload_file(".pf-v5-c-file-upload:has(#network-openvpn-settings-ca-filename) input[type=file]", tf_ca.name)
self.saveDialog()
b.wait_visible(".pf-v5-c-alert:contains('CA certificate is empty.')")
b.click("#network-openvpn-settings-ca-group button:contains('Clear')")
tf_ca.write(ca)
b.upload_file(".pf-v5-c-file-upload:has(#network-openvpn-settings-ca-filename) input[type=file]", tf_ca.name)

user_cert = m1.execute("cat client.crt")
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tf_user_cert:
b.upload_file(".pf-v5-c-file-upload:has(#network-openvpn-settings-user-cert-filename) input[type=file]", tf_user_cert.name)
self.saveDialog()
b.wait_visible(".pf-v5-c-alert:contains('User certificate is empty.')")
b.click("#network-openvpn-settings-user-cert-group button:contains('Clear')")
tf_user_cert.write(user_cert)
b.upload_file(".pf-v5-c-file-upload:has(#network-openvpn-settings-user-cert-filename) input[type=file]", tf_user_cert.name)

user_key = m1.execute("cat client.key")
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tf_user_key:
b.upload_file(".pf-v5-c-file-upload:has(#network-openvpn-settings-user-key-filename) input[type=file]", tf_user_key.name)
self.saveDialog()
b.wait_visible(".pf-v5-c-alert:contains('User private key is empty.')")
b.click("#network-openvpn-settings-private-key-group button:contains('Clear')")
tf_user_key.write(user_key)
b.upload_file(".pf-v5-c-file-upload:has(#network-openvpn-settings-user-key-filename) input[type=file]", tf_user_key.name)

self.saveDialog()
b.wait_not_present("#network-openvpn-settings-dialog")
b.click(f"#networking-interfaces button:contains('{iface_name}')")
b.click(".pf-v5-c-switch__toggle")
b.go("/network")
# FIX: the creation of bogus NM route, this could be either a problem with client config or server config not push the correct routes to client during initialization
m1.execute("until ip route | grep -q '192.168.100.12 via 172.27.0.2'; do sleep 1; done")
m1.execute("ip route del 192.168.100.12 via 172.27.0.2")
m1.execute("sleep 5; ping -c 1 10.8.0.1")
m2.execute("ping -c 1 10.8.0.6")


if __name__ == "__main__":
testlib.test_main()

0 comments on commit ca446c9

Please sign in to comment.