From dd3238e0dcb09bcd24074ef62864b278018c4f2f Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 3 Apr 2021 16:48:28 -0500 Subject: [PATCH 1/9] [feature] Add Wireguard support #185 Related to #185 --- docs/source/backends/wireguard.rst | 13 ++ docs/source/index.rst | 1 + netjsonconfig/__init__.py | 2 + .../backends/openwrt/converters/interfaces.py | 14 ++- netjsonconfig/backends/wireguard/__init__.py | 0 .../backends/wireguard/converters.py | 30 +++++ netjsonconfig/backends/wireguard/parser.py | 21 ++++ netjsonconfig/backends/wireguard/renderer.py | 15 +++ netjsonconfig/backends/wireguard/schema.py | 97 ++++++++++++++ .../wireguard/templates/wireguard.jinja2 | 20 +++ netjsonconfig/backends/wireguard/wireguard.py | 37 ++++++ netjsonconfig/utils.py | 2 +- setup.cfg | 2 +- tests/wireguard/__init__.py | 0 tests/wireguard/test_backend.py | 118 ++++++++++++++++++ 15 files changed, 367 insertions(+), 5 deletions(-) create mode 100644 docs/source/backends/wireguard.rst create mode 100644 netjsonconfig/backends/wireguard/__init__.py create mode 100644 netjsonconfig/backends/wireguard/converters.py create mode 100644 netjsonconfig/backends/wireguard/parser.py create mode 100644 netjsonconfig/backends/wireguard/renderer.py create mode 100644 netjsonconfig/backends/wireguard/schema.py create mode 100644 netjsonconfig/backends/wireguard/templates/wireguard.jinja2 create mode 100644 netjsonconfig/backends/wireguard/wireguard.py create mode 100644 tests/wireguard/__init__.py create mode 100644 tests/wireguard/test_backend.py diff --git a/docs/source/backends/wireguard.rst b/docs/source/backends/wireguard.rst new file mode 100644 index 000000000..61fd853e7 --- /dev/null +++ b/docs/source/backends/wireguard.rst @@ -0,0 +1,13 @@ +================= +Wireguard Backend +================= + +.. include:: ../_github.rst + +The ``Wireguard`` backend allows to generate Wireguard compatible configurations. + +This backend is designed to allow updating the configuration of a wireguard peer +automatically when the configuration of a VPN server is changed in OpenWISP, or +when a new peer is added to OpenWISP (a new device with wireguard VPN client template). + +TODO... diff --git a/docs/source/index.rst b/docs/source/index.rst index 6ece7b715..8e5c9a69a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -59,6 +59,7 @@ Contents: /backends/openwrt /backends/openwisp /backends/openvpn + /backends/wireguard /backends/create_your_backend /general/commandline_utility /general/running_tests diff --git a/netjsonconfig/__init__.py b/netjsonconfig/__init__.py index 054323f78..41027f18b 100644 --- a/netjsonconfig/__init__.py +++ b/netjsonconfig/__init__.py @@ -5,6 +5,7 @@ from .backends.openvpn.openvpn import OpenVpn # noqa from .backends.openwisp.openwisp import OpenWisp # noqa from .backends.openwrt.openwrt import OpenWrt # noqa +from .backends.wireguard.wireguard import Wireguard # noqa from .version import VERSION, __version__, get_version # noqa @@ -13,6 +14,7 @@ def get_backends(): 'openwrt': OpenWrt, 'openwisp': OpenWisp, 'openvpn': OpenVpn, + 'wireguard': Wireguard, } logger = logging.getLogger(__name__) diff --git a/netjsonconfig/backends/openwrt/converters/interfaces.py b/netjsonconfig/backends/openwrt/converters/interfaces.py index df6476ce2..1629ff540 100644 --- a/netjsonconfig/backends/openwrt/converters/interfaces.py +++ b/netjsonconfig/backends/openwrt/converters/interfaces.py @@ -45,6 +45,8 @@ def __intermediate_addresses(self, interface): converts NetJSON address to UCI intermediate data structure """ + if interface.get('proto') == 'wireguard': + return self.__intermediate_wg_addresses(interface) address_list = self.get_copy(interface, 'addresses') # do not ignore interfaces if they do not contain any address if not address_list: @@ -83,14 +85,20 @@ def __intermediate_addresses(self, interface): result += dhcp return result + def __intermediate_wg_addresses(self, interface): + address_list = interface.pop('wg_addresses') + static = {'addresses': address_list, 'proto': interface['proto']} + return [static] + def __intermediate_interface(self, interface, uci_name): """ converts NetJSON interface to UCI intermediate data structure """ - interface.update( - {'.type': 'interface', '.name': uci_name, 'ifname': interface.pop('name')} - ) + interface.update({'.type': 'interface', '.name': uci_name}) + name = interface.pop('name') + if interface.get('proto') != 'wireguard': + interface['ifname'] = name if 'network' in interface: del interface['network'] if 'mac' in interface: diff --git a/netjsonconfig/backends/wireguard/__init__.py b/netjsonconfig/backends/wireguard/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netjsonconfig/backends/wireguard/converters.py b/netjsonconfig/backends/wireguard/converters.py new file mode 100644 index 000000000..de779949a --- /dev/null +++ b/netjsonconfig/backends/wireguard/converters.py @@ -0,0 +1,30 @@ +from ..base.converter import BaseConverter +from .schema import schema + + +class Wireguard(BaseConverter): + netjson_key = 'wireguard' + intermediate_key = 'wireguard' + _schema = schema + + def to_intermediate_loop(self, block, result, index=None): + vpn = self.__intermediate_vpn(block) + result.setdefault('wireguard', []) + result['wireguard'].append(vpn) + return result + + def __intermediate_vpn(self, config, remove=None): + config['ListenPort'] = config.pop('port') + config['PrivateKey'] = config.pop('private_key') + config['peers'] = self.__intermediate_peers(config.get('peers', [])) + return self.sorted_dict(config) + + def __intermediate_peers(self, peers): + peer_list = [] + for peer in peers: + peer['AllowedIPs'] = peer.pop('allowed_ips') + peer['PublicKey'] = peer.pop('public_key') + peer['Endpoint'] = peer.pop('endpoint', None) + peer['PreSharedKey'] = peer.pop('preshared_key', None) + peer_list.append(self.sorted_dict(peer)) + return peer_list diff --git a/netjsonconfig/backends/wireguard/parser.py b/netjsonconfig/backends/wireguard/parser.py new file mode 100644 index 000000000..3e2d70b2a --- /dev/null +++ b/netjsonconfig/backends/wireguard/parser.py @@ -0,0 +1,21 @@ +import re + +from ..base.parser import BaseParser + +vpn_pattern = re.compile('^# wireguard config:\s', flags=re.MULTILINE) +config_pattern = re.compile('^([^\s]*) ?(.*)$') +config_suffix = '.conf' + + +class WireguardParser(BaseParser): + def parse_text(self, config): + raise NotImplementedError() + + def parse_tar(self, tar): + raise NotImplementedError() + + def _get_vpns(self, text): + raise NotImplementedError() + + def _get_config(self, contents): + raise NotImplementedError() diff --git a/netjsonconfig/backends/wireguard/renderer.py b/netjsonconfig/backends/wireguard/renderer.py new file mode 100644 index 000000000..aafce2087 --- /dev/null +++ b/netjsonconfig/backends/wireguard/renderer.py @@ -0,0 +1,15 @@ +from ..base.renderer import BaseRenderer + + +class WireguardRenderer(BaseRenderer): + """ + Wireguard Renderer + """ + + def cleanup(self, output): + # remove indentations + output = output.replace(' ', '') + # remove last newline + if output.endswith('\n\n'): + output = output[0:-1] + return output diff --git a/netjsonconfig/backends/wireguard/schema.py b/netjsonconfig/backends/wireguard/schema.py new file mode 100644 index 000000000..721d91ff7 --- /dev/null +++ b/netjsonconfig/backends/wireguard/schema.py @@ -0,0 +1,97 @@ +""" +Wireguard specific JSON-Schema definition +""" +from copy import deepcopy + +from ...schema import schema as default_schema + +base_wireguard_schema = { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": True, + "properties": { + "wireguard": { + "type": "array", + "title": "Wireguard", + "uniqueItems": True, + "additionalItems": True, + "propertyOrder": 12, + "items": { + "type": "object", + "title": "Wireguard tunnel", + "additionalProperties": True, + "required": ["name", "port", "private_key"], + "properties": { + "name": { + "title": "interface name", + "description": "Wireguard interface name", + "type": "string", + "minLength": 2, + "maxLength": 15, + "pattern": "^[^\\s]*$", + "propertyOrder": 1, + }, + "port": { + "title": "port", + "type": "integer", + "default": 51820, + "maximum": 65535, + "minimum": 1, + "propertyOrder": 2, + }, + "private_key": { + "title": "private key", + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^[^\\s]*$", + "propertyOrder": 3, + }, + "peers": { + "type": "array", + "title": "Peers", + "uniqueItems": True, + "additionalItems": True, + "propertyOrder": 11, + "items": { + "type": "object", + "title": "Peer", + "required": ["public_key", "allowed_ips"], + "properties": { + "public_key": { + "title": "public key", + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^[^\\s]*$", + "propertyOrder": 1, + }, + "allowed_ips": { + "title": "public key", + "type": "string", + "propertyOrder": 2, + }, + "endpoint": { + "title": "public key", + "type": "string", + "propertyOrder": 3, + }, + "preshared_key": { + "title": "pre-shared key", + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^[^\\s]*$", + "propertyOrder": 4, + }, + }, + }, + }, + }, + }, + } + }, +} + +schema = deepcopy(base_wireguard_schema) +schema['properties']['files'] = default_schema['properties']['files'] diff --git a/netjsonconfig/backends/wireguard/templates/wireguard.jinja2 b/netjsonconfig/backends/wireguard/templates/wireguard.jinja2 new file mode 100644 index 000000000..23b6cf5b8 --- /dev/null +++ b/netjsonconfig/backends/wireguard/templates/wireguard.jinja2 @@ -0,0 +1,20 @@ +{% for vpn in data.wireguard %} + # wireguard config: {{ vpn.name }} + + [Interface] + {% for key, value in vpn.items() %} + {% if key not in ['name', 'peers'] %} + {{ key }} = {{ value }} + {% endif %} + {% endfor %} + + {% for peer in vpn.peers %} + [Peer] + {% for key, value in peer.items() %} + {% if value %} + {{ key }} = {{ value }} + {% endif %} + {% endfor %} + + {% endfor %} +{% endfor %} diff --git a/netjsonconfig/backends/wireguard/wireguard.py b/netjsonconfig/backends/wireguard/wireguard.py new file mode 100644 index 000000000..17b0b7d79 --- /dev/null +++ b/netjsonconfig/backends/wireguard/wireguard.py @@ -0,0 +1,37 @@ +from ..base.backend import BaseBackend +from . import converters +from .parser import config_suffix, vpn_pattern +from .renderer import WireguardRenderer +from .schema import schema + + +class Wireguard(BaseBackend): + schema = schema + converters = [converters.Wireguard] + renderer = WireguardRenderer + + def _generate_contents(self, tar): + """ + Adds configuration files to tarfile instance. + + :param tar: tarfile instance + :returns: None + """ + text = self.render(files=False) + # create a list with all the packages (and remove empty entries) + vpn_instances = vpn_pattern.split(text) + if '' in vpn_instances: + vpn_instances.remove('') + # create a file for each VPN + for vpn in vpn_instances: + lines = vpn.split('\n') + vpn_name = lines[0] + text_contents = '\n'.join(lines[2:]) + # do not end with double new line + if text_contents.endswith('\n\n'): + text_contents = text_contents[0:-1] + self._add_file( + tar=tar, + name='{0}{1}'.format(vpn_name, config_suffix), + contents=text_contents, + ) diff --git a/netjsonconfig/utils.py b/netjsonconfig/utils.py index 2b02c13ff..797ad3a2a 100644 --- a/netjsonconfig/utils.py +++ b/netjsonconfig/utils.py @@ -105,7 +105,7 @@ def evaluate_vars(data, context=None): else: pattern = var_pattern if var in context: - data = re.sub(pattern, context[var], data) + data = re.sub(pattern, str(context[var]), data) return data diff --git a/setup.cfg b/setup.cfg index 4a0376e4d..ce5a070d1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ universal=1 [flake8] max-line-length=110 -max-complexity=9 +max-complexity=10 # W503: line break before or after operator # W504: line break after or after operator # W605: invalid escape sequence diff --git a/tests/wireguard/__init__.py b/tests/wireguard/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/wireguard/test_backend.py b/tests/wireguard/test_backend.py new file mode 100644 index 000000000..d160162ef --- /dev/null +++ b/tests/wireguard/test_backend.py @@ -0,0 +1,118 @@ +import tarfile +import unittest + +from netjsonconfig import Wireguard + + +class TestBackend(unittest.TestCase): + """ + tests for Wireguard backend + """ + + maxDiff = None + + def test_confs(self): + c = Wireguard( + { + "wireguard": [ + { + "name": "test1", + "private_key": "QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", + "port": 40842, + }, + { + "name": "test2", + "private_key": "AFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", + "port": 40843, + }, + ] + } + ) + expected = """# wireguard config: test1 + +[Interface] +ListenPort = 40842 +PrivateKey = QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI= + +# wireguard config: test2 + +[Interface] +ListenPort = 40843 +PrivateKey = AFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI= +""" + self.assertEqual(c.render(), expected) + + def test_peers(self): + c = Wireguard( + { + "wireguard": [ + { + "name": "test1", + "private_key": "QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", + "port": 40842, + "peers": [ + { + "public_key": "jqHs76yCH0wThMSqogDshndAiXelfffUJVcFmz352HI=", + "allowed_ips": "10.0.0.3/32", + }, + { + "public_key": "94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=", + "allowed_ips": "10.0.0.4/32", + "preshared_key": "xisFXck9KfEZga4hlkproH6+86S8ki1tmLtMtqVipjg=", + "endpoint": "192.168.1.35:4908", + }, + ], + } + ] + } + ) + expected = """# wireguard config: test1 + +[Interface] +ListenPort = 40842 +PrivateKey = QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI= + +[Peer] +AllowedIPs = 10.0.0.3/32 +PublicKey = jqHs76yCH0wThMSqogDshndAiXelfffUJVcFmz352HI= + +[Peer] +AllowedIPs = 10.0.0.4/32 +Endpoint = 192.168.1.35:4908 +PreSharedKey = xisFXck9KfEZga4hlkproH6+86S8ki1tmLtMtqVipjg= +PublicKey = 94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE= +""" + self.assertEqual(c.render(), expected) + + def test_generate(self): + c = Wireguard( + { + "wireguard": [ + { + "name": "test1", + "private_key": "QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", + "port": 40842, + "peers": [ + { + "public_key": "jqHs76yCH0wThMSqogDshndAiXelfffUJVcFmz352HI=", + "allowed_ips": "10.0.0.3/32", + } + ], + } + ] + } + ) + tar = tarfile.open(fileobj=c.generate(), mode='r') + self.assertEqual(len(tar.getmembers()), 1) + # network + vpn1 = tar.getmember('test1.conf') + contents = tar.extractfile(vpn1).read().decode() + expected = """[Interface] +ListenPort = 40842 +PrivateKey = QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI= + +[Peer] +AllowedIPs = 10.0.0.3/32 +PublicKey = jqHs76yCH0wThMSqogDshndAiXelfffUJVcFmz352HI= +""" + self.assertEqual(contents, expected) From d62ee4dca488b0538e6f32caedeaa3a69f4c4003 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 3 Apr 2021 16:46:33 -0500 Subject: [PATCH 2/9] [feature] Added support for VXLAN interfaces #186 Related to #186 --- netjsonconfig/backends/base/converter.py | 4 + .../backends/openwrt/converters/interfaces.py | 15 +++ netjsonconfig/backends/openwrt/schema.py | 70 ++++++++++ netjsonconfig/backends/wireguard/schema.py | 1 + tests/openwrt/test_vxlan.py | 122 ++++++++++++++++++ tests/wireguard/test_backend.py | 6 + 6 files changed, 218 insertions(+) create mode 100644 tests/openwrt/test_vxlan.py diff --git a/netjsonconfig/backends/base/converter.py b/netjsonconfig/backends/base/converter.py index 889762134..c03c00c95 100644 --- a/netjsonconfig/backends/base/converter.py +++ b/netjsonconfig/backends/base/converter.py @@ -51,6 +51,10 @@ def type_cast(self, item, schema=None): json_type = properties[key]['type'] except KeyError: json_type = None + # if multiple types are supported, the first + # one takes precedence when parsing + if isinstance(json_type, list) and json_type: + json_type = json_type[0] if json_type == 'integer' and not isinstance(value, int): value = int(value) elif json_type == 'boolean' and not isinstance(value, bool): diff --git a/netjsonconfig/backends/openwrt/converters/interfaces.py b/netjsonconfig/backends/openwrt/converters/interfaces.py index 1629ff540..e1fc2bc26 100644 --- a/netjsonconfig/backends/openwrt/converters/interfaces.py +++ b/netjsonconfig/backends/openwrt/converters/interfaces.py @@ -4,6 +4,7 @@ from ..schema import schema from .base import OpenWrtConverter +from ..schema import schema class Interfaces(OpenWrtConverter): @@ -128,6 +129,11 @@ def __intermediate_interface(self, interface, uci_name): def _intermediate_modem_manager(self, interface): interface['proto'] = 'modemmanager' interface['pincode'] = interface.pop('pin', None) + + def _intermediate_vxlan(self, interface): + interface['proto'] = 'vxlan' + interface['peeraddr'] = interface.pop('vtep') + interface['vid'] = interface.pop('vni') return interface _address_keys = ['address', 'mask', 'family', 'gateway'] @@ -314,6 +320,15 @@ def _netjson_modem_manager(self, interface): _netjson_modemmanager = _netjson_modem_manager + _vxlan_schema = schema['definitions']['vxlan_interface']['allOf'][0] + + def _netjson_vxlan(self, interface): + interface['type'] = interface.pop('proto', None) + interface['vtep'] = interface.pop('peeraddr', None) + interface['vni'] = interface.pop('vid', None) + interface['port'] = interface['port'] + return self.type_cast(interface, schema=self._vxlan_schema) + def __netjson_address(self, address, interface): ip = ip_interface(address) family = 'ipv{0}'.format(ip.version) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 748a821b8..702aa540c 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -207,6 +207,75 @@ }, }, }, + "vxlan_interface": { + "title": "VXLAN interface", + "required": ["vtep", "port", "vni", "tunlink"], + "allOf": [ + { + "properties": { + "type": { + "type": "string", + "enum": ["vxlan"], + "default": "vxlan", + "propertyOrder": 1, + }, + "vtep": { + "type": "string", + "format": "hostname", + "title": "VTEP", + "description": "VXLAN Tunnel End Point", + "propertyOrder": 1.1, + }, + "port": { + "type": "integer", + "propertyOrder": 1.2, + "default": 4789, + "minimum": 1, + "maximum": 65535, + }, + "vni": { + "type": ["integer", "string"], + "title": "VNI", + "description": "VXLAN Network Identifier", + "propertyOrder": 1.3, + "minimum": 1, + "maximum": 16777216, + }, + "tunlink": { + "type": "string", + "title": "TUN link", + "description": "Interface to which the VXLAN tunnel will be bound", + "propertyOrder": 1.4, + }, + "rxcsum": { + "type": "boolean", + "title": "RX checksum validation", + "description": "Use checksum validation in RX (receiving) direction", + "default": True, + "format": "checkbox", + "propertyOrder": 1.5, + }, + "txcsum": { + "type": "boolean", + "title": "TX checksum validation", + "description": "Use checksum validation in TX (transmission) direction", + "default": True, + "format": "checkbox", + "propertyOrder": 1.6, + }, + "mtu": {"type": "integer", "default": 1280}, + "ttl": { + "type": "integer", + "title": "TTL", + "description": "TTL of the encapsulation packets", + "default": 64, + "propertyOrder": 3, + }, + } + }, + {"$ref": "#/definitions/interface_settings"}, + ], + }, "base_radio_settings": { "properties": { "driver": { @@ -265,6 +334,7 @@ "oneOf": [ {"$ref": "#/definitions/dialup_interface"}, {"$ref": "#/definitions/modemmanager_interface"}, + {"$ref": "#/definitions/vxlan_interface"}, ] } }, diff --git a/netjsonconfig/backends/wireguard/schema.py b/netjsonconfig/backends/wireguard/schema.py index 721d91ff7..7e25ba91f 100644 --- a/netjsonconfig/backends/wireguard/schema.py +++ b/netjsonconfig/backends/wireguard/schema.py @@ -94,4 +94,5 @@ } schema = deepcopy(base_wireguard_schema) +schema['required'] = ['wireguard'] schema['properties']['files'] = default_schema['properties']['files'] diff --git a/tests/openwrt/test_vxlan.py b/tests/openwrt/test_vxlan.py new file mode 100644 index 000000000..8a83dee88 --- /dev/null +++ b/tests/openwrt/test_vxlan.py @@ -0,0 +1,122 @@ +import unittest + +from netjsonconfig import OpenWrt +from netjsonconfig.utils import _TabsMixin + + +class TestVxlan(unittest.TestCase, _TabsMixin): + maxDiff = None + + def test_render_vxlan(self): + o = OpenWrt( + { + "interfaces": [ + { + "name": "vxlan1", + "type": "vxlan", + "vtep": "10.0.0.1", + "port": 4789, + "vni": 1, + "tunlink": "wg0", + "rxcsum": True, + "txcsum": True, + "mtu": 1280, + "ttl": 64, + } + ] + } + ) + expected = self._tabs( + """package network + +config interface 'vxlan1' + option ifname 'vxlan1' + option mtu '1280' + option peeraddr '10.0.0.1' + option port '4789' + option proto 'vxlan' + option rxcsum '1' + option ttl '64' + option tunlink 'wg0' + option txcsum '1' + option vid '1' +""" + ) + self.assertEqual(o.render(), expected) + + def test_parse_vxlan(self): + native = self._tabs( + """package network + +config interface 'vxlan1' + option ifname 'vxlan1' + option mtu '1280' + option peeraddr '10.0.0.1' + option port '4789' + option proto 'vxlan' + option rxcsum '1' + option ttl '64' + option tunlink 'wg0' + option txcsum '1' + option vid '1' +""" + ) + expected = { + "interfaces": [ + { + "name": "vxlan1", + "type": "vxlan", + "vtep": "10.0.0.1", + "port": 4789, + "vni": 1, + "tunlink": "wg0", + "rxcsum": True, + "txcsum": True, + "mtu": 1280, + "ttl": 64, + } + ] + } + o = OpenWrt(native=native) + self.assertEqual(o.config, expected) + + def test_render_vxlan_with_variables(self): + o = OpenWrt( + { + "interfaces": [ + { + "type": "vxlan", + "name": "vxlan2", + "vtep": "{{ vtep_e9081f8d67c8470d850ceb9c33bd0314 }}", + "port": 4789, + "vni": "{{ vni_e9081f8d67c8470d850ceb9c33bd0314 }}", + "tunlink": "wg0", + "rxcsum": False, + "txcsum": False, + "mtu": 1280, + "ttl": 64, + } + ] + }, + context={ + "vtep_e9081f8d67c8470d850ceb9c33bd0314": "10.0.0.2", + "vni_e9081f8d67c8470d850ceb9c33bd0314": "2", + }, + ) + expected = self._tabs( + """package network + +config interface 'vxlan2' + option ifname 'vxlan2' + option mtu '1280' + option peeraddr '10.0.0.2' + option port '4789' + option proto 'vxlan' + option rxcsum '0' + option ttl '64' + option tunlink 'wg0' + option txcsum '0' + option vid '2' +""" + ) + self.assertEqual(o.render(), expected) diff --git a/tests/wireguard/test_backend.py b/tests/wireguard/test_backend.py index d160162ef..6121c3f54 100644 --- a/tests/wireguard/test_backend.py +++ b/tests/wireguard/test_backend.py @@ -2,6 +2,7 @@ import unittest from netjsonconfig import Wireguard +from netjsonconfig.exceptions import ValidationError class TestBackend(unittest.TestCase): @@ -11,6 +12,11 @@ class TestBackend(unittest.TestCase): maxDiff = None + def test_test_schema(self): + with self.assertRaises(ValidationError) as context_manager: + Wireguard({}).validate() + self.assertIn("'wireguard' is a required property", str(context_manager.exception)) + def test_confs(self): c = Wireguard( { From 1b0f260fffd86ca18d402d190745d24dcf5da77a Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 3 Apr 2021 16:47:18 -0500 Subject: [PATCH 3/9] [chores] Moved shared VPN backend logic to BaseVpnBackend --- netjsonconfig/backends/base/backend.py | 36 +++++++++++++++++++ netjsonconfig/backends/openvpn/openvpn.py | 33 +++-------------- netjsonconfig/backends/wireguard/wireguard.py | 33 +++-------------- 3 files changed, 46 insertions(+), 56 deletions(-) diff --git a/netjsonconfig/backends/base/backend.py b/netjsonconfig/backends/base/backend.py index cade778ab..863a0985b 100644 --- a/netjsonconfig/backends/base/backend.py +++ b/netjsonconfig/backends/base/backend.py @@ -333,3 +333,39 @@ def __restore_intermediate_data(self): del self.intermediate_data self.intermediate_data = self._intermediate_copy del self._intermediate_copy + + +class BaseVpnBackend(BaseBackend): + """ + Shared logic between VPN backends + Requires setting the following attributes: + + - vpn_pattern + - config_suffix + """ + + def _generate_contents(self, tar): + """ + Adds configuration files to tarfile instance. + + :param tar: tarfile instance + :returns: None + """ + text = self.render(files=False) + # create a list with all the packages (and remove empty entries) + vpn_instances = self.vpn_pattern.split(text) + if '' in vpn_instances: + vpn_instances.remove('') + # create a file for each VPN + for vpn in vpn_instances: + lines = vpn.split('\n') + vpn_name = lines[0] + text_contents = '\n'.join(lines[2:]) + # do not end with double new line + if text_contents.endswith('\n\n'): + text_contents = text_contents[0:-1] + self._add_file( + tar=tar, + name='{0}{1}'.format(vpn_name, self.config_suffix), + contents=text_contents, + ) diff --git a/netjsonconfig/backends/openvpn/openvpn.py b/netjsonconfig/backends/openvpn/openvpn.py index 2ab53ac1f..2e809f6e8 100644 --- a/netjsonconfig/backends/openvpn/openvpn.py +++ b/netjsonconfig/backends/openvpn/openvpn.py @@ -1,12 +1,12 @@ from ...schema import X509_FILE_MODE -from ..base.backend import BaseBackend +from ..base.backend import BaseVpnBackend from . import converters from .parser import OpenVpnParser, config_suffix, vpn_pattern from .renderer import OpenVpnRenderer from .schema import schema -class OpenVpn(BaseBackend): +class OpenVpn(BaseVpnBackend): """ OpenVPN 2.x Configuration Backend """ @@ -16,32 +16,9 @@ class OpenVpn(BaseBackend): parser = OpenVpnParser renderer = OpenVpnRenderer list_identifiers = ['name'] - - def _generate_contents(self, tar): - """ - Adds configuration files to tarfile instance. - - :param tar: tarfile instance - :returns: None - """ - text = self.render(files=False) - # create a list with all the packages (and remove empty entries) - vpn_instances = vpn_pattern.split(text) - if '' in vpn_instances: - vpn_instances.remove('') - # create a file for each VPN - for vpn in vpn_instances: - lines = vpn.split('\n') - vpn_name = lines[0] - text_contents = '\n'.join(lines[2:]) - # do not end with double new line - if text_contents.endswith('\n\n'): - text_contents = text_contents[0:-1] - self._add_file( - tar=tar, - name='{0}{1}'.format(vpn_name, config_suffix), - contents=text_contents, - ) + # BaseVpnBackend attributes + vpn_pattern = vpn_pattern + config_suffix = config_suffix @classmethod def auto_client( diff --git a/netjsonconfig/backends/wireguard/wireguard.py b/netjsonconfig/backends/wireguard/wireguard.py index 17b0b7d79..c082d2bc9 100644 --- a/netjsonconfig/backends/wireguard/wireguard.py +++ b/netjsonconfig/backends/wireguard/wireguard.py @@ -1,37 +1,14 @@ -from ..base.backend import BaseBackend +from ..base.backend import BaseVpnBackend from . import converters from .parser import config_suffix, vpn_pattern from .renderer import WireguardRenderer from .schema import schema -class Wireguard(BaseBackend): +class Wireguard(BaseVpnBackend): schema = schema converters = [converters.Wireguard] renderer = WireguardRenderer - - def _generate_contents(self, tar): - """ - Adds configuration files to tarfile instance. - - :param tar: tarfile instance - :returns: None - """ - text = self.render(files=False) - # create a list with all the packages (and remove empty entries) - vpn_instances = vpn_pattern.split(text) - if '' in vpn_instances: - vpn_instances.remove('') - # create a file for each VPN - for vpn in vpn_instances: - lines = vpn.split('\n') - vpn_name = lines[0] - text_contents = '\n'.join(lines[2:]) - # do not end with double new line - if text_contents.endswith('\n\n'): - text_contents = text_contents[0:-1] - self._add_file( - tar=tar, - name='{0}{1}'.format(vpn_name, config_suffix), - contents=text_contents, - ) + # BaseVpnBackend attributes + vpn_pattern = vpn_pattern + config_suffix = config_suffix From d00dc9563432284f2960f64a554b2f943d9b980b Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 3 Apr 2021 16:47:36 -0500 Subject: [PATCH 4/9] [feature] Added wireguard interfaces and peers to OpenWRT --- .../backends/openwrt/converters/__init__.py | 2 + .../backends/openwrt/converters/base.py | 2 +- .../backends/openwrt/converters/interfaces.py | 47 +++- .../openwrt/converters/wireguard_peers.py | 34 +++ netjsonconfig/backends/openwrt/openwrt.py | 1 + netjsonconfig/backends/openwrt/schema.py | 216 ++++++++++++++- .../backends/wireguard/converters.py | 5 +- netjsonconfig/backends/wireguard/schema.py | 28 +- netjsonconfig/schema.py | 34 ++- tests/openwrt/test_wireguard.py | 256 ++++++++++++++++++ tests/wireguard/test_backend.py | 7 +- 11 files changed, 591 insertions(+), 41 deletions(-) create mode 100644 netjsonconfig/backends/openwrt/converters/wireguard_peers.py create mode 100644 tests/openwrt/test_wireguard.py diff --git a/netjsonconfig/backends/openwrt/converters/__init__.py b/netjsonconfig/backends/openwrt/converters/__init__.py index 2eb379a80..5993c2b83 100644 --- a/netjsonconfig/backends/openwrt/converters/__init__.py +++ b/netjsonconfig/backends/openwrt/converters/__init__.py @@ -8,6 +8,7 @@ from .routes import Routes from .rules import Rules from .switch import Switch +from .wireguard_peers import WireguardPeers from .wireless import Wireless __all__ = [ @@ -21,5 +22,6 @@ 'Routes', 'Rules', 'Switch', + 'WireguardPeers', 'Wireless', ] diff --git a/netjsonconfig/backends/openwrt/converters/base.py b/netjsonconfig/backends/openwrt/converters/base.py index b155c9f2d..30ffc8980 100644 --- a/netjsonconfig/backends/openwrt/converters/base.py +++ b/netjsonconfig/backends/openwrt/converters/base.py @@ -6,7 +6,7 @@ class OpenWrtConverter(BaseConverter): def should_skip_block(self, block): _type = block.get('.type') - return not block or _type not in self._uci_types + return not block or (self._uci_types and _type not in self._uci_types) def _get_uci_name(self, name): return name.replace('.', '_').replace('-', '_') diff --git a/netjsonconfig/backends/openwrt/converters/interfaces.py b/netjsonconfig/backends/openwrt/converters/interfaces.py index e1fc2bc26..173a9a986 100644 --- a/netjsonconfig/backends/openwrt/converters/interfaces.py +++ b/netjsonconfig/backends/openwrt/converters/interfaces.py @@ -4,7 +4,6 @@ from ..schema import schema from .base import OpenWrtConverter -from ..schema import schema class Interfaces(OpenWrtConverter): @@ -46,8 +45,9 @@ def __intermediate_addresses(self, interface): converts NetJSON address to UCI intermediate data structure """ - if interface.get('proto') == 'wireguard': - return self.__intermediate_wg_addresses(interface) + # wireguard interfaces need a different format + if interface.get('type') == 'wireguard': + return self.__intermediate_wireguard_addresses(interface) address_list = self.get_copy(interface, 'addresses') # do not ignore interfaces if they do not contain any address if not address_list: @@ -86,9 +86,15 @@ def __intermediate_addresses(self, interface): result += dhcp return result - def __intermediate_wg_addresses(self, interface): - address_list = interface.pop('wg_addresses') - static = {'addresses': address_list, 'proto': interface['proto']} + def __intermediate_wireguard_addresses(self, interface): + addresses = interface.pop('addresses') + address_list = [] + for address_dict in addresses: + address = address_dict['address'] + if 'mask' in address_dict: + address = f'{address}/{address_dict["mask"]}' + address_list.append(address) + static = {'addresses': address_list, 'proto': 'wireguard'} return [static] def __intermediate_interface(self, interface, uci_name): @@ -97,9 +103,7 @@ def __intermediate_interface(self, interface, uci_name): UCI intermediate data structure """ interface.update({'.type': 'interface', '.name': uci_name}) - name = interface.pop('name') - if interface.get('proto') != 'wireguard': - interface['ifname'] = name + interface['ifname'] = interface.pop('name') if 'network' in interface: del interface['network'] if 'mac' in interface: @@ -130,6 +134,12 @@ def _intermediate_modem_manager(self, interface): interface['proto'] = 'modemmanager' interface['pincode'] = interface.pop('pin', None) + def _intermediate_wireguard(self, interface): + interface['proto'] = 'wireguard' + interface['listen_port'] = interface.pop('port', None) + del interface['ifname'] + return interface + def _intermediate_vxlan(self, interface): interface['proto'] = 'vxlan' interface['peeraddr'] = interface.pop('vtep') @@ -320,6 +330,25 @@ def _netjson_modem_manager(self, interface): _netjson_modemmanager = _netjson_modem_manager + _wireguard_schema = schema['definitions']['wireguard_interface']['allOf'][0] + + def _netjson_wireguard(self, interface): + interface['type'] = interface.pop('proto', None) + interface['port'] = interface.pop('listen_port', None) + addresses = [] + for address in interface['addresses']: + cidr = ip_interface(address) + addresses.append( + { + 'address': str(cidr.ip), + 'mask': cidr.network.prefixlen, + 'proto': 'static', + 'family': f'ipv{cidr.ip.version}', + } + ) + interface['addresses'] = addresses + return self.type_cast(interface, schema=self._wireguard_schema) + _vxlan_schema = schema['definitions']['vxlan_interface']['allOf'][0] def _netjson_vxlan(self, interface): diff --git a/netjsonconfig/backends/openwrt/converters/wireguard_peers.py b/netjsonconfig/backends/openwrt/converters/wireguard_peers.py new file mode 100644 index 000000000..99b7e201e --- /dev/null +++ b/netjsonconfig/backends/openwrt/converters/wireguard_peers.py @@ -0,0 +1,34 @@ +from ..schema import schema +from .base import OpenWrtConverter + + +class WireguardPeers(OpenWrtConverter): + netjson_key = 'wireguard_peers' + intermediate_key = 'network' + _schema = schema['properties']['wireguard_peers']['items'] + # unfortunately due to the design of the + # wireguard OpenWRT package, this is unpredictable + _uci_types = None + + def to_intermediate_loop(self, block, result, index=None): + result.setdefault('network', []) + result['network'].append(self.__intermediate_peer(block)) + return result + + def __intermediate_peer(self, peer): + interface = peer.pop("interface") + peer.update({'.type': f'wireguard_{interface}', '.name': f'wgpeer_{interface}'}) + if not peer.get('endpoint_host') and 'endpoint_port' in peer: + del peer['endpoint_port'] + return self.sorted_dict(peer) + + def to_netjson_loop(self, block, result, index): + result.setdefault('wireguard_peers', []) + result['wireguard_peers'].append(self.__netjson_peer(block)) + return result + + def __netjson_peer(self, peer): + del peer['.name'] + interface = peer.pop('.type').replace('wireguard_', '') + peer['interface'] = interface + return self.type_cast(peer) diff --git a/netjsonconfig/backends/openwrt/openwrt.py b/netjsonconfig/backends/openwrt/openwrt.py index b6dc64934..c73b54713 100644 --- a/netjsonconfig/backends/openwrt/openwrt.py +++ b/netjsonconfig/backends/openwrt/openwrt.py @@ -22,6 +22,7 @@ class OpenWrt(BaseBackend): converters.Radios, converters.Wireless, converters.OpenVpn, + converters.WireguardPeers, converters.Default, ] parser = OpenWrtParser diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 702aa540c..dd952483f 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -4,19 +4,21 @@ from ...schema import schema as default_schema from ...utils import merge_config from ..openvpn.schema import base_openvpn_schema +from ..wireguard.schema import base_wireguard_schema from .timezones import timezones default_radio_driver = "mac80211" -_interface_properties = default_schema["definitions"]["interface_settings"][ - "properties" -] + +wireguard = base_wireguard_schema["properties"]["wireguard"]["items"]["properties"] +wireguard_peers = wireguard["peers"]["items"]["properties"] +interface_settings = default_schema["definitions"]["interface_settings"]["properties"] schema = merge_config( default_schema, { "definitions": { - "interface_settings": { + "base_interface_settings": { "properties": { "network": { "type": "string", @@ -42,7 +44,7 @@ "items": { "title": "network", "type": "string", - "$ref": "#/definitions/interface_settings/properties/network", + "$ref": "#/definitions/base_interface_settings/properties/network", }, "propertyOrder": 19, } @@ -158,6 +160,7 @@ }, } }, + {"$ref": "#/definitions/base_interface_settings"}, {"$ref": "#/definitions/interface_settings"}, ], }, @@ -166,10 +169,10 @@ "title": "Modem manager interface", "required": ["name", "device"], "properties": { - "name": _interface_properties["name"], - "mtu": _interface_properties["mtu"], - "autostart": _interface_properties["autostart"], - "disabled": _interface_properties["disabled"], + "name": interface_settings["name"], + "mtu": interface_settings["mtu"], + "autostart": interface_settings["autostart"], + "disabled": interface_settings["disabled"], "type": { "type": "string", "enum": ["modem-manager"], @@ -207,6 +210,142 @@ }, }, }, + "wireguard_interface": { + "type": "object", + "title": "Wireguard interface", + "required": ["private_key"], + "additionalProperties": True, + "allOf": [ + { + "properties": { + "type": { + "type": "string", + "enum": ["wireguard"], + "default": "wireguard", + "propertyOrder": 1, + }, + "private_key": wireguard["private_key"], + "port": wireguard["port"], + "mtu": { + "type": "integer", + "default": 1420, + "propertyOrder": 1.1, + }, + "nohostroute": { + "type": "boolean", + "format": "checkbox", + "default": False, + "title": "no host route", + "description": ( + "Do not add routes to ensure the tunnel " + "endpoints are routed via non-tunnel device" + ), + "propertyOrder": 3, + }, + "fwmark": { + "type": "string", + "title": "firewall mark", + "description": ( + "Firewall mark to apply to tunnel endpoint packets, " + "will be automatically determined if left blank" + ), + "propertyOrder": 3.1, + }, + "ip6prefix": { + "title": "IPv6 prefixes", + "description": "IPv6 prefixes to delegate to other interfaces", + "type": "array", + "items": { + "type": "string", + "title": "IPv6 prefix", + "uniqueItems": True, + }, + "propertyOrder": 9, + }, + # unfortunately some duplication with the base IP address + # definition is needed to achieve functional usability and + # consistency with the rest of the schema because the + # wireguard OpenWRT package uses a different configuration + # format for addresses + "addresses": { + "type": "array", + "title": "addresses", + "uniqueItems": True, + "propertyOrder": 20, + "items": { + "required": ["proto", "family", "address", "mask"], + "title": "address", + "oneOf": [ + { + "type": "object", + "title": "ipv4", + "properties": { + "proto": { + "title": "protocol", + "type": "string", + "propertyOrder": 1, + "enum": ["static"], + }, + "family": { + "title": "family", + "type": "string", + "propertyOrder": 2, + "enum": ["ipv4"], + }, + "address": { + "type": "string", + "title": "ipv4 address", + "minLength": 7, + "format": "ipv4", + "propertyOrder": 3, + }, + "mask": { + "type": "number", + "minimum": 8, + "maxmium": 32, + "default": 32, + }, + }, + }, + { + "type": "object", + "title": "ipv6", + "properties": { + "proto": { + "title": "protocol", + "type": "string", + "propertyOrder": 1, + "enum": ["static"], + }, + "family": { + "title": "family", + "type": "string", + "propertyOrder": 2, + "enum": ["ipv6"], + }, + "address": { + "type": "string", + "title": "ipv6 address", + "minLength": 3, + "format": "ipv6", + "propertyOrder": 3, + }, + "mask": { + "type": "number", + "minimum": 4, + "maxmium": 128, + "default": 128, + }, + }, + }, + ], + }, + }, + } + }, + {"$ref": "#/definitions/base_interface_settings"}, + ], + }, "vxlan_interface": { "title": "VXLAN interface", "required": ["vtep", "port", "vni", "tunlink"], @@ -271,9 +410,10 @@ "default": 64, "propertyOrder": 3, }, + "mac": interface_settings["mac"], } }, - {"$ref": "#/definitions/interface_settings"}, + {"$ref": "#/definitions/base_interface_settings"}, ], }, "base_radio_settings": { @@ -335,6 +475,7 @@ {"$ref": "#/definitions/dialup_interface"}, {"$ref": "#/definitions/modemmanager_interface"}, {"$ref": "#/definitions/vxlan_interface"}, + {"$ref": "#/definitions/wireguard_interface"}, ] } }, @@ -559,6 +700,61 @@ }, }, }, + "wireguard_peers": { + "type": "array", + "title": "Wireguard Peers", + "uniqueItems": True, + "propertyOrder": 13, + "items": { + "type": "object", + "title": "Wireguard peer", + "additionalProperties": True, + "required": ["interface", "public_key", "allowed_ips"], + "properties": { + "interface": { + "type": "string", + "title": "interface", + "description": "name of the wireguard interface", + "minLength": 2, + "maxLength": 15, + "pattern": "^[^\\s]*$", + "propertyOrder": 0, + }, + "public_key": wireguard_peers["public_key"], + "allowed_ips": { + "type": "array", + "title": "allowed IPs", + "propertyOrder": 2, + "uniqueItems": True, + "items": {"type": "string", "title": "IP/prefix"}, + }, + "endpoint_host": wireguard_peers["endpoint_host"], + "endpoint_port": wireguard_peers["endpoint_port"], + "preshared_key": wireguard_peers["preshared_key"], + "persistent_keepalive": { + "type": "integer", + "title": "keep alive", + "description": ( + "Number of second between keepalive " + "messages, 0 means disabled" + ), + "default": 0, + "propertyOrder": 6, + }, + "route_allowed_ips": { + "type": "boolean", + "format": "checkbox", + "title": "route allowed IPs", + "description": ( + "Automatically create a route for " + "each Allowed IPs for this peer" + ), + "default": False, + "propertyOrder": 7, + }, + }, + }, + }, }, }, ) diff --git a/netjsonconfig/backends/wireguard/converters.py b/netjsonconfig/backends/wireguard/converters.py index de779949a..b7ebbbe8e 100644 --- a/netjsonconfig/backends/wireguard/converters.py +++ b/netjsonconfig/backends/wireguard/converters.py @@ -24,7 +24,10 @@ def __intermediate_peers(self, peers): for peer in peers: peer['AllowedIPs'] = peer.pop('allowed_ips') peer['PublicKey'] = peer.pop('public_key') - peer['Endpoint'] = peer.pop('endpoint', None) peer['PreSharedKey'] = peer.pop('preshared_key', None) + host = peer.pop('endpoint_host', None) + port = peer.pop('endpoint_port', None) + if host and port: + peer['Endpoint'] = f'{host}:{port}' peer_list.append(self.sorted_dict(peer)) return peer_list diff --git a/netjsonconfig/backends/wireguard/schema.py b/netjsonconfig/backends/wireguard/schema.py index 7e25ba91f..546b8ffce 100644 --- a/netjsonconfig/backends/wireguard/schema.py +++ b/netjsonconfig/backends/wireguard/schema.py @@ -42,7 +42,6 @@ "private_key": { "title": "private key", "type": "string", - "minLength": 44, "maxLength": 44, "pattern": "^[^\\s]*$", "propertyOrder": 3, @@ -61,28 +60,43 @@ "public_key": { "title": "public key", "type": "string", - "minLength": 44, "maxLength": 44, "pattern": "^[^\\s]*$", "propertyOrder": 1, }, "allowed_ips": { - "title": "public key", + "title": "allowed IP addresses", "type": "string", "propertyOrder": 2, }, - "endpoint": { - "title": "public key", + "endpoint_host": { + "title": "endpoint host", "type": "string", "propertyOrder": 3, }, + "endpoint_port": { + "title": "endpoint port", + "type": "integer", + "description": ( + "Wireguard port. Will be ignored if " + "\"endpoint host\" is left empty." + ), + "default": 51820, + "maximum": 65535, + "minimum": 1, + "propertyOrder": 4, + }, "preshared_key": { "title": "pre-shared key", + "description": ( + "Optional shared secret, to provide an " + "additional layer of symmetric-key cryptography " + "for post-quantum resistance" + ), "type": "string", - "minLength": 44, "maxLength": 44, "pattern": "^[^\\s]*$", - "propertyOrder": 4, + "propertyOrder": 5, }, }, }, diff --git a/netjsonconfig/schema.py b/netjsonconfig/schema.py index cbddf8968..a387c81b6 100644 --- a/netjsonconfig/schema.py +++ b/netjsonconfig/schema.py @@ -30,7 +30,6 @@ "properties": { "address": {"type": "string", "propertyOrder": 3}, "mask": {"type": "integer", "propertyOrder": 4}, - "gateway": {"type": "string", "propertyOrder": 5}, }, }, "ipv4_address": { @@ -51,9 +50,11 @@ }, "mask": {"minimum": 8, "maxmium": 32, "default": 24}, "gateway": { + "type": "string", "title": "ipv4 gateway", "description": "optional ipv4 gateway", "maxLength": 16, + "propertyOrder": 5, }, }, }, @@ -79,9 +80,11 @@ }, "mask": {"minimum": 4, "maxmium": 128, "default": 64}, "gateway": { + "type": "string", "title": "ipv6 gateway", "description": "optional ipv6 gateway", "maxLength": 45, + "propertyOrder": 5, }, }, }, @@ -100,9 +103,9 @@ }, ], }, - "interface_settings": { + "base_interface_settings": { "type": "object", - "title": "Interface settings", + "title": "Base Interface settings", "additionalProperties": True, "required": ["name", "type"], "properties": { @@ -120,6 +123,19 @@ "minimum": 68, "propertyOrder": 2, }, + "disabled": { + "type": "boolean", + "description": "disable this interface without deleting its configuration", + "default": False, + "format": "checkbox", + "propertyOrder": 6, + }, + }, + }, + "interface_settings": { + "type": "object", + "title": "Interface settings", + "properties": { "mac": { "type": "string", "title": "MAC address", @@ -136,13 +152,6 @@ "format": "checkbox", "propertyOrder": 5, }, - "disabled": { - "type": "boolean", - "description": "disable this interface without deleting its configuration", - "default": False, - "format": "checkbox", - "propertyOrder": 6, - }, "addresses": { "type": "array", "title": "Addresses", @@ -172,6 +181,7 @@ } } }, + {"$ref": "#/definitions/base_interface_settings"}, {"$ref": "#/definitions/interface_settings"}, ], }, @@ -199,6 +209,7 @@ }, } }, + {"$ref": "#/definitions/base_interface_settings"}, {"$ref": "#/definitions/interface_settings"}, ], }, @@ -229,11 +240,12 @@ "items": { "title": "bridged interface", "type": "string", - "$ref": "#/definitions/interface_settings/properties/name", + "$ref": "#/definitions/base_interface_settings/properties/name", }, }, } }, + {"$ref": "#/definitions/base_interface_settings"}, {"$ref": "#/definitions/interface_settings"}, ], }, diff --git a/tests/openwrt/test_wireguard.py b/tests/openwrt/test_wireguard.py new file mode 100644 index 000000000..5ff681e4d --- /dev/null +++ b/tests/openwrt/test_wireguard.py @@ -0,0 +1,256 @@ +import unittest + +from netjsonconfig import OpenWrt +from netjsonconfig.utils import _TabsMixin + + +class TestWireguard(unittest.TestCase, _TabsMixin): + maxDiff = None + + def test_render_wireguard_interface(self): + o = OpenWrt( + { + "interfaces": [ + { + "name": "wg0", + "type": "wireguard", + "private_key": "sGQitlaWF8LJjmNJOPoQkm9BVAtMtdfwpFT6zLSixlQ=", + "port": 51820, + "mtu": 1420, + "nohostroute": False, + "fwmark": "", + "addresses": [ + { + "proto": "static", + "family": "ipv4", + "address": "10.0.0.3", + "mask": 24, + } + ], + } + ] + } + ) + expected = self._tabs( + """package network + +config interface 'wg0' + list addresses '10.0.0.3/24' + option listen_port '51820' + option mtu '1420' + option nohostroute '0' + option private_key 'sGQitlaWF8LJjmNJOPoQkm9BVAtMtdfwpFT6zLSixlQ=' + option proto 'wireguard' +""" + ) + self.assertEqual(o.render(), expected) + + def test_parse_wireguard_interface(self): + native = self._tabs( + """package network + +config interface 'wg0' + list addresses '10.0.0.3/24' + option listen_port '51820' + option mtu '1420' + option nohostroute '0' + option private_key 'sGQitlaWF8LJjmNJOPoQkm9BVAtMtdfwpFT6zLSixlQ=' + option proto 'wireguard' +""" + ) + expected = { + "interfaces": [ + { + "name": "wg0", + "type": "wireguard", + "private_key": "sGQitlaWF8LJjmNJOPoQkm9BVAtMtdfwpFT6zLSixlQ=", + "port": 51820, + "mtu": 1420, + "nohostroute": False, + "addresses": [ + { + "proto": "static", + "family": "ipv4", + "address": "10.0.0.3", + "mask": 24, + } + ], + } + ] + } + o = OpenWrt(native=native) + self.assertEqual(o.config, expected) + + def test_render_wireguard_interface_with_variables(self): + o = OpenWrt( + { + "interfaces": [ + { + "name": "wg0", + "type": "wireguard", + "private_key": "{{private_key}}", + "port": 51820, + "mtu": 1420, + "nohostroute": False, + "fwmark": "", + "addresses": [ + { + "proto": "static", + "family": "ipv4", + "address": "{{ip_address_8097b09be57a4b278e2ef2ea9ea809f3}}", + "mask": 32, + } + ], + } + ] + }, + context={ + "private_key": "sGQitlaWF8LJjmNJOPoQkm9BVAtMtdfwpFT6zLSixlQ=", + "ip_address_8097b09be57a4b278e2ef2ea9ea809f3": "10.0.0.3", + }, + ) + expected = self._tabs( + """package network + +config interface 'wg0' + list addresses '10.0.0.3/32' + option listen_port '51820' + option mtu '1420' + option nohostroute '0' + option private_key 'sGQitlaWF8LJjmNJOPoQkm9BVAtMtdfwpFT6zLSixlQ=' + option proto 'wireguard' +""" + ) + self.assertEqual(o.render(), expected) + + def test_render_wireguard_peer(self): + o = OpenWrt( + { + "wireguard_peers": [ + { + "interface": "wg0", + "public_key": "rn+isMBpyQ4HX6ZzE709bKnZw5IaLZoIS3hIjmfKCkk=", + "allowed_ips": ["10.0.0.1/32"], + "endpoint_host": "192.168.1.42", + "endpoint_port": 40840, + "preshared_key": "oPZmGdHBseaV1TF0julyElNuJyeKs2Eo+o62R/09IB4=", + "persistent_keepalive": 30, + "route_allowed_ips": True, + } + ] + } + ) + expected = self._tabs( + """package network + +config wireguard_wg0 'wgpeer_wg0' + list allowed_ips '10.0.0.1/32' + option endpoint_host '192.168.1.42' + option endpoint_port '40840' + option persistent_keepalive '30' + option preshared_key 'oPZmGdHBseaV1TF0julyElNuJyeKs2Eo+o62R/09IB4=' + option public_key 'rn+isMBpyQ4HX6ZzE709bKnZw5IaLZoIS3hIjmfKCkk=' + option route_allowed_ips '1' +""" + ) + self.assertEqual(o.render(), expected) + + def test_render_wireguard_peer_with_variables(self): + o = OpenWrt( + { + "wireguard_peers": [ + { + "interface": "wg0", + "public_key": "{{pub_key_8097b09be57a4b278e2ef2ea9ea809f3}}", + "allowed_ips": [ + "{{server_ip_max_prefix_8097b09be57a4b278e2ef2ea9ea809f3}}" + ], + "endpoint_host": "{{vpn_host_8097b09be57a4b278e2ef2ea9ea809f3}}", + "endpoint_port": 40840, + "preshared_key": "{{pre_key_8097b09be57a4b278e2ef2ea9ea809f3}}", + "persistent_keepalive": 30, + "route_allowed_ips": True, + } + ] + }, + context={ + "server_ip_max_prefix_8097b09be57a4b278e2ef2ea9ea809f3": "10.0.0.1/32", + "vpn_host_8097b09be57a4b278e2ef2ea9ea809f3": "192.168.1.42", + "pub_key_8097b09be57a4b278e2ef2ea9ea809f3": "rn+isMBpyQ4HX6ZzE709bKnZw5IaLZoIS3hIjmfKCkk=", + "pre_key_8097b09be57a4b278e2ef2ea9ea809f3": "oPZmGdHBseaV1TF0julyElNuJyeKs2Eo+o62R/09IB4=", + }, + ) + expected = self._tabs( + """package network + +config wireguard_wg0 'wgpeer_wg0' + list allowed_ips '10.0.0.1/32' + option endpoint_host '192.168.1.42' + option endpoint_port '40840' + option persistent_keepalive '30' + option preshared_key 'oPZmGdHBseaV1TF0julyElNuJyeKs2Eo+o62R/09IB4=' + option public_key 'rn+isMBpyQ4HX6ZzE709bKnZw5IaLZoIS3hIjmfKCkk=' + option route_allowed_ips '1' +""" + ) + self.assertEqual(o.render(), expected) + + def test_render_wireguard_peer_no_endpoint_host(self): + o = OpenWrt( + { + "wireguard_peers": [ + { + "interface": "wg0", + "public_key": "rn+isMBpyQ4HX6ZzE709bKnZw5IaLZoIS3hIjmfKCkk=", + "allowed_ips": ["10.0.0.1/32"], + "endpoint_port": 40840, + "preshared_key": "oPZmGdHBseaV1TF0julyElNuJyeKs2Eo+o62R/09IB4=", + "persistent_keepalive": 30, + "route_allowed_ips": True, + } + ] + } + ) + expected = self._tabs( + """package network + +config wireguard_wg0 'wgpeer_wg0' + list allowed_ips '10.0.0.1/32' + option persistent_keepalive '30' + option preshared_key 'oPZmGdHBseaV1TF0julyElNuJyeKs2Eo+o62R/09IB4=' + option public_key 'rn+isMBpyQ4HX6ZzE709bKnZw5IaLZoIS3hIjmfKCkk=' + option route_allowed_ips '1' +""" + ) + self.assertEqual(o.render(), expected) + + def test_parse_wireguard_peer(self): + native = self._tabs( + """package network + +config wireguard_wg0 'wgpeer_wg0' + list allowed_ips '10.0.0.1/32' + option endpoint_host '192.168.1.42' + option endpoint_port '40840' + option persistent_keepalive '30' + option preshared_key 'oPZmGdHBseaV1TF0julyElNuJyeKs2Eo+o62R/09IB4=' + option public_key 'rn+isMBpyQ4HX6ZzE709bKnZw5IaLZoIS3hIjmfKCkk=' + option route_allowed_ips '1' +""" + ) + expected = { + "wireguard_peers": [ + { + "allowed_ips": ["10.0.0.1/32"], + "endpoint_host": "192.168.1.42", + "endpoint_port": 40840, + "interface": "wg0", + "persistent_keepalive": 30, + "preshared_key": "oPZmGdHBseaV1TF0julyElNuJyeKs2Eo+o62R/09IB4=", + "public_key": "rn+isMBpyQ4HX6ZzE709bKnZw5IaLZoIS3hIjmfKCkk=", + "route_allowed_ips": True, + } + ] + } + o = OpenWrt(native=native) + self.assertEqual(o.config, expected) diff --git a/tests/wireguard/test_backend.py b/tests/wireguard/test_backend.py index 6121c3f54..0dcb2c07e 100644 --- a/tests/wireguard/test_backend.py +++ b/tests/wireguard/test_backend.py @@ -15,7 +15,9 @@ class TestBackend(unittest.TestCase): def test_test_schema(self): with self.assertRaises(ValidationError) as context_manager: Wireguard({}).validate() - self.assertIn("'wireguard' is a required property", str(context_manager.exception)) + self.assertIn( + "'wireguard' is a required property", str(context_manager.exception) + ) def test_confs(self): c = Wireguard( @@ -65,7 +67,8 @@ def test_peers(self): "public_key": "94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=", "allowed_ips": "10.0.0.4/32", "preshared_key": "xisFXck9KfEZga4hlkproH6+86S8ki1tmLtMtqVipjg=", - "endpoint": "192.168.1.35:4908", + "endpoint_host": "192.168.1.35", + "endpoint_port": 4908, }, ], } From 8c40e97ff3262cf084a0b464eb29558b8c9eff3f Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 3 Apr 2021 16:47:39 -0500 Subject: [PATCH 5/9] [chores] Wireguard: public_key and allowed_ips shall not be empty --- netjsonconfig/backends/openwrt/schema.py | 6 +++++- netjsonconfig/backends/wireguard/schema.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index dd952483f..2824fea8d 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -726,7 +726,11 @@ "title": "allowed IPs", "propertyOrder": 2, "uniqueItems": True, - "items": {"type": "string", "title": "IP/prefix"}, + "items": { + "type": "string", + "title": "IP/prefix", + "minLength": 1, + }, }, "endpoint_host": wireguard_peers["endpoint_host"], "endpoint_port": wireguard_peers["endpoint_port"], diff --git a/netjsonconfig/backends/wireguard/schema.py b/netjsonconfig/backends/wireguard/schema.py index 546b8ffce..65f15d7dd 100644 --- a/netjsonconfig/backends/wireguard/schema.py +++ b/netjsonconfig/backends/wireguard/schema.py @@ -61,12 +61,14 @@ "title": "public key", "type": "string", "maxLength": 44, + "minLength": 1, "pattern": "^[^\\s]*$", "propertyOrder": 1, }, "allowed_ips": { "title": "allowed IP addresses", "type": "string", + "minLength": 1, "propertyOrder": 2, }, "endpoint_host": { From be1f097a5bc213b18481149b95865ce05b29b69f Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 3 Apr 2021 16:47:44 -0500 Subject: [PATCH 6/9] [fix] Updated modem-manager interface code --- .../backends/openwrt/converters/interfaces.py | 3 +- netjsonconfig/backends/openwrt/schema.py | 91 +++++++++++-------- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/interfaces.py b/netjsonconfig/backends/openwrt/converters/interfaces.py index 173a9a986..9d5f0928d 100644 --- a/netjsonconfig/backends/openwrt/converters/interfaces.py +++ b/netjsonconfig/backends/openwrt/converters/interfaces.py @@ -133,6 +133,7 @@ def __intermediate_interface(self, interface, uci_name): def _intermediate_modem_manager(self, interface): interface['proto'] = 'modemmanager' interface['pincode'] = interface.pop('pin', None) + return interface def _intermediate_wireguard(self, interface): interface['proto'] = 'wireguard' @@ -320,7 +321,7 @@ def _netjson_dialup(self, interface): interface['type'] = 'dialup' return interface - _modem_manager_schema = schema['definitions']['modemmanager_interface'] + _modem_manager_schema = schema['definitions']['modemmanager_interface']['allOf'][0] def _netjson_modem_manager(self, interface): del interface['proto'] diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 2824fea8d..3bd088c70 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -168,47 +168,58 @@ "type": "object", "title": "Modem manager interface", "required": ["name", "device"], - "properties": { - "name": interface_settings["name"], - "mtu": interface_settings["mtu"], - "autostart": interface_settings["autostart"], - "disabled": interface_settings["disabled"], - "type": { - "type": "string", - "enum": ["modem-manager"], - "default": "dialup", - "propertyOrder": 1, - }, - "apn": {"type": "string", "title": "APN", "propertyOrder": 1.1}, - "pin": { - "type": "string", - "title": "PIN code", - "propertyOrder": 1.2, - }, - "device": { - "type": "string", - "description": "Leave blank to use the hardware default", - "propertyOrder": 1.3, - }, - "username": {"type": "string", "propertyOrder": 1.4}, - "password": {"type": "string", "propertyOrder": 1.5}, - "metric": {"type": "integer", "default": 50, "propertyOrder": 1.6}, - "iptype": { - "type": "string", - "title": "IP type", - "default": "ipv4", - "enum": ["ipv4", "ipv6", "ipv4v6"], - "options": {"enum_titles": ["IPv4", "IPv6", "IPv4 and IPv6"]}, - "propertyOrder": 1.7, - }, - "lowpower": { - "type": "boolean", - "title": "Low power mode", - "format": "checkbox", - "default": False, - "propertyOrder": 1.8, + "allOf": [ + { + "properties": { + "type": { + "type": "string", + "enum": ["modem-manager"], + "default": "dialup", + "propertyOrder": 1, + }, + "apn": { + "type": "string", + "title": "APN", + "propertyOrder": 1.1, + }, + "pin": { + "type": "string", + "title": "PIN code", + "propertyOrder": 1.2, + }, + "device": { + "type": "string", + "description": "Leave blank to use the hardware default", + "propertyOrder": 1.3, + }, + "username": {"type": "string", "propertyOrder": 1.4}, + "password": {"type": "string", "propertyOrder": 1.5}, + "metric": { + "type": "integer", + "default": 50, + "propertyOrder": 1.6, + }, + "iptype": { + "type": "string", + "title": "IP type", + "default": "ipv4", + "enum": ["ipv4", "ipv6", "ipv4v6"], + "options": { + "enum_titles": ["IPv4", "IPv6", "IPv4 and IPv6"] + }, + "propertyOrder": 1.7, + }, + "lowpower": { + "type": "boolean", + "title": "Low power mode", + "format": "checkbox", + "default": False, + "propertyOrder": 1.8, + }, + } }, - }, + {"$ref": "#/definitions/base_interface_settings"}, + ], }, "wireguard_interface": { "type": "object", From e36d3fc16f7721ccb04d02ae909e96000e506d94 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 14 Apr 2021 20:10:39 +0530 Subject: [PATCH 7/9] [feature] Added autoclient classmethods to Wireguard, VXLAN and OpenWRT backend --- netjsonconfig/__init__.py | 1 + netjsonconfig/backends/openwrt/openwrt.py | 69 ++++++++++ netjsonconfig/backends/openwrt/schema.py | 2 - netjsonconfig/backends/vxlan/__init__.py | 0 .../backends/vxlan/vxlan_wireguard.py | 19 +++ .../backends/wireguard/converters.py | 1 + netjsonconfig/backends/wireguard/wireguard.py | 27 ++++ tests/openwrt/test_backend.py | 121 ++++++++++++++++++ tests/openwrt/test_wireguard.py | 8 +- tests/vxlan/__init__.py | 0 tests/vxlan/test_vxlan_wireguard.py | 25 ++++ tests/wireguard/test_backend.py | 51 ++++++++ tests/wireguard/test_parser.py | 34 +++++ 13 files changed, 352 insertions(+), 6 deletions(-) create mode 100644 netjsonconfig/backends/vxlan/__init__.py create mode 100644 netjsonconfig/backends/vxlan/vxlan_wireguard.py create mode 100644 tests/vxlan/__init__.py create mode 100644 tests/vxlan/test_vxlan_wireguard.py create mode 100644 tests/wireguard/test_parser.py diff --git a/netjsonconfig/__init__.py b/netjsonconfig/__init__.py index 41027f18b..1ba605d3a 100644 --- a/netjsonconfig/__init__.py +++ b/netjsonconfig/__init__.py @@ -5,6 +5,7 @@ from .backends.openvpn.openvpn import OpenVpn # noqa from .backends.openwisp.openwisp import OpenWisp # noqa from .backends.openwrt.openwrt import OpenWrt # noqa +from .backends.vxlan.vxlan_wireguard import VxlanWireguard # noqa from .backends.wireguard.wireguard import Wireguard # noqa from .version import VERSION, __version__, get_version # noqa diff --git a/netjsonconfig/backends/openwrt/openwrt.py b/netjsonconfig/backends/openwrt/openwrt.py index c73b54713..3bd567379 100644 --- a/netjsonconfig/backends/openwrt/openwrt.py +++ b/netjsonconfig/backends/openwrt/openwrt.py @@ -1,4 +1,6 @@ from ..base.backend import BaseBackend +from ..vxlan.vxlan_wireguard import VxlanWireguard +from ..wireguard.wireguard import Wireguard from . import converters from .parser import OpenWrtParser, config_path, packages_pattern from .renderer import OpenWrtRenderer @@ -51,3 +53,70 @@ def _generate_contents(self, tar): name='{0}{1}'.format(config_path, package_name), contents=text_contents, ) + + @classmethod + def wireguard_auto_client(cls, **kwargs): + data = Wireguard.auto_client(**kwargs) + config = { + 'interfaces': [ + { + 'name': data['interface_name'], + 'type': 'wireguard', + 'private_key': data['client']['private_key'], + 'port': data['client']['port'], + # Default values for Wireguard Interface + 'mtu': 1420, + 'nohostroute': False, + 'fwmark': '', + 'ip6prefix': [], + 'addresses': [], + 'network': '', + } + ], + 'wireguard_peers': [ + { + 'interface': data['interface_name'], + 'public_key': data['server']['public_key'], + 'allowed_ips': data['server']['allowed_ips'], + 'endpoint_host': data['server']['endpoint_host'], + 'endpoint_port': data['server']['endpoint_port'], + # Default values for Wireguard Peers + 'preshared_key': '', + 'persistent_keepalive': 60, + 'route_allowed_ips': True, + } + ], + } + if data['client']['ip_address']: + config['interfaces'][0]['addresses'] = [ + { + 'proto': 'static', + 'family': 'ipv4', + 'address': data['client']['ip_address'], + 'mask': 32, + }, + ] + return config + + @classmethod + def vxlan_wireguard_auto_client(cls, **kwargs): + config = cls.wireguard_auto_client(**kwargs) + vxlan_config = VxlanWireguard.auto_client(**kwargs) + vxlan_interface = { + 'name': 'vxlan', + 'type': 'vxlan', + 'vtep': vxlan_config['server_ip_address'], + 'port': 4789, + 'vni': vxlan_config['vni'], + 'tunlink': config['interfaces'][0]['name'], + # Default values for VXLAN interface + 'rxcsum': True, + 'txcsum': True, + 'mtu': 1280, + 'ttl': 64, + 'mac': '', + 'disabled': False, + 'network': '', + } + config['interfaces'].append(vxlan_interface) + return config diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 3bd088c70..fa2f36a49 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -307,7 +307,6 @@ "type": "string", "title": "ipv4 address", "minLength": 7, - "format": "ipv4", "propertyOrder": 3, }, "mask": { @@ -371,7 +370,6 @@ }, "vtep": { "type": "string", - "format": "hostname", "title": "VTEP", "description": "VXLAN Tunnel End Point", "propertyOrder": 1.1, diff --git a/netjsonconfig/backends/vxlan/__init__.py b/netjsonconfig/backends/vxlan/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netjsonconfig/backends/vxlan/vxlan_wireguard.py b/netjsonconfig/backends/vxlan/vxlan_wireguard.py new file mode 100644 index 000000000..080d2697e --- /dev/null +++ b/netjsonconfig/backends/vxlan/vxlan_wireguard.py @@ -0,0 +1,19 @@ +from ..wireguard.wireguard import Wireguard + + +class VxlanWireguard(Wireguard): + @classmethod + def auto_client(cls, vni=0, server_ip_address='', **kwargs): + """ + Returns a configuration dictionary representing VXLAN configuration + that is compatible with the passed server configuration. + + :param vni: Virtual Network Identifier + :param server_ip_address: server internal tunnel address + :returns: dictionary representing VXLAN properties + """ + config = { + 'server_ip_address': server_ip_address, + 'vni': vni, + } + return config diff --git a/netjsonconfig/backends/wireguard/converters.py b/netjsonconfig/backends/wireguard/converters.py index b7ebbbe8e..21463500c 100644 --- a/netjsonconfig/backends/wireguard/converters.py +++ b/netjsonconfig/backends/wireguard/converters.py @@ -16,6 +16,7 @@ def to_intermediate_loop(self, block, result, index=None): def __intermediate_vpn(self, config, remove=None): config['ListenPort'] = config.pop('port') config['PrivateKey'] = config.pop('private_key') + config['Address'] = config.pop('address') config['peers'] = self.__intermediate_peers(config.get('peers', [])) return self.sorted_dict(config) diff --git a/netjsonconfig/backends/wireguard/wireguard.py b/netjsonconfig/backends/wireguard/wireguard.py index c082d2bc9..07776a69d 100644 --- a/netjsonconfig/backends/wireguard/wireguard.py +++ b/netjsonconfig/backends/wireguard/wireguard.py @@ -12,3 +12,30 @@ class Wireguard(BaseVpnBackend): # BaseVpnBackend attributes vpn_pattern = vpn_pattern config_suffix = config_suffix + + @classmethod + def auto_client(cls, host='', public_key='', server={}, port=51820, **kwargs): + """ + Returns a configuration dictionary representing Wireguard configuration + that is compatible with the passed server configuration. + + :param host: remote VPN server + :param port: listen port for Wireguard Client + :param server: dictionary representing a single Wireguard server configuration + :param public_key: public key of the Wireguard server + :returns: dictionary representing a Wireguard server and client properties + """ + return { + 'interface_name': server.get('name', ''), + 'client': { + 'port': port, + 'private_key': kwargs.get('private_key', '{{private_key}}'), + 'ip_address': kwargs.get('ip_address'), + }, + 'server': { + 'public_key': public_key, + 'endpoint_host': host, + 'endpoint_port': server.get('port', 51820), + 'allowed_ips': [kwargs.get('server_ip_network', '')], + }, + } diff --git a/tests/openwrt/test_backend.py b/tests/openwrt/test_backend.py index ac90802fd..3f640e36a 100644 --- a/tests/openwrt/test_backend.py +++ b/tests/openwrt/test_backend.py @@ -421,3 +421,124 @@ def test_override_file(self): # ensure the additional files are there present in the tar.gz archive tar = tarfile.open(fileobj=o.generate(), mode='r') self.assertEqual(len(tar.getmembers()), 1) + + def _get_wireguard_empty_configuration(self): + return { + 'interfaces': [ + { + 'addresses': [], + 'fwmark': '', + 'ip6prefix': [], + 'mtu': 1420, + 'name': '', + 'network': '', + 'nohostroute': False, + 'port': 51820, + 'private_key': '{{private_key}}', + 'type': 'wireguard', + } + ], + 'wireguard_peers': [ + { + 'allowed_ips': [''], + 'endpoint_host': '', + 'endpoint_port': 51820, + 'interface': '', + 'persistent_keepalive': 60, + 'preshared_key': '', + 'public_key': '', + 'route_allowed_ips': True, + } + ], + } + + def _get_vxlan_wireguard_empty_configuration(self): + wireguard_config = self._get_wireguard_empty_configuration() + vxlan_config = { + 'disabled': False, + 'mac': '', + 'mtu': 1280, + 'name': 'vxlan', + 'network': '', + 'port': 4789, + 'rxcsum': True, + 'ttl': 64, + 'tunlink': '', + 'txcsum': True, + 'type': 'vxlan', + 'vni': 0, + 'vtep': '', + } + wireguard_config['interfaces'].append(vxlan_config) + return wireguard_config + + def test_wireguard_auto_client(self): + with self.subTest('No arguments provided'): + expected = self._get_wireguard_empty_configuration() + self.assertDictEqual(OpenWrt.wireguard_auto_client(), expected) + with self.subTest('Required arguments provided'): + expected = self._get_wireguard_empty_configuration() + expected['interfaces'][0].update( + { + 'name': 'wg', + 'private_key': '{{private_key}}', + 'addresses': [ + { + 'address': '10.0.0.2', + 'family': 'ipv4', + 'mask': 32, + 'proto': 'static', + }, + ], + } + ) + expected['wireguard_peers'][0].update( + { + 'allowed_ips': ['10.0.0.1/24'], + 'endpoint_host': '0.0.0.0', + 'public_key': 'server_public_key', + 'interface': 'wg', + } + ) + self.assertDictEqual( + OpenWrt.wireguard_auto_client( + host='0.0.0.0', + public_key='server_public_key', + server={'name': 'wg', 'port': 51820}, + server_ip_network='10.0.0.1/24', + ip_address='10.0.0.2', + ), + expected, + ) + + def test_vxlan_wireguard_auto_client(self): + with self.subTest('No arguments provided'): + expected = self._get_vxlan_wireguard_empty_configuration() + self.assertDictEqual(OpenWrt.vxlan_wireguard_auto_client(), expected) + with self.subTest('Required arguments provided'): + expected = self._get_vxlan_wireguard_empty_configuration() + expected['interfaces'][0].update( + {'name': 'wg', 'private_key': '{{private_key}}'} + ) + expected['wireguard_peers'][0].update( + { + 'allowed_ips': ['10.0.0.1/24'], + 'endpoint_host': '0.0.0.0', + 'public_key': 'server_public_key', + 'interface': 'wg', + } + ) + expected['interfaces'][1].update( + {'tunlink': 'wg', 'vni': 1, 'vtep': '10.0.0.1'} + ) + self.assertDictEqual( + OpenWrt.vxlan_wireguard_auto_client( + host='0.0.0.0', + public_key='server_public_key', + server={'name': 'wg', 'port': 51820}, + server_ip_network='10.0.0.1/24', + vni=1, + server_ip_address='10.0.0.1', + ), + expected, + ) diff --git a/tests/openwrt/test_wireguard.py b/tests/openwrt/test_wireguard.py index 5ff681e4d..a89dc5795 100644 --- a/tests/openwrt/test_wireguard.py +++ b/tests/openwrt/test_wireguard.py @@ -161,9 +161,9 @@ def test_render_wireguard_peer_with_variables(self): "wireguard_peers": [ { "interface": "wg0", - "public_key": "{{pub_key_8097b09be57a4b278e2ef2ea9ea809f3}}", + "public_key": "{{public_key_8097b09be57a4b278e2ef2ea9ea809f3}}", "allowed_ips": [ - "{{server_ip_max_prefix_8097b09be57a4b278e2ef2ea9ea809f3}}" + "{{server_ip_network_8097b09be57a4b278e2ef2ea9ea809f3}}" ], "endpoint_host": "{{vpn_host_8097b09be57a4b278e2ef2ea9ea809f3}}", "endpoint_port": 40840, @@ -174,9 +174,9 @@ def test_render_wireguard_peer_with_variables(self): ] }, context={ - "server_ip_max_prefix_8097b09be57a4b278e2ef2ea9ea809f3": "10.0.0.1/32", + "server_ip_network_8097b09be57a4b278e2ef2ea9ea809f3": "10.0.0.1/32", "vpn_host_8097b09be57a4b278e2ef2ea9ea809f3": "192.168.1.42", - "pub_key_8097b09be57a4b278e2ef2ea9ea809f3": "rn+isMBpyQ4HX6ZzE709bKnZw5IaLZoIS3hIjmfKCkk=", + "public_key_8097b09be57a4b278e2ef2ea9ea809f3": "rn+isMBpyQ4HX6ZzE709bKnZw5IaLZoIS3hIjmfKCkk=", "pre_key_8097b09be57a4b278e2ef2ea9ea809f3": "oPZmGdHBseaV1TF0julyElNuJyeKs2Eo+o62R/09IB4=", }, ) diff --git a/tests/vxlan/__init__.py b/tests/vxlan/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/vxlan/test_vxlan_wireguard.py b/tests/vxlan/test_vxlan_wireguard.py new file mode 100644 index 000000000..078689a6f --- /dev/null +++ b/tests/vxlan/test_vxlan_wireguard.py @@ -0,0 +1,25 @@ +import unittest + +from netjsonconfig import VxlanWireguard + + +class TestBackend(unittest.TestCase): + def test_auto_client(self): + with self.subTest('No arguments are provided'): + expected = { + 'server_ip_address': '', + 'vni': 0, + } + self.assertDictEqual(VxlanWireguard.auto_client(), expected) + + with self.subTest('All arguments are provided'): + expected = { + 'server_ip_address': '10.0.0.1', + 'vni': 1, + } + self.assertDictEqual( + VxlanWireguard.auto_client( + vni=1, server_ip_address='10.0.0.1', server={} + ), + expected, + ) diff --git a/tests/wireguard/test_backend.py b/tests/wireguard/test_backend.py index 0dcb2c07e..aa1aa93b8 100644 --- a/tests/wireguard/test_backend.py +++ b/tests/wireguard/test_backend.py @@ -27,11 +27,13 @@ def test_confs(self): "name": "test1", "private_key": "QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", "port": 40842, + "address": "10.0.0.1/24", }, { "name": "test2", "private_key": "AFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", "port": 40843, + "address": "10.0.1.1/24", }, ] } @@ -39,12 +41,14 @@ def test_confs(self): expected = """# wireguard config: test1 [Interface] +Address = 10.0.0.1/24 ListenPort = 40842 PrivateKey = QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI= # wireguard config: test2 [Interface] +Address = 10.0.1.1/24 ListenPort = 40843 PrivateKey = AFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI= """ @@ -58,6 +62,7 @@ def test_peers(self): "name": "test1", "private_key": "QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", "port": 40842, + "address": "10.0.0.1/24", "peers": [ { "public_key": "jqHs76yCH0wThMSqogDshndAiXelfffUJVcFmz352HI=", @@ -78,6 +83,7 @@ def test_peers(self): expected = """# wireguard config: test1 [Interface] +Address = 10.0.0.1/24 ListenPort = 40842 PrivateKey = QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI= @@ -101,6 +107,7 @@ def test_generate(self): "name": "test1", "private_key": "QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", "port": 40842, + "address": "10.0.0.1/24", "peers": [ { "public_key": "jqHs76yCH0wThMSqogDshndAiXelfffUJVcFmz352HI=", @@ -117,6 +124,7 @@ def test_generate(self): vpn1 = tar.getmember('test1.conf') contents = tar.extractfile(vpn1).read().decode() expected = """[Interface] +Address = 10.0.0.1/24 ListenPort = 40842 PrivateKey = QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI= @@ -125,3 +133,46 @@ def test_generate(self): PublicKey = jqHs76yCH0wThMSqogDshndAiXelfffUJVcFmz352HI= """ self.assertEqual(contents, expected) + + def test_auto_client(self): + with self.subTest('No arguments are provided'): + expected = { + 'interface_name': '', + 'client': { + 'port': 51820, + 'private_key': '{{private_key}}', + 'ip_address': None, + }, + 'server': { + 'public_key': '', + 'endpoint_host': '', + 'endpoint_port': 51820, + 'allowed_ips': [''], + }, + } + self.assertDictEqual(Wireguard.auto_client(), expected) + with self.subTest('Required arguments are provided'): + expected = { + 'interface_name': 'wg', + 'client': { + 'port': 51820, + 'private_key': '{{private_key}}', + 'ip_address': '10.0.0.2', + }, + 'server': { + 'public_key': 'server_public_key', + 'endpoint_host': '0.0.0.0', + 'endpoint_port': 51820, + 'allowed_ips': ['10.0.0.1/24'], + }, + } + self.assertDictEqual( + Wireguard.auto_client( + host='0.0.0.0', + public_key='server_public_key', + server={'name': 'wg', 'port': 51820}, + server_ip_network='10.0.0.1/24', + ip_address='10.0.0.2', + ), + expected, + ) diff --git a/tests/wireguard/test_parser.py b/tests/wireguard/test_parser.py new file mode 100644 index 000000000..0f081f498 --- /dev/null +++ b/tests/wireguard/test_parser.py @@ -0,0 +1,34 @@ +import unittest +from unittest.mock import patch + +from netjsonconfig.backends.wireguard.parser import WireguardParser + + +class TestBaseParser(unittest.TestCase): + """ + Tests for netjsonconfig.backends.wireguard.parser.BaseParser + """ + + def test_parse_text(self): + # Creating an instance of WireguardParser will raise + # NotImplementedError since it will requires "parse_text" + with self.assertRaises(NotImplementedError): + WireguardParser(config="") + + @patch.object(WireguardParser, 'parse_text', return_value=None) + def test_parse_tar(self, mocked): + parser = WireguardParser(config="") + with self.assertRaises(NotImplementedError): + parser.parse_tar(tar=None) + + @patch.object(WireguardParser, 'parse_text', return_value=None) + def test_get_vpns(self, mocked): + parser = WireguardParser(config="") + with self.assertRaises(NotImplementedError): + parser._get_vpns(text=None) + + @patch.object(WireguardParser, 'parse_text', return_value=None) + def test_get_config(self, mocked): + parser = WireguardParser(config="") + with self.assertRaises(NotImplementedError): + parser._get_config(contents=None) From 8af254f34d71b62a8e0bcfb67dd3489df5f18fa7 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 21 May 2021 01:34:05 +0530 Subject: [PATCH 8/9] [docs] Added documentation for WireGuard and VXLAN VPN backend - Added examples for WireGuard, VXLAN and VXLAN over WireGuard - Updated OpenWrt documentation to describle usage of auto_client methods --- README.rst | 3 +- docs/source/backends/openwrt.rst | 283 ++++++++++++++++++ docs/source/backends/vpn.rst | 14 + docs/source/backends/vxlan_over_wireguard.rst | 79 +++++ docs/source/backends/wireguard.rst | 192 +++++++++++- docs/source/general/basics.rst | 2 + docs/source/index.rst | 4 +- 7 files changed, 568 insertions(+), 9 deletions(-) create mode 100644 docs/source/backends/vpn.rst create mode 100644 docs/source/backends/vxlan_over_wireguard.rst diff --git a/README.rst b/README.rst index c2345cb3e..4452b1aac 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ netjsonconfig .. image:: https://badge.fury.io/py/netjsonconfig.svg :target: http://badge.fury.io/py/netjsonconfig - + .. image:: https://pepy.tech/badge/netjsonconfig :target: https://pepy.tech/project/netjsonconfig :alt: downloads @@ -43,6 +43,7 @@ Its main features are listed below for your reference: * `OpenWRT `_ / `LEDE `_ support * `OpenWisp Firmware `_ support * `OpenVPN `_ support +* `WireGuard `_ support * Possibility to support more firmwares via custom backends * Based on the `NetJSON RFC `_ * **Validation** based on `JSON-Schema `_ diff --git a/docs/source/backends/openwrt.rst b/docs/source/backends/openwrt.rst index 3749c051c..7ab8b0a2f 100644 --- a/docs/source/backends/openwrt.rst +++ b/docs/source/backends/openwrt.rst @@ -2285,6 +2285,289 @@ Will be rendered as follows:: option proto 'udp' option tls_server '1' +WireGuard +--------- + +This backend includes the schema of the ``Wireguard`` backend, inheriting its features. + +For details regarding the **WireGuard** schema please see :ref:`wireguard_backend_schema`. + +Schema additions +~~~~~~~~~~~~~~~~ + +The ``OpenWrt`` backend adds a few properties to the WireGuard schema, see below. + ++-----------------+---------+--------------+-------------------------------------------------------------+ +| key name | type | default | description | ++=================+=========+==============+=============================================================+ +| ``network`` | string | ``None`` | logical interface name (UCI specific), | +| | | | | +| | | | 2 to 15 alphanumeric characters, dashes and underscores | ++-----------------+---------+--------------+-------------------------------------------------------------+ +| ``nohostroute`` | boolean | ``False`` | do not add routes to ensure the tunnel endpoints are routed | +| | | | via non-tunnel device | ++-----------------+---------+--------------+-------------------------------------------------------------+ +| ``fwmark`` | string | ``None`` | firewall mark to apply to tunnel endpoint packets | ++-----------------+---------+--------------+-------------------------------------------------------------+ +| ``ip6prefix`` | list | ``[]`` | IPv6 prefixes to delegate to other interfaces | ++-----------------+---------+--------------+-------------------------------------------------------------+ +| ``addresses`` | list | ``[]`` | list of unique IPv4 or IPv6 addresses | ++-----------------+---------+--------------+-------------------------------------------------------------+ + +The ``OpenWrt`` backend also adds ``wireguard_peers`` option for sepecifying a list of +WireGuard Peers. It add the following properties to the ``wireguard_peers`` property of +WireGuard schema. + ++-----------------------+---------+-----------+------------------------------------------------------------------------+ +| key name | type | default | description | ++=======================+=========+===========+========================================================================+ +| ``interface`` | string | ``None`` | name of the wireguard interface, | +| | | | | +| | | | 2 to 15 alphanumeric characters, dashes and underscores | ++-----------------------+---------+-----------+------------------------------------------------------------------------+ +| ``route_allowed_ips`` | boolean | ``False`` | automatically create a route for each of the Allowed IPs for this peer | ++-----------------------+---------+-----------+------------------------------------------------------------------------+ + +WireGuard example +~~~~~~~~~~~~~~~~~ + +The following *configuration dictionary*: + +.. code-block:: python + + { + "interfaces": [ + { + "name": "wg", + "type": "wireguard", + "private_key": "QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", + "port": 51820, + "mtu": 1420, + "nohostroute": False, + "fwmark": "", + "ip6prefix": [], + "addresses": [ + { + "proto": "static", + "family": "ipv4", + "address": "10.0.0.5/32", + "mask": 32, + } + ], + "network": "", + } + ], + "wireguard_peers": [ + { + "interface": "wg", + "public_key": "94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=", + "allowed_ips": ["10.0.0.1/32"], + "endpoint_host": "wireguard.test.com", + "endpoint_port": 51820, + "preshared_key": "", + "persistent_keepalive": 60, + "route_allowed_ips": True, + } + ] + } + +Will be rendered as follows: + +.. code-block:: text + + package network + + config interface 'wg' + list addresses '10.0.0.5/32/32' + option listen_port '51820' + option mtu '1420' + option nohostroute '0' + option private_key 'QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=' + option proto 'wireguard' + + config wireguard_wg 'wgpeer' + list allowed_ips '10.0.0.1/32' + option endpoint_host 'wireguard.test.com' + option endpoint_port '51820' + option persistent_keepalive '60' + option public_key '94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=' + option route_allowed_ips '1' + +VXLAN +----- + +``OpenWrt`` backend includes the schema requied for generating VXLAN +interface configouration. This is useful of setting up layer 2 tunnels. + + +VXLAN Settings +~~~~~~~~~~~~~~ + ++-------------+-------------------+--------------+-------------------------------------------------------------+ +| key name | type | default | description | ++=============+===================+==============+=============================================================+ +| ``network`` | string | ``None`` | name of interface, | +| | | | | +| | | | 2 to 15 alphanumeric characters, dashes and underscores | ++-------------+-------------------+--------------+-------------------------------------------------------------+ +| ``vtep`` | string | ``False`` | VXLAN tunnel endpoint | ++-------------+-------------------+--------------+-------------------------------------------------------------+ +| ``port`` | integer | ``4789`` | port for VXLAN connection | ++-------------+-------------------+--------------+-------------------------------------------------------------+ +| ``vni`` | integer or string | ``None`` | VXLAN Network Identifier | ++-------------+-------------------+--------------+-------------------------------------------------------------+ +| ``tunlink`` | list | ``[]`` | interface to which the VXLAN tunnel will be bound | ++-------------+-------------------+--------------+-------------------------------------------------------------+ +| ``rxcsum`` | boolean | ``True`` | use checksum validation in RX direction | ++-------------+-------------------+--------------+-------------------------------------------------------------+ +| ``txcsum`` | boolean | ``True`` | use checksum validation in TX direction | ++-------------+-------------------+--------------+-------------------------------------------------------------+ +| ``mtu`` | integer | ``1280`` | MTU for route, only numbers are allowed | ++-------------+-------------------+--------------+-------------------------------------------------------------+ +| ``ttl`` | integer | ``64`` | TTL of the encapsulation packets | ++-------------+-------------------+--------------+-------------------------------------------------------------+ + +VXLAN example +~~~~~~~~~~~~~ + +The following *configuration dictionary*: + +.. code-block:: python + + { + "interfaces": [ + { + "name": "vxlan", + "type": "vxlan", + "vtep": "10.0.0.1", + "port": 4789, + "vni": 1, + "tunlink": "", + "rxcsum": True, + "txcsum": True, + "mtu": 1280, + "ttl": 64, + "mac": "", + "disabled": False, + "network": "", + }, + ] + } + +Will be rendered as follows: + +.. code-block:: text + + package network + + config interface 'vxlan' + option enabled '0' + option ifname 'vxlan' + option mtu '1280' + option peeraddr '10.0.0.1' + option port '4789' + option proto 'vxlan' + option rxcsum '1' + option ttl '64' + option txcsum '1' + option vid '1' + +VXLAN over WireGuard example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since a layer 2 tunnel can be encapsulated in a layer 3 tunnel, here is an +example configuration for setting up a VXLAN tunnel over WireGuard. + +The following *configuration dictionary*: + +.. code-block:: python + + { + "interfaces": [ + { + "name": "wgvxlan", + "type": "wireguard", + "private_key": "QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", + "port": 51820, + "mtu": 1420, + "nohostroute": False, + "fwmark": "", + "ip6prefix": [], + "addresses": [ + { + "proto": "static", + "family": "ipv4", + "address": "10.0.0.5/32", + "mask": 32, + } + ], + "network": "", + }, + { + "name": "vxlan", + "type": "vxlan", + "vtep": "10.0.0.1", + "port": 4789, + "vni": 1, + "tunlink": "wgvxlan", + "rxcsum": True, + "txcsum": True, + "mtu": 1280, + "ttl": 64, + "mac": "", + "disabled": False, + "network": "", + }, + ], + "wireguard_peers": [ + { + "interface": "wgvxlan", + "public_key": "94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=", + "allowed_ips": ["10.0.0.1/32"], + "endpoint_host": "wireguard.test.com", + "endpoint_port": 51820, + "preshared_key": "", + "persistent_keepalive": 60, + "route_allowed_ips": True, + } + ] + } + +Will be rendered as follows: + +.. code-block:: text + + package network + + config interface 'wgvxlan' + list addresses '10.0.0.5/32/32' + option listen_port '51820' + option mtu '1420' + option nohostroute '0' + option private_key 'QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=' + option proto 'wireguard' + + config interface 'vxlan' + option enabled '1' + option ifname 'vxlan' + option mtu '1280' + option peeraddr '10.0.0.1' + option port '4789' + option proto 'vxlan' + option rxcsum '1' + option ttl '64' + option tunlink 'wgvxlan' + option txcsum '1' + option vid '1' + + config wireguard_wgvxlan 'wgpeer' + list allowed_ips '10.0.0.1/32' + option endpoint_host 'wireguard.test.com' + option endpoint_port '51820' + option persistent_keepalive '60' + option public_key '94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=' + option route_allowed_ips '1' + All the other settings ---------------------- diff --git a/docs/source/backends/vpn.rst b/docs/source/backends/vpn.rst new file mode 100644 index 000000000..579aa0d52 --- /dev/null +++ b/docs/source/backends/vpn.rst @@ -0,0 +1,14 @@ +============ +VPN Backends +============ + +.. include:: ../_github.rst + +``netjsonconfig`` currently supports three VPN backends: + +.. toctree:: + :maxdepth: 2 + + /backends/openvpn + /backends/wireguard + /backends/vxlan_over_wireguard diff --git a/docs/source/backends/vxlan_over_wireguard.rst b/docs/source/backends/vxlan_over_wireguard.rst new file mode 100644 index 000000000..bddb6f28d --- /dev/null +++ b/docs/source/backends/vxlan_over_wireguard.rst @@ -0,0 +1,79 @@ +============================ +VXLAN over WireGuard Backend +============================ + +.. include:: ../_github.rst + +The ``VXLAN over WireGuard`` backend extends :doc:`Wireguard backend ` +to add configurations required for configuring VXLAN tunnels encapsulated in +WireGuard tunnels. + +Automatic generation of clients +------------------------------- + +.. automethod:: netjsonconfig.OpenWrt.vxlan_wireguard_auto_client + +Example: + +.. code-block:: python + + from netjsonconfig import OpenWrt + + server_config = { + "name": "wgvxlan", + "port": 51820, + "public_key": "94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=", + "server_ip_network": "10.0.0.1/32", + "server_ip_address": "10.0.0.1" + } + client_config = OpenWrt.vxlan_wireguard_auto_client(host='wireguard.test.com', + vni=1, + server_ip_address=server_config['server_ip_address'], + server=server_config, + public_key=server_config['public_key'], + port=51820, + private_key='QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=', + ip_address='10.0.0.5/32', + server_ip_network=server_config['server_ip_network']) + print(OpenWrt(client_config).render()) + +Will be rendered as: + +.. code-block:: text + + package network + + config interface 'wgvxlan' + list addresses '10.0.0.5/32/32' + option listen_port '51820' + option mtu '1420' + option nohostroute '0' + option private_key 'QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=' + option proto 'wireguard' + + config interface 'vxlan' + option enabled '1' + option ifname 'vxlan' + option mtu '1280' + option peeraddr '10.0.0.1' + option port '4789' + option proto 'vxlan' + option rxcsum '1' + option ttl '64' + option tunlink 'wgvxlan' + option txcsum '1' + option vid '1' + + config wireguard_wgvxlan 'wgpeer' + list allowed_ips '10.0.0.1/32' + option endpoint_host 'wireguard.test.com' + option endpoint_port '51820' + option persistent_keepalive '60' + option public_key '94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=' + option route_allowed_ips '1' + +.. note:: + + The current implementation of **VXLAN over WireGuard** VPN backend is implemented with + **OpenWrt** backend. Hence, the example above shows configuration generated for + OpenWrt. diff --git a/docs/source/backends/wireguard.rst b/docs/source/backends/wireguard.rst index 61fd853e7..5601e5f1e 100644 --- a/docs/source/backends/wireguard.rst +++ b/docs/source/backends/wireguard.rst @@ -1,13 +1,193 @@ ================= -Wireguard Backend +WireGuard Backend ================= .. include:: ../_github.rst -The ``Wireguard`` backend allows to generate Wireguard compatible configurations. +The ``WireGuard`` backend allows to generate WireGuard configurations. -This backend is designed to allow updating the configuration of a wireguard peer -automatically when the configuration of a VPN server is changed in OpenWISP, or -when a new peer is added to OpenWISP (a new device with wireguard VPN client template). +Its schema is limited to a subset of the features available in WireGuard and it doesn't recognize +interfaces, radios, wireless settings and so on. -TODO... +The main methods work just like the :doc:`OpenWRT backend `: + +* ``__init__`` +* ``render`` +* ``generate`` +* ``write`` +* ``json`` + +The main differences are in the resulting configuration and in its schema. + +See an example of initialization and rendering below: + +.. code-block:: python + + from netjsonconfig import Wireguard + + config = Wireguard( + { + "wireguard": [ + { + "name": "wg", + "private_key": "QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=", + "port": 40842, + "address": "10.0.0.1/24", + "peers": [ + { + "public_key": "jqHs76yCH0wThMSqogDshndAiXelfffUJVcFmz352HI=", + "allowed_ips": "10.0.0.3/32", + }, + { + "public_key": "94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=", + "allowed_ips": "10.0.0.4/32", + "preshared_key": "xisFXck9KfEZga4hlkproH6+86S8ki1tmLtMtqVipjg=", + "endpoint_host": "192.168.1.35", + "endpoint_port": 4908, + }, + ], + } + ] + } + ) + print(config.render()) + +Will return the following output: + +.. code-block:: text + + # wireguard config: wg + + [Interface] + Address = 10.0.0.1/24 + ListenPort = 40842 + PrivateKey = QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI= + + [Peer] + AllowedIPs = 10.0.0.3/32 + PublicKey = jqHs76yCH0wThMSqogDshndAiXelfffUJVcFmz352HI= + + [Peer] + AllowedIPs = 10.0.0.4/32 + Endpoint = 192.168.1.35:4908 + PreSharedKey = xisFXck9KfEZga4hlkproH6+86S8ki1tmLtMtqVipjg= + PublicKey = 94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE= + +.. _wireguard_backend_schema: + +WireGuard backend schema +------------------------ + +The ``Wireguard`` backend schema is limited, it only recognizes an ``wireguard`` key with +a list of dictionaries representing vpn instances. The structure of these dictionaries +is described below. + +Alternatively you may also want to take a look at the `WireGuard JSON-Schema source code +`_. + +According to the `NetJSON `_ spec, any unrecognized property will be ignored. + +Server settings (valid both for client and server) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Required properties: + +- name +- port +- private_key + ++-----------------+---------+--------------+------------------------------------------------------------------------------------+ +| key name | type | default | allowed values | ++=================+=========+==============+====================================================================================+ +| ``name`` | string | | 2 to 15 alphanumeric characters, dashes and underscores | ++-----------------+---------+--------------+------------------------------------------------------------------------------------+ +| ``port`` | integer | ``51820`` | integers | ++-----------------+---------+--------------+------------------------------------------------------------------------------------+ +| ``private_key`` | string | | base64-encoded private key | ++-----------------+---------+--------------+------------------------------------------------------------------------------------+ +| ``peers`` | list | ``[]`` | list of dictionaries containing following information of | +| | | | each peer: | +| | | | | +| | | | +-------------------+---------+--------------------------------------------------+ | +| | | | | key name | type | allowed values | | +| | | | +===================+=========+==================================================+ | +| | | | | ``public_key`` | string | base64-encoded public key of peer | | +| | | | +-------------------+---------+--------------------------------------------------+ | +| | | | | ``allowed_ips`` | string | internal VPN IP address of peer in CIDR notation | | +| | | | +-------------------+---------+--------------------------------------------------+ | +| | | | | ``endpoint_host`` | string | public IP address of peer | | +| | | | +-------------------+---------+--------------------------------------------------+ | +| | | | | ``endpoint_port`` | integer | integers | | +| | | | +-------------------+---------+--------------------------------------------------+ | +| | | | | ``preshared_key`` | string | base64-encoded pre-shared key | | +| | | | +-------------------+---------+--------------------------------------------------+ | ++-----------------+---------+--------------+------------------------------------------------------------------------------------+ + +Working around schema limitations +--------------------------------- + +The schema does not include all the possible WireGuard settings, but it can render appropriately +any property not included in the schema as long as its type is one the following: + +* boolean +* integer +* strings +* lists + +For a list of all the WireGuard configuration settings, refer to the `"Configuration" section +of wg-quick(8) `_ and +`"Configuration File Format" section of wg(8) `_ + +Automatic generation of clients +------------------------------- + +.. automethod:: netjsonconfig.OpenWrt.wireguard_auto_client + +Example: + +.. code-block:: python + + from netjsonconfig import OpenWrt + + server_config = { + "name": "wg", + "port": 51820, + "public_key": "94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=", + "server_ip_network": "10.0.0.1/32" + } + client_config = OpenWrt.wireguard_auto_client(host='wireguard.test.com', + server=server_config, + public_key=server_config['public_key'], + port=51820, + private_key='QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=', + ip_address='10.0.0.5/32', + server_ip_network=server_config['server_ip_network']) + print(OpenWrt(client_config).render()) + +Will be rendered as: + +.. code-block:: text + + package network + + config interface 'wg' + list addresses '10.0.0.5/32/32' + option listen_port '51820' + option mtu '1420' + option nohostroute '0' + option private_key 'QFdbnuYr7rrF4eONCAs7FhZwP7BXX/jD/jq2LXCpaXI=' + option proto 'wireguard' + + config wireguard_wg 'wgpeer' + list allowed_ips '10.0.0.1/32' + option endpoint_host 'wireguard.test.com' + option endpoint_port '51820' + option persistent_keepalive '60' + option public_key '94a+MnZSdzHCzOy5y2K+0+Xe7lQzaa4v7lEiBZ7elVE=' + option route_allowed_ips '1' + +.. note:: + + The current implementation of **WireGuard VPN** backend is implemented with + **OpenWrt** backend. Hence, the example above shows configuration generated for + OpenWrt. diff --git a/docs/source/general/basics.rst b/docs/source/general/basics.rst index 902102f59..323f55ae3 100644 --- a/docs/source/general/basics.rst +++ b/docs/source/general/basics.rst @@ -103,6 +103,8 @@ The current implemented backends are: * :doc:`OpenWrt ` * :doc:`OpenWisp ` (based on the ``OpenWrt`` backend) * :doc:`OpenVpn ` (custom backend implementing only OpenVPN configuration) + * :doc:`WireGuard ` (custom backend implementing only WireGuard configuration) + * :doc:`VXLAN over WireGuard ` (custom backend implementing only VXLAN over WireGuard configuration) Example initialization of ``OpenWrt`` backend: diff --git a/docs/source/index.rst b/docs/source/index.rst index 8e5c9a69a..f01c09a5b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -36,6 +36,7 @@ Its main features are: * `OpenWRT `_ / `LEDE `_ support * `OpenWisp Firmware `_ support * `OpenVPN `_ support + * `Wireguard `_ support * Plugin interface for external backends, support more firmwares with an external package * :doc:`Create your backend ` as a plugin @@ -58,8 +59,7 @@ Contents: /general/basics /backends/openwrt /backends/openwisp - /backends/openvpn - /backends/wireguard + /backends/vpn /backends/create_your_backend /general/commandline_utility /general/running_tests From 63652239670f0b638fcfd603fc61ab352ee5ac43 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 21 May 2021 03:35:31 +0530 Subject: [PATCH 9/9] [ci] Updated package install command --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b6bcb044..a0569ea34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: pip install -U -r requirements-test.txt - name: Install netjsonconfig - run: python setup.py -q develop + run: pip install -U -e . - name: QA checks run: ./run-qa-checks