Skip to content

Commit

Permalink
[feature] Added support for ZeroTier to the OpenWRT backend #207
Browse files Browse the repository at this point in the history
The schema is taken from OpenAPI specification:

https://docs.zerotier.com/service/v1/ (self-hosted controllers)
https://docs.zerotier.com/openapi/centralv1.json (central controllers)

The Zerotier network configuration keys taken from:

https://github.com/zerotier/ZeroTierOne/blob/dev/node/NetworkConfig.hpp

Closes #208

Co-authored-by: Federico Capoano <f.capoano@openwisp.io>
  • Loading branch information
Aryamanz29 and nemesifier committed Jul 31, 2023
1 parent 06234ef commit 2bae0da
Show file tree
Hide file tree
Showing 12 changed files with 570 additions and 232 deletions.
95 changes: 91 additions & 4 deletions docs/source/backends/zerotier.rst
Expand Up @@ -206,9 +206,21 @@ Required properties:
| | | | +----------------------+---------+---------------------------------------------------------------+ |
| | | | | key name | type | description | |
| | | | +======================+=========+===============================================================+ |
| | | | | ``6plane`` | boolean | whether 6PLANE addressing should be used for IPv6 assignment | |
| | | | | ``6plane`` | boolean | 6PLANE assigns each device a single IPv6 address from a | |
| | | | | | | | |
| | | | | | | fully routable /80 block. It utilizes NDP emulation to route | |
| | | | | | | | |
| | | | | | | the entire /80 to the device owner, enabling up to 2^48 IPs | |
| | | | | | | | |
| | | | | | | without additional configuration. Ideal for Docker or VM hosts| |
| | | | +----------------------+---------+---------------------------------------------------------------+ |
| | | | | ``rfc4193`` | boolean | whether RFC4193 addressing should be used for IPv6 assignment | |
| | | | | ``rfc4193`` | boolean | RFC4193 assigns each device a single IPv6 /128 address | |
| | | | | | | | |
| | | | | | | computed from the network ID and device address and uses NDP | |
| | | | | | | | |
| | | | | | | emulation to make these addresses instantly resolvable without| |
| | | | | | | | |
| | | | | | | multicast | |
| | | | +----------------------+---------+---------------------------------------------------------------+ |
| | | | | ``zt`` | boolean | whether ZeroTier should assign IPv6 addresses to members | |
| | | | +----------------------+---------+---------------------------------------------------------------+ |
Expand Down Expand Up @@ -257,6 +269,35 @@ Required properties:
| ``remoteTraceLevel`` | integer | | level of network tracing |
+------------------------+---------+--------------+----------------------------------------------------------------------------------------------------+

Client specific settings
~~~~~~~~~~~~~~~~~~~~~~~~

Required properties:

* id
* name

+------------------------+---------+--------------+----------------------------------------------------------------------------------------------------+
| key name | type | default | description |
+========================+=========+==============+====================================================================================================+
| ``name`` | string | | name of the zerotier network |
+------------------------+---------+--------------+----------------------------------------------------------------------------------------------------+
| ``id`` | list | ``[]`` | list of strings containing **16-digit** hexadecimal network IDs to join |
| | | | |
| | | | **note:** must contain at least one network ID |
+------------------------+---------+--------------+----------------------------------------------------------------------------------------------------+
| ``config_path`` | string | | path to the persistent configuration folder |
+------------------------+---------+--------------+----------------------------------------------------------------------------------------------------+
| ``copy_config_path`` | string | ``'0'`` | specifies whether to copy the configuration file to RAM |
| | | | |
| | | | ``'0'`` - No, ``'1'`` - Yes, this prevents writing to flash in zerotier controller mode |
+------------------------+---------+--------------+----------------------------------------------------------------------------------------------------+
| ``port`` | integer | ``9993`` | port number of the zerotier service |
+------------------------+---------+--------------+----------------------------------------------------------------------------------------------------+
| ``local_conf`` | boolean | | path of the local zerotier configuration |
+------------------------+---------+--------------+----------------------------------------------------------------------------------------------------+
| ``secret`` | boolean | | secret key of the zerotier client (network member), leave it blank to be automatically determined |
+------------------------+---------+--------------+----------------------------------------------------------------------------------------------------+

Working around schema limitations
---------------------------------
Expand All @@ -269,9 +310,55 @@ any property not included in the schema as long as its type is one the following
* strings
* lists

For a list of all ZeroTier network configuration settings, refer to the following OpenAPI API specifications:
Automatic generation of clients
-------------------------------

.. automethod:: netjsonconfig.OpenWrt.zerotier_auto_client

Example:

