From cd19c47fdb967f5c7e286f5c657a5ae63912de77 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sun, 13 Dec 2020 17:59:35 +0100 Subject: [PATCH] [vpn] implement creating vpns --- uncloud_net/migrations/0001_initial.py | 14 ++++++- uncloud_net/models.py | 49 +++++++++++++++++++++++- uncloud_net/selectors.py | 18 ++++++++- uncloud_net/serializers.py | 20 ++++------ uncloud_net/services.py | 52 ++++++++++++++++---------- uncloud_net/views.py | 32 +++++++--------- 6 files changed, 130 insertions(+), 55 deletions(-) diff --git a/uncloud_net/migrations/0001_initial.py b/uncloud_net/migrations/0001_initial.py index 36ba522..6794156 100644 --- a/uncloud_net/migrations/0001_initial.py +++ b/uncloud_net/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1 on 2020-12-13 10:38 +# Generated by Django 3.1 on 2020-12-13 13:42 from django.conf import settings import django.core.validators @@ -32,11 +32,21 @@ class Migration(migrations.Migration): ('wireguard_private_key', models.CharField(max_length=48)), ], ), + migrations.CreateModel( + name='WireGuardVPNFreeLeases', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('pool_index', models.IntegerField(unique=True)), + ('vpnpool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_net.wireguardvpnpool')), + ], + ), migrations.CreateModel( name='WireGuardVPN', fields=[ - ('address', models.GenericIPAddressField(primary_key=True, serialize=False)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('pool_index', models.IntegerField(unique=True)), ('wireguard_public_key', models.CharField(max_length=48)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('vpnpool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_net.wireguardvpnpool')), ], ), diff --git a/uncloud_net/models.py b/uncloud_net/models.py index 2f573bd..1b69a9e 100644 --- a/uncloud_net/models.py +++ b/uncloud_net/models.py @@ -25,6 +25,24 @@ class WireGuardVPNPool(models.Model): vpn_server_hostname = models.CharField(max_length=256) wireguard_private_key = models.CharField(max_length=48) + @property + def max_pool_index(self): + """ + Return the highest possible network / last network id + """ + + bits = self.subnetwork_mask - self.network_mask + + return (2**bits)-1 + + @property + def ip_network(self): + return ipaddress.ip_network(f"{self.network}/{self.network_mask}") + + def __str__(self): + return f"{self.ip_network} (subnets: /{self.subnetwork_mask})" + + class WireGuardVPN(models.Model): """ Created VPNNetworks @@ -34,10 +52,39 @@ class WireGuardVPN(models.Model): vpnpool = models.ForeignKey(WireGuardVPNPool, on_delete=models.CASCADE) - address = models.GenericIPAddressField(primary_key=True) + pool_index = models.IntegerField(unique=True) wireguard_public_key = models.CharField(max_length=48) + @property + def network_mask(self): + return self.vpnpool.subnetwork_mask + + @property + def address(self): + """ + Locate the correct subnet in the supernet + + First get the network itself + + """ + + net = self.vpnpool.ip_network + subnet = net[(2**(128-self.vpnpool.subnetwork_mask)) * self.pool_index] + + return str(subnet) + + def __str__(self): + return f"{self.address} ({self.pool_index})" + +class WireGuardVPNFreeLeases(models.Model): + """ + Previously used VPNNetworks + """ + vpnpool = models.ForeignKey(WireGuardVPNPool, + on_delete=models.CASCADE) + + pool_index = models.IntegerField(unique=True) ################################################################################ diff --git a/uncloud_net/selectors.py b/uncloud_net/selectors.py index 70fafd2..bcb1ea8 100644 --- a/uncloud_net/selectors.py +++ b/uncloud_net/selectors.py @@ -4,7 +4,10 @@ from django.db.models import Count, F from .models import * -def get_suitable_pool(subnetwork_mask): +# def get_num_used_networks(pool): +# return pool.wireguardvpn_set.count() + +def get_suitable_pools(subnetwork_mask): """ Find suitable pools for a certain network size. @@ -42,3 +45,16 @@ def allowed_vpn_network_reservation_size(): # Need to return set of tuples, see # https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-choices return set([ (pool.subnetwork_mask, pool.subnetwork_mask) for pool in pools ]) + + +#def get_next_vpnnetwork(pool): + # get all associated networks + # look for the lowest free number + # return that + + + # select last used one + # try to increment by one -> get new network + + # if that fails search through the existing vpns for the first unused number + # diff --git a/uncloud_net/serializers.py b/uncloud_net/serializers.py index 8c6c567..46d2344 100644 --- a/uncloud_net/serializers.py +++ b/uncloud_net/serializers.py @@ -8,23 +8,17 @@ from .models import * from .services import * class WireGuardVPNSerializer(serializers.ModelSerializer): + address = serializers.CharField(read_only=True) + network_mask = serializers.IntegerField() + class Meta: model = WireGuardVPN - fields = [ 'wireguard_public_key' ] + fields = [ 'wireguard_public_key', 'address', 'network_mask' ] read_only_fields = [ 'address ' ] - def create(self, validated_data): - pass - -# class WireGuardVPNPoolSerializer(serializers.ModelSerializer): -# class Meta: -# model = WireGuardVPNPool -# fields = '__all__' - -# class WireGuardVPNSerializer(serializers.ModelSerializer): -# class Meta: -# model = VPNNetworkReservation -# fields = '__all__' + extra_kwargs = { + 'network_mask': {'write_only': True } + } # class VPNNetworkSerializer(serializers.ModelSerializer): diff --git a/uncloud_net/services.py b/uncloud_net/services.py index 45e14c9..ec7a266 100644 --- a/uncloud_net/services.py +++ b/uncloud_net/services.py @@ -4,32 +4,46 @@ from .models import * from .selectors import * @transaction.atomic -def create_wireguard_vpn(*, - public_key: str, - network_mask: int - ) -> WireGuardVPN: +def create_wireguard_vpn(owner, public_key, network_mask): - pool = get_suitable_pool(network_mask)[0] + pool = get_suitable_pools(network_mask)[0] + count = pool.wireguardvpn_set.count() - # FIXME: exception - which? - if not pools: - return None + # First object + if count == 0: + return WireGuardVPN.objects.create(owner=owner, + vpnpool=pool, + pool_index=0, + wireguard_public_key=public_key) - # last_net = ipaddress.ip_network(self.used_networks.last().address) - # last_net_ip = last_net[0] + else: # Select last network and try +1 it + last_net = WireGuardVPN.objects.filter(vpnpool=pool).order_by('pool_index').last() - # if last_net_ip.version == 6: - # offset_to_next = 2**(128 - self.subnetwork_size) - # elif last_net_ip.version == 4: - # offset_to_next = 2**(32 - self.subnetwork_size) + next_index = last_net.pool_index + 1 - # next_net_ip = last_net_ip + offset_to_next + if next_index <= pool.max_pool_index: + return WireGuardVPN.objects.create(owner=owner, + vpnpool=pool, + pool_index=next_index, + wireguard_public_key=public_key) - # return str(next_net_ip) - # else: - # # first network to be created - # return self.network + + # Still there? Then we need to lookup previously used networks + try: + free_lease = WireGuardVPNFreeLeases.objects.get(vpnpool=pool) + + vpn = WireGuardVPN.objects.create(owner=owner, + vpnpool=pool, + pool_index=free_lease.pool_index, + wireguard_public_key=public_key) + + free_lease.delete() + + return vpn + + except WireGuardVPNFreeLeases.DoesNotExist: + pass @property def wireguard_config_filename(self): diff --git a/uncloud_net/views.py b/uncloud_net/views.py index dee3fac..f91ff7c 100644 --- a/uncloud_net/views.py +++ b/uncloud_net/views.py @@ -1,15 +1,16 @@ from django.views.generic.edit import CreateView from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin +from rest_framework.response import Response from django.shortcuts import render from rest_framework import viewsets, permissions - from .models import * from .serializers import * from .selectors import * +from .services import * from .forms import * # class VPNPoolViewSet(viewsets.ModelViewSet): @@ -17,12 +18,6 @@ from .forms import * # permission_classes = [permissions.IsAdminUser] # queryset = VPNPool.objects.all() -# class VPNNetworkReservationViewSet(viewsets.ModelViewSet): -# serializer_class = VPNNetworkReservationSerializer -# permission_classes = [permissions.IsAdminUser] -# queryset = VPNNetworkReservation.objects.all() - - class WireGuardVPNViewSet(viewsets.ModelViewSet): serializer_class = WireGuardVPNSerializer permission_classes = [permissions.IsAuthenticated] @@ -35,6 +30,17 @@ class WireGuardVPNViewSet(viewsets.ModelViewSet): return obj + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + vpn = create_wireguard_vpn( + owner=self.request.user, + public_key=serializer.validated_data['wireguard_public_key'], + network_mask=serializer.validated_data['network_mask'] + ) + return Response(WireGuardVPNSerializer(vpn).data) + class WireGuardVPNCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = WireGuardVPN @@ -48,15 +54,3 @@ class WireGuardVPNCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView def get_success_message(self, cleaned_data): return self.success_message % dict(cleaned_data, the_prefix = self.object.prefix) - - # def get_context_data(self, **kwargs): - # context = super().get_context_data(**kwargs) - # context['available_sizes'] = 2 - # return context - - # def post(request, *args, **kwargs): - # print(request) - # print(*args) - # print(*kwargs) - - # def post(self, request, *args, **kwargs):