Skip to content

Commit

Permalink
[feature:gsoc23] Added ZeroTier VPN backend & OpenWrt support #208 #284
Browse files Browse the repository at this point in the history
Closes #208
Closes #284

Co-authored-by: Federico Capoano <f.capoano@openwisp.io>
  • Loading branch information
Aryamanz29 and nemesifier committed Oct 11, 2023
1 parent 68681a1 commit 571af14
Show file tree
Hide file tree
Showing 23 changed files with 2,160 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Its main features are listed below for your reference:
* `OpenWisp Firmware <https://github.com/openwisp/OpenWISP-Firmware>`_ support
* `OpenVPN <https://openvpn.net>`_ support
* `WireGuard <https://www.wireguard.com/>`_ support
* `ZeroTier <https://www.zerotier.com/>`_ support
* Possibility to support more firmwares via custom backends
* Based on the `NetJSON RFC <http://netjson.org/rfc.html>`_
* **Validation** based on `JSON-Schema <http://json-schema.org/>`_
Expand Down
1 change: 1 addition & 0 deletions docs/source/backends/vpn.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ VPN Backends
/backends/openvpn
/backends/wireguard
/backends/vxlan_over_wireguard
/backends/zerotier
415 changes: 415 additions & 0 deletions docs/source/backends/zerotier.rst

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/source/general/basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ The current implemented backends are:
* :doc:`OpenVpn </backends/openvpn>` (custom backend implementing only OpenVPN configuration)
* :doc:`WireGuard </backends/wireguard>` (custom backend implementing only WireGuard configuration)
* :doc:`VXLAN over WireGuard </backends/vxlan_over_wireguard>` (custom backend implementing only VXLAN over WireGuard configuration)
* :doc:`ZeroTier </backends/zerotier>` (custom backend implementing only ZeroTier configuration)

Example initialization of ``OpenWrt`` backend:

Expand Down
2 changes: 2 additions & 0 deletions netjsonconfig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .backends.openwrt.openwrt import OpenWrt # noqa
from .backends.vxlan.vxlan_wireguard import VxlanWireguard # noqa
from .backends.wireguard.wireguard import Wireguard # noqa
from .backends.zerotier.zerotier import ZeroTier # noqa
from .version import VERSION, __version__, get_version # noqa


Expand All @@ -16,6 +17,7 @@ def get_backends():
'openwisp': OpenWisp,
'openvpn': OpenVpn,
'wireguard': Wireguard,
'zerotier': ZeroTier,
}
logger = logging.getLogger(__name__)

Expand Down
4 changes: 3 additions & 1 deletion netjsonconfig/backends/base/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,9 @@ def _generate_contents(self, tar):
# create a file for each VPN
for vpn in vpn_instances:
lines = vpn.split('\n')
vpn_name = lines[0]
# It's better to split lines[0] using
# `config_suffix` to extract the correct vpn_name
vpn_name = lines[0].split(self.config_suffix)[0]
text_contents = '\n'.join(lines[2:])
# do not end with double new line
if text_contents.endswith('\n\n'):
Expand Down
2 changes: 2 additions & 0 deletions netjsonconfig/backends/openwrt/converters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .switch import Switch
from .wireguard_peers import WireguardPeers
from .wireless import Wireless
from .zerotier import ZeroTier

__all__ = [
'Default',
Expand All @@ -24,4 +25,5 @@
'Switch',
'WireguardPeers',
'Wireless',
'ZeroTier',
]
2 changes: 1 addition & 1 deletion netjsonconfig/backends/openwrt/converters/openvpn.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __intermediate_vpn(self, vpn):
def __netjson_vpn(self, vpn):
if vpn.get('server_bridge') == '1':
vpn['server_bridge'] = ''
# 'enabled' defaults to False in OpenWRT
# 'disabled' defaults to False in OpenWRT
vpn['disabled'] = vpn.pop('enabled', '0') == '0'
vpn['name'] = vpn.pop('.name')
del vpn['.type']
Expand Down
62 changes: 62 additions & 0 deletions netjsonconfig/backends/openwrt/converters/zerotier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from ...zerotier.converters import ZeroTier as BaseZeroTier
from ..schema import schema
from .base import OpenWrtConverter