.. code-block:: python
from netjsonconfig import OpenWrt
server_config = {
"id": ["9536600adf654321"],
"name": "zerotier-openwisp-network",
}
client_config = OpenWrt.zerotier_auto_client(
nwid=server_config['id'], name=server_config['name']
)
print(OpenWrt(client_config).render())
Will be rendered as:

.. code-block:: text
package zerotier
config zerotier 'zerotier_openwisp_network'
option enabled '1'
list join '9536600adf654321'
.. note::

The current implementation of **ZeroTier VPN** backend is implemented with
**OpenWrt** backend. Hence, the example above shows configuration generated for
OpenWrt.


Useful resources
----------------

The default flow rules used in `zerotier/schema.py
<https://github.com/openwisp/netjsonconfig/blob/master/netjsonconfig/backends/zerotier/schema.py>`_
for the ZeroTier self-hosted controller are taken from the flow rules mentioned in the documentation below.

- `ZeroTier Controller Network Flow Rules <https://docs.zerotier.com/zerotier/rules/>`_

To explore a comprehensive list of all available ZeroTier network
configuration settings, please refer to the following OpenAPI API specifications.

- `ZeroTier Service (schema: ControllerNetwork) <https://docs.zerotier.com/openapi/servicev1.json>`_

- `ZeroTier Central (schema: NetworkConfig) <https://docs.zerotier.com/openapi/centralv1.json>`_

2 changes: 2 additions & 0 deletions netjsonconfig/backends/openwrt/converters/__init__.py
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
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
27 changes: 27 additions & 0 deletions netjsonconfig/backends/openwrt/converters/zerotier.py
@@ -0,0 +1,27 @@
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):
vpn.update(
{
'.name': self._get_uci_name(vpn.pop('name')),
'.type': 'zerotier',
'join': vpn.pop('id'),
'enabled': not vpn.pop('disabled', False),
}
)
return super().__intermediate_vpn(vpn, remove=[''])

def __netjson_vpn(self, vpn):
vpn['id'] = vpn.pop('join')
vpn['name'] = vpn.pop('.name').replace('_', '-')
# 'disabled' defaults to False in OpenWRT
vpn['disabled'] = vpn.pop('enabled', '0') == '0'
del vpn['.type']
return super().__netjson_vpn(vpn)
7 changes: 7 additions & 0 deletions netjsonconfig/backends/openwrt/openwrt.py
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
81 changes: 81 additions & 0 deletions netjsonconfig/backends/openwrt/schema.py
Expand Up @@ -1001,6 +1001,87 @@
},
},
},
"zerotier": {
"type": "array",
"title": "ZeroTier Networks",
"uniqueItems": True,
"propertyOrder": 14,
"items": {
"type": "object",
"title": "Network Member",
"additionalProperties": True,
"required": ["id", "name"],
"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": "openwisp_zerotier_network",
"description": "Name of the zerotier network",
},
"id": {
"type": "array",
"title": "16-digit hexadecimal network IDs to join",
"minItems": 1,
"propertyOrder": 3,
"uniqueItems": True,
"items": {
"type": "string",
"title": "Network ID",
"maxLength": 16,
"minLength": 16,
},
},
"config_path": {
"type": "string",
"propertyOrder": 4,
"description": (
"Path to the persistent configuration "
"folder (for zerotier controller mode)"
),
},
"copy_config_path": {
"type": "string",
"propertyOrder": 5,
"enum": ["0", "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": 6,
"description": "Port number of the zerotier service",
},
"local_conf": {
"type": "string",
"propertyOrder": 7,
"description": "Path of the local zerotier configuration",
},
"secret": {
"type": "string",
"propertyOrder": 8,
"description": (
"Secret key of the zerotier client (network member), "
"leave it blank to be automatically determined"
),
},
},
},
},
},
},
)
Expand Down
11 changes: 8 additions & 3 deletions netjsonconfig/backends/zerotier/converters.py
Expand Up @@ -5,7 +5,7 @@
class ZeroTier(BaseConverter):
netjson_key = 'zerotier'
intermediate_key = 'zerotier'
_schema = schema
_schema = schema['definitions']['zerotier_server']

def to_intermediate_loop(self, block, result, index=None):
vpn = self.__intermediate_vpn(block)
Expand All @@ -16,7 +16,12 @@ def to_intermediate_loop(self, block, result, index=None):
def __intermediate_vpn(self, config, remove=None):
return self.sorted_dict(config)

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

def __netjson_vpn(self, vpn):
vpn = self.type_cast(vpn, self._schema)
return vpn

0 comments on commit 2bae0da

Please sign in to comment.