Skip to content

Commit

Permalink
[feature] Added auth and ip assignment for ZeroTier members
Browse files Browse the repository at this point in the history
  • Loading branch information
Aryamanz29 committed Jul 28, 2023
1 parent 555ac55 commit 7b02aff
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 12 deletions.
12 changes: 6 additions & 6 deletions openwisp_controller/config/api/zerotier_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def _get_endpoint(self, property, operation, id):
def __init__(self, host, token, subnet='', ip=''):
self.host = host
self.token = token
self.subnet = str(subnet)
self.subnet = subnet
self.ip = str(ip)
self.url = f'http://{host}'
self.headers = {
Expand Down Expand Up @@ -49,15 +49,15 @@ def _get_repsonse(self, repsonse):
return repsonse

def _add_routes_and_ip_assignment(self, config):
config['routes'] = [{'target': self.subnet, 'via': ''}]
config['routes'] = [{'target': str(self.subnet), 'via': ''}]
try:
ip_end = self.subnet[-2]
ip_start = self.subnet[1]
ip_end = str(self.subnet[-2])
ip_start = str(self.subnet[1])
# In case of prefix length 32 (ipv4)
# or 128 (ipv6) only single host is available
except IndexError:
ip_end = self.subnet[0]
ip_start = self.subnet[0]
ip_end = str(self.subnet[0])
ip_start = str(self.subnet[0])

config['ipAssignmentPools'] = [{"ipRangeEnd": ip_end, "ipRangeStart": ip_start}]
return config
Expand Down
13 changes: 13 additions & 0 deletions openwisp_controller/config/base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,11 @@ def get_vpn_context(self):
if vpn.subnet:
if vpnclient.ip:
context[vpn_context_keys['ip_address']] = vpnclient.ip.ip_address
if vpnclient.zt_identity_secret:
context[vpn_context_keys['member_id']] = vpnclient.member_id
context[
vpn_context_keys['zt_identity_secret']
] = vpnclient.zt_identity_secret
if 'vni' in vpn_context_keys and vpnclient.vni:
context[vpn_context_keys['vni']] = f'{vpnclient.vni}'
return context
Expand Down Expand Up @@ -676,5 +681,13 @@ def manage_backend_changed(cls, instance_id, old_backend, backend, **kwargs):
old_templates = device_group.templates.filter(backend=old_backend)
config.manage_group_templates(templates, old_templates, not created)

def _check_zt_vpn_client(self):
# TODO: Improve this implementation
vpn_client = self.vpnclient_set.filter(
vpn__backend=app_settings.VPN_BACKENDS[3][0]
).first()
if vpn_client:
vpn_client._update_zt_network_member()


AbstractConfig._meta.get_field('config').blank = True
48 changes: 48 additions & 0 deletions openwisp_controller/config/base/vpn.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
from .. import crypto
from .. import settings as app_settings
from ..api.zerotier_service import ZerotierService
from ..exceptions import ZeroTierIdentityGenerationError
from ..signals import vpn_peers_changed, vpn_server_modified
from ..tasks import (
create_vpn_dh,
trigger_vpn_server_endpoint,
trigger_zerotier_server_delete,
trigger_zerotier_server_join,
trigger_zerotier_server_update,
trigger_zerotier_server_update_member,
)
from .base import BaseConfig

Expand Down Expand Up @@ -552,6 +554,10 @@ def _get_auto_context_keys(self):
context_keys.update({'node_id': 'node_id_{}'.format(pk)})
context_keys.update({'network_id': 'network_id_{}'.format(pk)})
context_keys.update({'network_name': 'network_name_{}'.format(pk)})
context_keys.update({'member_id': 'member_id_{}'.format(pk)})
context_keys.update(
{'zt_identity_secret': 'zt_identity_secret_{}'.format(pk)}
)
return context_keys

def auto_client(self, auto_cert=True, template_backend_class=None):
Expand Down Expand Up @@ -595,6 +601,9 @@ def auto_client(self, auto_cert=True, template_backend_class=None):
name=self.name,
nwid=[self.network_id],
)
# TODO: Add the 'secret' property for ZeroTier in netjsonconfig
identity_key = f'{{{{ zt_identity_secret_{self.pk.hex} }}}}'
auto['zerotier'][0].update({'secret': identity_key})
else:
# The OpenVPN backend does not support these kwargs,
# hence, they are removed before creating configuration
Expand Down Expand Up @@ -760,6 +769,9 @@ class AbstractVpnClient(models.Model):
# needed for wireguard
public_key = models.CharField(blank=True, max_length=44)
private_key = models.CharField(blank=True, max_length=44)
# needed for zerotier
member_id = models.CharField(blank=True, max_length=10)
zt_identity_secret = models.CharField(blank=True, max_length=44)
# needed for vxlan
vni = models.PositiveIntegerField(
null=True,
Expand Down Expand Up @@ -799,6 +811,7 @@ def save(self, *args, **kwargs):
self._auto_ip()
self._auto_wireguard()
self._auto_vxlan()
self._auto_zt_identity_secret()
super().save(*args, **kwargs)

def _auto_x509(self):
Expand Down Expand Up @@ -922,6 +935,41 @@ def _auto_ip(self):
return
self.ip = self.vpn.subnet.request_ip()

def _generate_zt_identity(self):
try:
result = subprocess.run(
'zerotier-idtool generate',
shell=True,
check=True,
# in seconds
timeout=5,
capture_output=True,
)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
err, exit_code = getattr(exc, 'stderr'), getattr(exc, 'returncode', 124)
# In case of timeout
# err: exc msg, code: 124
if err is None:
err = exc
err_msg = (
f'Unable to generate zerotier identity secret, '
f'Error: {err}, Exit code: {exit_code}'
)
raise ZeroTierIdentityGenerationError(err_msg)
return result.stdout.decode('utf-8')

def _auto_zt_identity_secret(self):
if self.vpn._is_backend_type('zerotier'):
self.zt_identity_secret = self._generate_zt_identity()
self.member_id = self.zt_identity_secret[:10]

def _update_zt_network_member(self):
transaction.on_commit(
lambda: trigger_zerotier_server_update_member.delay(
vpn_id=self.vpn.pk, ip=str(self.ip.ip_address), node_id=self.member_id
)
)

@classmethod
def invalidate_clients_cache(cls, vpn):
"""
Expand Down
4 changes: 4 additions & 0 deletions openwisp_controller/config/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ class OrganizationDeviceLimitExceeded(ValidationError):
def __init__(self):
error = {'device_limit': [self.error_message]}
super().__init__(error, code=None, params=None)


class ZeroTierIdentityGenerationError(Exception):
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.19 on 2023-07-27 12:14

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('config', '0050_vpn_node_network_id'),
]

operations = [
migrations.AddField(
model_name='vpnclient',
name='member_id',
field=models.CharField(blank=True, max_length=10),
),
migrations.AddField(
model_name='vpnclient',
name='zt_identity_secret',
field=models.CharField(blank=True, max_length=44),
),
]
14 changes: 8 additions & 6 deletions openwisp_controller/config/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def trigger_zerotier_server_update(self, config, vpn_id):
Vpn = load_model('config', 'Vpn')
vpn = Vpn.objects.get(pk=vpn_id)
service_method = ZerotierService(
vpn.host, vpn.auth_token, vpn.subnet, vpn.ip
vpn.host, vpn.auth_token, vpn.subnet.subnet, vpn.ip.ip_address
).update_network
response, updated_config = self.handle_api_call(
service_method,
Expand Down Expand Up @@ -251,27 +251,29 @@ def trigger_zerotier_server_update(self, config, vpn_id):
autoretry_for=(RequestException,),
**API_TASK_RETRY_OPTIONS,
)
def trigger_zerotier_server_update_member(self, vpn_id):
def trigger_zerotier_server_update_member(self, vpn_id, ip=None, node_id=None):
Vpn = load_model('config', 'Vpn')
vpn = Vpn.objects.get(pk=vpn_id)
ip = ip or vpn.ip
node_id = node_id or vpn.node_id
service_method = ZerotierService(
vpn.host,
vpn.auth_token,
ip=vpn.ip,
ip=ip,
).update_network_member
self.handle_api_call(
service_method,
vpn.node_id,
node_id,
vpn.network_id,
instance=vpn,
action='update_member',
info=(
f'Successfully updated ZeroTier network member: {vpn.node_id}, '
f'Successfully updated ZeroTier network member: {node_id}, '
f'ZeroTier network: {vpn.network_id}, '
f'ZeroTier VPN server UUID: {vpn_id}'
),
err=(
f'Failed to update ZeroTier network member: {vpn.node_id}, '
f'Failed to update ZeroTier network member: {node_id}, '
f'ZeroTier network: {vpn.network_id}, '
f'ZeroTier VPN server UUID: {vpn_id}'
),
Expand Down
4 changes: 4 additions & 0 deletions openwisp_controller/connection/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ def update_config(self):
logger.exception(e)
else:
self.device.config.set_status_applied()
# After successfully applying the configuration
# to the device, run this check for ZeroTier member
# authentication and IP assignment
self.device.config._check_zt_vpn_client()
self.disconnect()

def save(self, *args, **kwargs):
Expand Down
2 changes: 2 additions & 0 deletions tests/openwisp2/sample_config/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,8 @@ class Migration(migrations.Migration):
to='sample_config.vpn',
),
),
('member_id', models.CharField(blank=True, max_length=10)),
('zt_identity_secret', models.CharField(blank=True, max_length=44)),
],
options={
'verbose_name': 'VPN client',
Expand Down

0 comments on commit 7b02aff

Please sign in to comment.