class ZeroTier(OpenWrtConverter, BaseZeroTier):
_uci_types = ['zerotier']
_schema = schema['properties']['zerotier']['items']

def __intermediate_vpn(self, vpn):
nwid_ifnames = vpn.get('networks', [])
files = self.netjson.get('files', [])
self.netjson['files'] = self.__get_zt_ifname_files(vpn, files)
vpn.update(
{
'.name': self._get_uci_name(vpn.pop('name')),
'.type': 'zerotier',
'config_path': vpn.get('config_path', '/etc/openwisp/zerotier'),
'copy_config_path': vpn.get('copy_config_path', '1'),
'join': [networks.get('id', '') for networks in nwid_ifnames],
'enabled': not vpn.pop('disabled', False),
}
)
del vpn['networks']
return super().__intermediate_vpn(vpn, remove=[''])

def __netjson_vpn(self, vpn):
nwids = vpn.pop('join')
vpn['name'] = vpn.pop('.name')
vpn['networks'] = [{"id": nwid, "ifname": f"owzt{nwid[-6:]}"} for nwid in nwids]
# 'disabled' defaults to False in OpenWRT
vpn['disabled'] = vpn.pop('enabled', '0') == '0'
del vpn['.type']
return super().__netjson_vpn(vpn)

def __get_zt_ifname_files(self, vpn, files):
config_path = vpn.get('config_path', '/etc/openwisp/zerotier')
nwid_ifnames = vpn.get('networks', [])
zt_file_contents = '# network_id=interface_name\n'

for networks in nwid_ifnames:
nwid = networks.get('id', '')
ifname = networks.get('ifname')
zt_file_contents += f"{nwid}={ifname}\n"

zt_interface_map = {
'path': f"{config_path}/devicemap",
'mode': '0644',
'contents': zt_file_contents,
}

if not files:
return [zt_interface_map]
updated_files = []
for file in files:
if file.get('path') == zt_interface_map.get('path'):
zt_interface_map['contents'] += '\n' + file['contents']
else:
updated_files.append(file)
if zt_interface_map.get('contents'):
updated_files.append(zt_interface_map)
return updated_files
7 changes: 7 additions & 0 deletions netjsonconfig/backends/openwrt/openwrt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from ..base.backend import BaseBackend
from ..vxlan.vxlan_wireguard import VxlanWireguard
from ..wireguard.wireguard import Wireguard
from ..zerotier.zerotier import ZeroTier
from . import converters
from .parser import OpenWrtParser, config_path, packages_pattern
from .renderer import OpenWrtRenderer
Expand All @@ -27,6 +28,7 @@ class OpenWrt(BaseBackend):
converters.Wireless,
converters.OpenVpn,
converters.WireguardPeers,
converters.ZeroTier,
converters.Default,
]
parser = OpenWrtParser
Expand Down Expand Up @@ -142,6 +144,11 @@ def vxlan_wireguard_auto_client(cls, **kwargs):
config['interfaces'].append(vxlan_interface)
return config

@classmethod
def zerotier_auto_client(cls, **kwargs):
data = ZeroTier.auto_client(**kwargs)
return {'zerotier': [data]}

def validate(self):
self._validate_radios()
super().validate()
Expand Down
107 changes: 107 additions & 0 deletions netjsonconfig/backends/openwrt/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,113 @@
},
},
},
"zerotier": {
"type": "array",
"title": "ZeroTier Networks",
"uniqueItems": True,
"propertyOrder": 14,
"items": {
"type": "object",
"title": "Network Member Configuration",
"additionalProperties": True,
"required": ["name", "networks"],
"properties": {
# ZeroTier customization (disabled) for OpenWRT
"disabled": {
"title": "disabled",
"description": "Disable this VPN without deleting its configuration",
"type": "boolean",
"default": False,
"format": "checkbox",
"propertyOrder": 1,
},
"name": {
"type": "string",
"propertyOrder": 2,
"default": "ow_zt",
"minLength": 1,
"description": "Name of the zerotier network member configuration",
},
"networks": {
"type": "array",
"title": "Networks",
"minItems": 1,
"propertyOrder": 3,
"uniqueItems": True,
"additionalProperties": True,
"items": {
"type": "object",
"title": "Network Member",
"allOf": [{"required": ["id", "ifname"]}],
"properties": {
"id": {
"type": "string",
"title": "Network ID",
"maxLength": 16,
"minLength": 16,
"description": "Network ID to join",
},
"ifname": {
"type": "string",
"title": "Interface name",
"minLength": 1,
"maxLength": 10,
"description": "Name of zerotier interface",
},
},
},
},
"secret": {
"type": "string",
"propertyOrder": 4,
"default": "{{secret}}",
"description": (
"Identity secret of the zerotier client (network member), "
"You can leave it as the default and OpenWISP will automatically determine it"
),
},
# Hidden properties
"config_path": {
"type": "string",
"propertyOrder": 5,
"options": {"hidden": True},
"default": "/etc/openwisp/zerotier",
"description": (
"Path to the persistent configuration "
"directory (for zerotier controller mode)"
),
},
"copy_config_path": {
"type": "string",
"propertyOrder": 6,
"options": {"hidden": True},
"enum": ["0", "1"],
"default": "1",
"description": (
"Specifies whether to copy the configuration "
"file to RAM ('0' - No, '1' - Yes), this prevents "
"writing to flash in zerotier controller mode"
),
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"default": 9993,
"propertyOrder": 7,
"description": "Port number of the zerotier service",
},
"local_conf": {
"type": "string",
"propertyOrder": 8,
"description": (
"Path of the local zerotier configuration "
"(only used for advanced configuration)"
),
},
},
},
},
},
},
)
Expand Down
Empty file.
27 changes: 27 additions & 0 deletions netjsonconfig/backends/zerotier/converters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from ..base.converter import BaseConverter
from .schema import schema


class ZeroTier(BaseConverter):
netjson_key = 'zerotier'
intermediate_key = 'zerotier'
_schema = schema['definitions']['zerotier_server']

def to_intermediate_loop(self, block, result, index=None):
vpn = self.__intermediate_vpn(block)
result.setdefault('zerotier', [])
result['zerotier'].append(vpn)
return result

def __intermediate_vpn(self, config, remove=None):
return self.sorted_dict(config)

def to_netjson_loop(self, block, result, index=None):
vpn = self.__netjson_vpn(block)
result.setdefault('zerotier', [])
result['zerotier'].append(vpn)
return result

def __netjson_vpn(self, vpn):
vpn = self.type_cast(vpn, self._schema)
return vpn
45 changes: 45 additions & 0 deletions netjsonconfig/backends/zerotier/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import re
import tarfile
from json import loads

from ..base.parser import BaseParser

vpn_pattern = re.compile('^// zerotier controller config:\s', flags=re.MULTILINE)
config_pattern = re.compile('^([^\s]*) ?(.*)$')
config_suffix = '.json'


class ZeroTierParser(BaseParser):
def parse_text(self, config):
return {'zerotier': self._get_vpn_config(config)}

def parse_tar(self, tar):
fileobj = tar.buffer if hasattr(tar, 'buffer') else tar
tar = tarfile.open(fileobj=fileobj)
text = ''
for member in tar.getmembers():
if not member.name.endswith(config_suffix):
continue
text += '// zerotier controller config: {name}\n\n{contents}\n'.format(
**{
'name': member.name,
'contents': tar.extractfile(member).read().decode(),
}
)
return self.parse_text(text)

def _get_vpn_config(self, text):
# Remove comments from the vpn text
text = re.sub(r'\/\*(\*(?!\/)|[^*])*\*\/|\/\/.*', '', text)
# Strip leading and trailing whitespace from the text
text = text.strip()
# Split the text into separate VPN instances
# using two or more newline characters as the delimiter
vpn_instances = re.split(r"\n{2,}", text)
# Parse each JSON object separately
vpn_configs = [
loads(vpn_instance)
for vpn_instance in vpn_instances
if vpn_instance.strip()
]
return vpn_configs
13 changes: 13 additions & 0 deletions netjsonconfig/backends/zerotier/renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from ..base.renderer import BaseRenderer


class ZeroTierRenderer(BaseRenderer):
"""
ZeroTier Renderer
"""

def cleanup(self, output):
# remove last newline
if output.endswith('\n\n'):
output = output[0:-1]
return output
Loading

0 comments on commit 571af14

Please sign in to comment.