From de20a1c70d5788187b8ea36e59493e301d770a65 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 20 Nov 2024 21:47:54 -0300 Subject: [PATCH] [feature] Added data-ciphers #322 Closes #322 --- netjsonconfig/backends/openvpn/converters.py | 28 +++ netjsonconfig/backends/openvpn/schema.py | 174 +++++++++++---- tests/openvpn/test_backend.py | 214 +++++++++++++++++++ tests/openvpn/test_parser.py | 160 ++++++++++++++ tests/openwrt/test_openvpn.py | 66 ++++++ 5 files changed, 605 insertions(+), 37 deletions(-) diff --git a/netjsonconfig/backends/openvpn/converters.py b/netjsonconfig/backends/openvpn/converters.py index 5853e826b..d8e5ed4e9 100644 --- a/netjsonconfig/backends/openvpn/converters.py +++ b/netjsonconfig/backends/openvpn/converters.py @@ -53,9 +53,23 @@ def __intermediate_vpn(self, config, remove=None): # do not display status-version if status directive not present if 'status' not in config and 'status_version' in config: del config['status_version'] + config = self.__output_data_ciphers(config) config = self.__add_tls_auth_key(config) return self.sorted_dict(config) + def __output_data_ciphers(self, config): + data_ciphers = config.get('data_ciphers', None) + if not data_ciphers: + return config + output = '' + for cipher in data_ciphers: + cipher_text = cipher['cipher'] + if cipher['optional']: + cipher_text = f'?{cipher_text}' + output = f'{output}:{cipher_text}' + config['data_ciphers'] = output[1:] + return config + def __add_tls_auth_key(self, config): tls_auth = config.get('tls_auth', None) if not tls_auth: @@ -109,4 +123,18 @@ def __netjson_vpn(self, vpn): else: remote.append(dict(host=items[0], port=int(items[1]))) vpn['remote'] = remote + vpn = self.__netjson_data_ciphers(vpn) + return vpn + + def __netjson_data_ciphers(self, vpn): + data_ciphers_text = vpn.get('data_ciphers') + if not data_ciphers_text: + return vpn + data_ciphers = [] + ciphers = data_ciphers_text.split(':') + for cipher in ciphers: + optional = cipher.startswith('?') + cipher_text = cipher if not optional else cipher[1:] + data_ciphers.append({'cipher': cipher_text, 'optional': optional}) + vpn['data_ciphers'] = data_ciphers return vpn diff --git a/netjsonconfig/backends/openvpn/schema.py b/netjsonconfig/backends/openvpn/schema.py index 6d50a318c..afa3f0e83 100644 --- a/netjsonconfig/backends/openvpn/schema.py +++ b/netjsonconfig/backends/openvpn/schema.py @@ -5,6 +5,91 @@ from ...schema import schema as default_schema +data_ciphers = [ + "AES-128-CBC", + "AES-128-CFB", + "AES-128-CFB1", + "AES-128-CFB8", + "AES-128-GCM", + "AES-128-OFB", + "AES-192-CBC", + "AES-192-CFB", + "AES-192-CFB1", + "AES-192-CFB8", + "AES-192-GCM", + "AES-192-OFB", + "AES-256-CBC", + "AES-256-CFB", + "AES-256-CFB1", + "AES-256-CFB8", + "AES-256-GCM", + "AES-256-OFB", + "ARIA-128-CBC", + "ARIA-128-CFB", + "ARIA-128-CFB1", + "ARIA-128-CFB8", + "ARIA-128-OFB", + "ARIA-192-CBC", + "ARIA-192-CFB", + "ARIA-192-CFB1", + "ARIA-192-CFB8", + "ARIA-192-OFB", + "ARIA-256-CBC", + "ARIA-256-CFB", + "ARIA-256-CFB1", + "ARIA-256-CFB8", + "ARIA-256-OFB", + "CAMELLIA-128-CBC", + "CAMELLIA-128-CFB", + "CAMELLIA-128-CFB1", + "CAMELLIA-128-CFB8", + "CAMELLIA-128-OFB", + "CAMELLIA-192-CBC", + "CAMELLIA-192-CFB", + "CAMELLIA-192-CFB1", + "CAMELLIA-192-CFB8", + "CAMELLIA-192-OFB", + "CAMELLIA-256-CBC", + "CAMELLIA-256-CFB", + "CAMELLIA-256-CFB1", + "CAMELLIA-256-CFB8", + "CAMELLIA-256-OFB", + "CHACHA20-POLY1305", + "SEED-CBC", + "SEED-CFB", + "SEED-OFB", + "SM4-CBC", + "SM4-CFB", + "SM4-OFB", + "BF-CBC", + "BF-CFB", + "BF-OFB", + "CAST5-CBC", + "CAST5-CFB", + "CAST5-OFB", + "DES-CBC", + "DES-CFB", + "DES-CFB1", + "DES-CFB8", + "DES-EDE-CBC", + "DES-EDE-CFB", + "DES-EDE-OFB", + "DES-EDE3-CBC", + "DES-EDE3-CFB", + "DES-EDE3-CFB1", + "DES-EDE3-CFB8", + "DES-EDE3-OFB", + "DES-OFB", + "DESX-CBC", + "RC2-40-CBC", + "RC2-64-CBC", + "RC2-CBC", + "RC2-CFB", + "RC2-OFB", + "none", +] +default_cipher = "AES-256-GCM" + base_openvpn_schema = { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", @@ -102,46 +187,61 @@ "default": "SHA1", "propertyOrder": 11, }, + "data_ciphers": { + "title": "data ciphers", + "description": ( + "Restrict the allowed ciphers to be negotiated " + "to the ciphers in this list." + ), + "type": "array", + "additionalItems": True, + "propertyOrder": 12.0, + "minItems": 1, + "default": [ + {"cipher": "AES-256-GCM", "optional": False}, + {"cipher": "AES-128-GCM", "optional": False}, + ], + "items": { + "type": "object", + "required": ["cipher", "optional"], + "properties": { + "cipher": { + "type": "string", + "enum": [""] + data_ciphers, + "default": "", + "propertyOrder": 1, + }, + "optional": { + "type": "boolean", + "default": False, + "format": "checkbox", + "propertyOrder": 2, + }, + }, + }, + }, + "data_ciphers_fallback": { + "title": "data ciphers fallback", + "type": "string", + "description": ( + "Configure a cipher that is used to fall back to if we " + "could not determine which cipher the peer is willing to use." + ), + "enum": data_ciphers, + "default": default_cipher, + "propertyOrder": 12.1, + }, "cipher": { "title": "cipher", "type": "string", - "description": "Encrypt data channel packets with cipher algorithm", - "enum": [ - "AES-128-CBC", - "AES-128-CFB", - "AES-128-CFB1", - "AES-128-CFB8", - "AES-128-GCM", - "AES-128-OFB", - "AES-192-CBC", - "AES-192-CFB", - "AES-192-CFB1", - "AES-192-CFB8", - "AES-192-GCM", - "AES-192-OFB", - "AES-256-CBC", - "AES-256-CFB", - "AES-256-CFB1", - "AES-256-CFB8", - "AES-256-GCM", - "AES-256-OFB", - "BF-CBC", - "BF-CFB", - "BF-OFB", - "CAMELLIA-128-CBC", - "CAMELLIA-128-CFB1", - "CAMELLIA-128-CFB8", - "CAMELLIA-128-OFB", - "CAMELLIA-192-CBC", - "CAMELLIA-192-CFB", - "CAMELLIA-192-CFB1", - "CAMELLIA-192-CFB8", - "CAMELLIA-192-OFB", - "CAMELLIA-256-CBC", - "none", - ], - "default": "BF-CBC", - "propertyOrder": 12, + "description": ( + "Encrypt data channel packets with cipher algorithm. " + "This option is deprecated in favour of data-ciphers " + "and data-ciphers-fallback." + ), + "enum": data_ciphers, + "default": default_cipher, + "propertyOrder": 12.2, }, "engine": { "title": "engine", diff --git a/tests/openvpn/test_backend.py b/tests/openvpn/test_backend.py index dc1598117..866140043 100644 --- a/tests/openvpn/test_backend.py +++ b/tests/openvpn/test_backend.py @@ -103,6 +103,107 @@ def test_server_mode(self): tls-server user nobody verb 3 +""" + self.assertEqual(c.render(), expected) + + def test_server_mode_with_data_ciphers(self): + c = OpenVpn( + { + "openvpn": [ + { + "auth": "SHA1", + "auth_user_pass_verify": "", + "auth_nocache": True, + "ca": "ca.pem", + "cert": "cert.pem", + "data_ciphers": [ + {"cipher": "AES-256-GCM", "optional": False}, + {"cipher": "AES-128-GCM", "optional": False}, + {"cipher": "CHACHA20-POLY1305", "optional": True}, + ], + "data_ciphers_fallback": "AES-128-GCM", + "cipher": "AES-128-GCM", + "client_cert_not_required": False, + "client_to_client": False, + "comp_lzo": "adaptive", + "crl_verify": "crl.pem", + "dev": "tap0", + "dev_type": "tap", + "dh": "dh.pem", + "down": "", + "duplicate_cn": True, + "engine": "rsax", + "fast_io": True, + "fragment": 0, + "group": "nogroup", + "keepalive": "20 60", + "key": "key.pem", + "local": "", + "log": "/var/log/openvpn.log", + "mode": "server", + "name": "test-server", + "mssfix": 1450, + "mtu_disc": "no", + "mtu_test": False, + "mute": 0, + "mute_replay_warnings": True, + "ns_cert_type": "", + "persist_key": True, + "persist_tun": True, + "port": 1194, + "proto": "udp", + "script_security": 0, + "secret": "", + "status": "/var/log/openvpn.status 10", + "status_version": 1, + "tls_server": True, + "tls_auth": "tls_auth.key 0", + "tun_ipv6": False, + "up": "", + "up_delay": 0, + "user": "nobody", + "username_as_common_name": False, + "verb": 3, + } + ] + } + ) + expected = """# openvpn config: test-server + +auth SHA1 +auth-nocache +ca ca.pem +cert cert.pem +cipher AES-128-GCM +comp-lzo adaptive +crl-verify crl.pem +data-ciphers AES-256-GCM:AES-128-GCM:?CHACHA20-POLY1305 +data-ciphers-fallback AES-128-GCM +dev tap0 +dev-type tap +dh dh.pem +duplicate-cn +engine rsax +fast-io +group nogroup +keepalive 20 60 +key key.pem +log /var/log/openvpn.log +mode server +mssfix 1450 +mtu-disc no +mute-replay-warnings +persist-key +persist-tun +port 1194 +proto udp +script-security 0 +status /var/log/openvpn.status 10 +status-version 1 +tls-auth tls_auth.key 0 +tls-server +user nobody +verb 3 """ self.assertEqual(c.render(), expected) @@ -208,6 +309,119 @@ def test_client_mode(self): up-delay 10 user nobody verb 1 +""" + self.assertEqual(c.render(), expected) + + def test_client_mode_data_cihpers(self): + c = OpenVpn( + { + "openvpn": [ + { + "auth": "SHA256", + "auth_user_pass": "", + "auth_nocache": True, + "ca": "ca.pem", + "cert": "cert.pem", + "cipher": "AES-128-GCM", + "data_ciphers": [ + {"cipher": "AES-256-GCM", "optional": False}, + {"cipher": "AES-128-GCM", "optional": False}, + {"cipher": "CHACHA20-POLY1305", "optional": True}, + ], + "data_ciphers_fallback": "AES-128-GCM", + "comp_lzo": "adaptive", + "dev": "tun0", + "dev_type": "tun", + "down": "/home/user/down-command.sh", + "engine": "", + "fast_io": False, + "fragment": 0, + "group": "", + "keepalive": "", + "key": "key.pem", + "local": "", + "log": "/var/log/openvpn.log", + "mode": "p2p", + "mssfix": 1450, + "mtu_disc": "yes", + "mtu_test": True, + "mute": 10, + "mute_replay_warnings": True, + "name": "test-client", + "nobind": True, + "ns_cert_type": "server", + "persist_key": True, + "persist_tun": True, + "port": 1195, + "proto": "tcp-client", + "pull": True, + "remote": [ + {"host": "vpn1.test.com", "port": 1194}, + {"host": "176.9.43.231", "port": 1195, "proto": "udp4"}, + {"host": "176.9.43.232", "port": 1195, "proto": "auto"}, + {"host": "176.9.43.233", "port": 1196, "proto": "udp6"}, + ], + "resolv_retry": "infinite", + "script_security": 1, + "secret": "", + "status": "/var/log/openvpn.status 30", + "status_version": 1, + "tls_client": True, + "tls_auth": "tls_auth.key 1", + "topology": "p2p", + "tun_ipv6": True, + "up": "/home/user/up-command.sh", + "up_delay": 10, + "user": "nobody", + "verb": 1, + } + ] + } + ) + expected = """# openvpn config: test-client + +auth SHA256 +auth-nocache +ca ca.pem +cert cert.pem +cipher AES-128-GCM +comp-lzo adaptive +data-ciphers AES-256-GCM:AES-128-GCM:?CHACHA20-POLY1305 +data-ciphers-fallback AES-128-GCM +dev tun0 +dev-type tun +down /home/user/down-command.sh +key key.pem +log /var/log/openvpn.log +mode p2p +mssfix 1450 +mtu-disc yes +mtu-test +mute 10 +mute-replay-warnings +nobind +ns-cert-type server +persist-key +persist-tun +port 1195 +proto tcp-client +pull +remote vpn1.test.com 1194 +remote 176.9.43.231 1195 udp4 +remote 176.9.43.232 1195 +remote 176.9.43.233 1196 udp6 +resolv-retry infinite +script-security 1 +status /var/log/openvpn.status 30 +status-version 1 +tls-auth tls_auth.key 1 +tls-client +topology p2p +tun-ipv6 +up /home/user/up-command.sh +up-delay 10 +user nobody +verb 1 """ self.assertEqual(c.render(), expected) diff --git a/tests/openvpn/test_parser.py b/tests/openvpn/test_parser.py index b153d8a56..6eac2bb5b 100644 --- a/tests/openvpn/test_parser.py +++ b/tests/openvpn/test_parser.py @@ -43,6 +43,166 @@ def test_parse_text(self): } self.assertDictEqual(o.config, expected) + def test_parse_server(self): + native = """# openvpn config: test-server + +auth SHA1 +auth-nocache +ca ca.pem +cert cert.pem +cipher AES-128-GCM +comp-lzo adaptive +crl-verify crl.pem +dev tap0 +dev-type tap +dh dh.pem +duplicate-cn +engine rsax +fast-io +group nogroup +keepalive 20 60 +key key.pem +log /var/log/openvpn.log +mode server +mssfix 1450 +mtu-disc no +mute-replay-warnings +persist-key +persist-tun +port 1194 +proto udp +script-security 0 +status /var/log/openvpn.status 10 +status-version 1 +tls-server +user nobody +verb 3 +""" + expected = { + "openvpn": [ + { + "auth": "SHA1", + "auth_nocache": True, + "ca": "ca.pem", + "cert": "cert.pem", + "cipher": "AES-128-GCM", + "comp_lzo": "adaptive", + "crl_verify": "crl.pem", + "dev": "tap0", + "dev_type": "tap", + "dh": "dh.pem", + "duplicate_cn": True, + "engine": "rsax", + "fast_io": True, + "group": "nogroup", + "keepalive": "20 60", + "key": "key.pem", + "log": "/var/log/openvpn.log", + "mode": "server", + "name": "test-server", + "mssfix": 1450, + "mtu_disc": "no", + "mute_replay_warnings": True, + "persist_key": True, + "persist_tun": True, + "port": 1194, + "proto": "udp", + "script_security": 0, + "status": "/var/log/openvpn.status 10", + "status_version": 1, + "tls_server": True, + "user": "nobody", + "verb": 3, + } + ] + } + o = OpenVpn(native=native) + self.assertDictEqual(o.config, expected) + + def test_parse_data_ciphers(self): + native = """# openvpn config: test-server + +auth SHA1 +auth-nocache +ca ca.pem +cert cert.pem +cipher AES-128-GCM +comp-lzo adaptive +crl-verify crl.pem +data-ciphers AES-256-GCM:AES-128-GCM:?CHACHA20-POLY1305 +data-ciphers-fallback AES-128-GCM +dev tap0 +dev-type tap +dh dh.pem +duplicate-cn +engine rsax +fast-io +group nogroup +keepalive 20 60 +key key.pem +log /var/log/openvpn.log +mode server +mssfix 1450 +mtu-disc no +mute-replay-warnings +persist-key +persist-tun +port 1194 +proto udp +script-security 0 +status /var/log/openvpn.status 10 +status-version 1 +tls-server +user nobody +verb 3 +""" + expected = { + "openvpn": [ + { + "auth": "SHA1", + "auth_nocache": True, + "ca": "ca.pem", + "cert": "cert.pem", + "cipher": "AES-128-GCM", + "comp_lzo": "adaptive", + "crl_verify": "crl.pem", + "data_ciphers": [ + {"cipher": "AES-256-GCM", "optional": False}, + {"cipher": "AES-128-GCM", "optional": False}, + {"cipher": "CHACHA20-POLY1305", "optional": True}, + ], + "data_ciphers_fallback": "AES-128-GCM", + "dev": "tap0", + "dev_type": "tap", + "dh": "dh.pem", + "duplicate_cn": True, + "engine": "rsax", + "fast_io": True, + "group": "nogroup", + "keepalive": "20 60", + "key": "key.pem", + "log": "/var/log/openvpn.log", + "mode": "server", + "name": "test-server", + "mssfix": 1450, + "mtu_disc": "no", + "mute_replay_warnings": True, + "persist_key": True, + "persist_tun": True, + "port": 1194, + "proto": "udp", + "script_security": 0, + "status": "/var/log/openvpn.status 10", + "status_version": 1, + "tls_server": True, + "user": "nobody", + "verb": 3, + } + ] + } + o = OpenVpn(native=native) + self.assertDictEqual(o.config, expected) + def test_parse_exception(self): try: OpenVpn(native=10) diff --git a/tests/openwrt/test_openvpn.py b/tests/openwrt/test_openvpn.py index 4bfe812f6..1e4075399 100644 --- a/tests/openwrt/test_openvpn.py +++ b/tests/openwrt/test_openvpn.py @@ -54,6 +54,17 @@ class TestOpenVpn(_TabsMixin, unittest.TestCase): } ] } + _server_netjson_data_ciphers = deepcopy(_server_netjson) + _server_netjson_data_ciphers['openvpn'][0].update( + { + 'data_ciphers': [ + {'cipher': 'AES-256-GCM', 'optional': False}, + {'cipher': 'AES-128-GCM', 'optional': False}, + {'cipher': 'CHACHA20-POLY1305', 'optional': True}, + ], + 'data_ciphers_fallback': 'AES-128-GCM', + } + ) _server_uci = """package openvpn config openvpn 'test_server' @@ -96,18 +107,73 @@ class TestOpenVpn(_TabsMixin, unittest.TestCase): option username_as_common_name '0' option verb '3' """ + _server_uci_data_ciphers = """package openvpn + +config openvpn 'test_server' + option auth 'SHA1' + option ca 'ca.pem' + option cert 'cert.pem' + option cipher 'BF-CBC' + option client_cert_not_required '0' + option client_to_client '0' + option comp_lzo 'yes' + option crl_verify 'crl.pem' + option data_ciphers 'AES-256-GCM:AES-128-GCM:?CHACHA20-POLY1305' + option data_ciphers_fallback 'AES-128-GCM' + option dev 'tap0' + option dev_type 'tap' + option dh 'dh.pem' + option duplicate_cn '1' + option enabled '1' + option engine 'rsax' + option fast_io '1' + option group 'nogroup' + option keepalive '20 60' + option key 'key.pem' + option log '/var/log/openvpn.log' + option mode 'server' + option mssfix '1450' + option mtu_disc 'no' + option mtu_test '0' + option mute '0' + option mute_replay_warnings '1' + option persist_key '1' + option persist_tun '1' + option port '1194' + option proto 'udp' + option script_security '0' + option status '/var/log/openvpn.status 10' + option status_version '1' + option tls_server '1' + option tun_ipv6 '0' + option up_delay '0' + option user 'nobody' + option username_as_common_name '0' + option verb '3' +""" def test_render_server_mode(self): c = OpenWrt(self._server_netjson) expected = self._tabs(self._server_uci) self.assertEqual(c.render(), expected) + def test_render_server_mode_data_ciphers(self): + c = OpenWrt(self._server_netjson_data_ciphers) + expected = self._tabs(self._server_uci_data_ciphers) + self.assertEqual(c.render(), expected) + def test_parse_server_mode(self): c = OpenWrt(native=self._server_uci) expected = deepcopy(self._server_netjson) del expected['openvpn'][0]['fragment'] self.assertEqual(c.config, expected) + def test_parse_server_mode_data_ciphers(self): + c = OpenWrt(native=self._server_uci_data_ciphers) + expected = deepcopy(self._server_netjson_data_ciphers) + del expected['openvpn'][0]['fragment'] + self.assertEqual(c.config, expected) + _client_netjson = { "openvpn": [ {