-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
515b6bc
commit ca446c9
Showing
6 changed files
with
326 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |