Skip to content

Commit 8dd834e

Browse files
authored
[feature] Added support for ZeroTier
Closes #604
1 parent 13a7965 commit 8dd834e

File tree

15 files changed

+1840
-34
lines changed

15 files changed

+1840
-34
lines changed

README.rst

+131
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,103 @@ OpenWISP. Register or create a device before proceeding.
12801280
**Voila!** You have successfully configured OpenWISP to manage VXLAN over
12811281
WireGuard tunnels for your devices.
12821282

1283+
How to setup ZeroTier Tunnels
1284+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1285+
1286+
Follow the procedure described below to setup ZeroTier tunnels on your devices.
1287+
1288+
**Note:** This example uses **Shared systemwide (no organization)** option as
1289+
the organization for VPN server and VPN client template. You can use any
1290+
organization as long as VPN server, VPN client template and Device has same
1291+
organization.
1292+
1293+
1. Configure Self-Hosted ZeroTier Network Controller
1294+
####################################################
1295+
1296+
If you haven't already set up a self-hosted Zerotier network controller on your server,
1297+
now is a good time to do so. You can start by simply installing Zerotier on your server
1298+
from the `official website <https://www.zerotier.com/download/>`_.
1299+
1300+
2. Create VPN server configuration for ZeroTier
1301+
###############################################
1302+
1303+
1. Visit ``/admin/config/vpn/add/`` to add a new VPN server.
1304+
2. We will set **Name** of this VPN server ``ZeroTier`` and **Host** as
1305+
``my-zerotier-server.mydomain.com:9993`` (update this to point to your ZeroTier VPN server).
1306+
3. Select ``ZeroTier`` from the dropdown as **VPN Backend**.
1307+
4. When using ZeroTier, OpenWISP takes care of managing IP addresses
1308+
(assigning an IP address to each VPN clients (Zerotier network members).
1309+
You can create a new subnet or select an existing one from the dropdown menu.
1310+
You can also assign an **Internal IP** to the Zerotier controller or
1311+
leave it empty for OpenWISP to configure. This IP address will be used
1312+
to assign it to the Zerotier controller running on the server.
1313+
5. Set the **Webhook AuthToken**, this will be ZeroTier authorization token which you
1314+
can obtain by running the following command on the ZeroTier controller:
1315+
1316+
.. code-block:: shell
1317+
1318+
sudo cat /var/lib/zerotier-one/authtoken.secret
1319+
1320+
.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-1.png
1321+
:alt: ZeroTier VPN server configuration example 1
1322+
1323+
.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-2.png
1324+
:alt: ZeroTier VPN server configuration example 2
1325+
1326+
.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-3.png
1327+
:alt: ZeroTier VPN server configuration example 3
1328+
1329+
.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-4.png
1330+
:alt: ZeroTier VPN server configuration example 4
1331+
1332+
6. After clicking on **Save and continue editing**, OpenWISP automatically detects
1333+
the node address of the Zerotier controller and creates a Zerotier network.
1334+
The **network_id** of this network can be viewed in the **System Defined Variables**
1335+
section, where it also provides internal IP address information.
1336+
1337+
.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png
1338+
:alt: ZeroTier VPN server configuration example 5
1339+
1340+
3. Create VPN client template for ZeroTier VPN Server
1341+
#####################################################
1342+
1343+
1. Visit ``/admin/config/template/add/`` to add a new template.
1344+
2. Set ``ZeroTier Client`` as **Name** (you can set whatever you want) and
1345+
select ``VPN-client`` as **type** from the dropdown list.
1346+
3. The **Backend** field refers to the backend of the device this template can
1347+
be applied to. For this example, we will leave it to ``OpenWRT``.
1348+
4. Select the correct VPN server from the dropdown for the **VPN** field. Here
1349+
it is ``ZeroTier``.
1350+
5. Make sure to check the **Automatic tunnel provisioning** option.
1351+
This will enable OpenWISP to automatically provision an IP address
1352+
for each ZeroTier VPN client.
1353+
6. After clicking on **Save and continue editing** button, you will see details
1354+
of *ZeroTier* VPN server in **System Defined Variables**. The template
1355+
configuration will be automatically generated which you can tweak
1356+
accordingly. We will use the automatically generated VPN client configuration
1357+
for this example.
1358+
1359+
.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png
1360+
:alt: ZeroTier VPN client template example
1361+
1362+
4. Apply ZeroTier VPN template to devices
1363+
#########################################
1364+
1365+
**Note**: This step assumes that you already have a device registered on
1366+
OpenWISP. Register or create a device before proceeding.
1367+
1368+
1. Open the **Configuration** tab of the concerned device.
1369+
2. Select the *ZeroTier Client* template.
1370+
3. Upon clicking on **Save and continue editing** button, you will see some
1371+
entries in **System Defined Variables**. It will contain internal IP address
1372+
for the ZeroTier client on the device along with details of VPN server.
1373+
1374+
.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration.png
1375+
:alt: ZeroTier VPN device configuration example
1376+
1377+
**Voila!** You have successfully configured OpenWISP
1378+
to manage ZeroTier tunnels for your devices.
1379+
12831380
How to configure automatic provisioning of subnets and IPs
12841381
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12851382

@@ -2709,6 +2806,7 @@ Available configuration backends. For more information, see `netjsonconfig backe
27092806
| | ('openwisp_controller.vpn_backends.OpenVpn', 'OpenVPN'), |
27102807
| | ('openwisp_controller.vpn_backends.Wireguard', 'WireGuard'), |
27112808
| | ('openwisp_controller.vpn_backends.VxlanWireguard', 'VXLAN over WireGuard'), |
2809+
| | ('openwisp_controller.vpn_backends.ZeroTier', 'ZeroTier'), |
27122810
| | ) |
27132811
+--------------+----------------------------------------------------------------------------------+
27142812

@@ -3230,6 +3328,39 @@ Allows to show a pie chart like the one in the screenshot.
32303328
Active groups are groups which have at least one device in them,
32313329
while emtpy groups do not have any device assigned.
32323330

3331+
``OPENWISP_CONTROLLER_API_TASK_RETRY_OPTIONS``
3332+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3333+
3334+
+--------------+-----------+
3335+
| **type**: | ``dict`` |
3336+
+--------------+-----------+
3337+
| **default**: | see below |
3338+
+--------------+-----------+
3339+
3340+
.. code-block:: python
3341+
3342+
# default value of OPENWISP_CONTROLLER_API_TASK_RETRY_OPTIONS:
3343+
3344+
dict(
3345+
max_retries=5, # total number of retries
3346+
retry_backoff=True, # exponential backoff
3347+
retry_backoff_max=600, # 10 minutes
3348+
retry_jitter=True, # randomness into exponential backoff
3349+
)
3350+
3351+
3352+
This setting is utilized by background API tasks executed
3353+
by `ZeroTier VPN servers and ZeroTier VPN clients <#how-to-setup-zerotier-tunnels>`_ to handle recoverable
3354+
HTTP status codes such as 429, 500, 502, 503, and 504. These tasks are retried with a maximum
3355+
of 5 attempts with an exponential backoff and jitter, with a maximum delay of 10 minutes.
3356+
3357+
This feature ensures that ZeroTier Service API calls
3358+
are resilient to recoverable failures, improving the reliability of the system.
3359+
3360+
For more information on these settings, you can refer to the `the celery documentation regarding automatic retries
3361+
for known errors. <https://docs.celeryq.dev/en/stable/userguide/tasks.html#automatic-retry-for-known-exceptions>`_
3362+
3363+
32333364
Signals
32343365
-------
32353366

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import requests
2+
from django.core.exceptions import ValidationError
3+
from django.utils.translation import gettext_lazy as _
4+
from requests.exceptions import ConnectionError, RequestException, Timeout
5+
6+
REQUEST_TIMEOUT = 5
7+
8+
9+
class ZerotierService:
10+
def _get_endpoint(self, property, operation, id):
11+
_API_ENDPOINTS = {
12+
'network': {
13+
'create': f'/controller/network/{id}______',
14+
'get': f'/controller/network/{id}',
15+
'update': f'/controller/network/{id}',
16+
'delete': f'/controller/network/{id}',
17+
}
18+
}
19+
return _API_ENDPOINTS.get(property).get(operation)
20+
21+
def __init__(self, host, token, subnet=''):
22+
self.host = host
23+
self.token = token
24+
self.subnet = subnet
25+
self.url = f'http://{host}'
26+
self.headers = {
27+
'X-ZT1-Auth': self.token,
28+
'Accept': 'application/json',
29+
'Content-Type': 'application/json',
30+
}
31+
32+
def _get_repsonse(self, repsonse):
33+
# remove redundant fields from the response
34+
_redundant_fields = [
35+
'authTokens',
36+
'authorizationEndpoint',
37+
'clientId',
38+
'rulesSource',
39+
'ssoEnabled',
40+
'creationTime',
41+
'name',
42+
'nwid',
43+
'objtype',
44+
'revision',
45+
'routes',
46+
'ipAssignmentPools',
47+
]
48+
for field in _redundant_fields:
49+
if field in repsonse.keys():
50+
del repsonse[field]
51+
return repsonse
52+
53+
def _add_routes_and_ip_assignment(self, config):
54+
config['routes'] = [{'target': str(self.subnet), 'via': ''}]
55+
ip_end = str(self.subnet.broadcast_address)
56+
ip_start = str(next(self.subnet.hosts()))
57+
config['ipAssignmentPools'] = [{"ipRangeEnd": ip_end, "ipRangeStart": ip_start}]
58+
return config
59+
60+
def join_network(self, network_id):
61+
url = f'{self.url}/network/{network_id}'
62+
response = requests.post(
63+
url, json={}, headers=self.headers, timeout=REQUEST_TIMEOUT
64+
)
65+
return response
66+
67+
def leave_network(self, network_id):
68+
url = f'{self.url}/network/{network_id}'
69+
response = requests.delete(url, headers=self.headers, timeout=REQUEST_TIMEOUT)
70+
return response
71+
72+
def update_network_member(self, node_id, network_id, member_ip):
73+
url = f'{self.url}/controller/network/{network_id}/member/{node_id}'
74+
# Authorize and assign ip to the network member
75+
response = requests.post(
76+
url,
77+
json={'authorized': True, 'ipAssignments': [str(member_ip)]},
78+
headers=self.headers,
79+
timeout=5,
80+
)
81+
return response
82+
83+
def get_node_status(self):
84+
url = f'{self.url}/status'
85+
try:
86+
response = requests.get(url, headers=self.headers, timeout=REQUEST_TIMEOUT)
87+
return response
88+
except (Timeout, ConnectionError) as e:
89+
raise ValidationError(
90+
{
91+
'host': _(
92+
'Failed to connect to the ZeroTier controller, Error: {0}'
93+
).format(e)
94+
}
95+
)
96+
97+
def create_network(self, node_id, config):
98+
url = f"{self.url}{self._get_endpoint('network', 'create', node_id)}"
99+
config = self._add_routes_and_ip_assignment(config)
100+
try:
101+
response = requests.post(
102+
url, json=config, headers=self.headers, timeout=REQUEST_TIMEOUT
103+
)
104+
response.raise_for_status()
105+
network_config = self._get_repsonse(response.json())
106+
return network_config
107+
except RequestException as e:
108+
raise ValidationError(
109+
_('Failed to create ZeroTier network, Error: {0}').format(e)
110+
)
111+
112+
def update_network(self, config, network_id):
113+
url = f"{self.url}{self._get_endpoint('network', 'update', network_id)}"
114+
config = self._add_routes_and_ip_assignment(config)
115+
response = requests.post(
116+
url, json=config, headers=self.headers, timeout=REQUEST_TIMEOUT
117+
)
118+
return response, self._get_repsonse(response.json())
119+
120+
def delete_network(self, network_id):
121+
url = f"{self.url}{self._get_endpoint('network', 'delete', network_id)}"
122+
response = requests.delete(url, headers=self.headers, timeout=REQUEST_TIMEOUT)
123+
return response

openwisp_controller/config/apps.py

+45
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ def connect_signals(self):
102102
sender=self.vpn_model,
103103
dispatch_uid='vpn.update_vpn_server_configuration',
104104
)
105+
post_delete.connect(
106+
self.vpn_model.post_delete,
107+
sender=self.vpn_model,
108+
dispatch_uid='vpn.post_delete',
109+
)
105110
post_save.connect(
106111
self.config_model.certificate_updated,
107112
sender=self.cert_model,
@@ -209,6 +214,46 @@ def register_notification_types(self):
209214
models=[self.device_model],
210215
)
211216

217+
register_notification_type(
218+
'api_task_error',
219+
{
220+
'verbose_name': _('Background API Task ERROR'),
221+
'verb': _('encountered an unrecoverable error'),
222+
'level': 'error',
223+
'email_subject': _(
224+
'[{site.name}] ERROR: "{notification.target}" - '
225+
'VPN Server {action} API Task {notification.verb}'
226+
),
227+
'message': _(
228+
'Unable to perform {action} operation on the '
229+
'[{notification.target}]({notification.target_link}) VPN server '
230+
'due to an unrecoverable error (status code: {status_code})'
231+
),
232+
# Disable email notifications by default
233+
'email_notification': False,
234+
},
235+
models=[self.vpn_model],
236+
)
237+
register_notification_type(
238+
'api_task_recovery',
239+
{
240+
'verbose_name': _('Background API Task RECOVERY'),
241+
'verb': _('has been completed successfully'),
242+
'level': 'info',
243+
'email_subject': _(
244+
'[{site.name}] RECOVERY: "{notification.target}" - '
245+
'VPN Server {action} API Task {notification.verb}'
246+
),
247+
'message': _(
248+
'The {action} operation on [{notification.target}]'
249+
'({notification.target_link}) {notification.verb}'
250+
),
251+
# Disable email notifications by default
252+
'email_notification': False,
253+
},
254+
models=[self.vpn_model],
255+
)
256+
212257
# Unregister default notification type
213258
try:
214259
unregister_notification_type('default')

0 commit comments

Comments
 (0)