[vpn] make a vpn creat-able!

[15:40] line:~% http -a nicoschottelius:$(pass ungleich.ch/nico.schottelius@ungleich.ch)  http://localhost:8000/net/vpn/ network_size=48  wireguard_public_key=$(wg genkey | wg pubkey)
HTTP/1.1 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 206
Content-Type: application/json
Date: Sun, 12 Apr 2020 13:40:26 GMT
Server: WSGIServer/0.2 CPython/3.7.3
Vary: Accept
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "extra_data": null,
    "network": "2a0a:e5c1:203::",
    "order": null,
    "owner": 30,
    "status": "PENDING",
    "uuid": "8f977a8f-e06a-4346-94ae-8f525df58b7b",
    "wireguard_public_key": "JvCuUTZHm9unasJkGsLKN0Bf/hu6ZSIv7dnIGPyJ6xA="
}
This commit is contained in:
Nico Schottelius 2020-04-12 15:40:39 +02:00
parent b55254b9b1
commit 85b4d70592
4 changed files with 100 additions and 50 deletions

View file

@ -23,3 +23,12 @@ http -a nicoschottelius:$(pass
vpn_hostname=vpn-2a0ae5c1200.ungleich.ch vpn_hostname=vpn-2a0ae5c1200.ungleich.ch
wireguard_private_key=... wireguard_private_key=...
``` ```
* Example http commands / REST calls
** creating a new vpn pool
http -a nicoschottelius:$(pass
ungleich.ch/nico.schottelius@ungleich.ch)
http://localhost:8000/admin/vpnpool/ network_size=40
subnetwork_size=48 network=2a0a:e5c1:200::
vpn_hostname=vpn-2a0ae5c1200.ungleich.ch wireguard_private_key=$(wg
genkey)
** Creating a new vpn network

View file

@ -12,11 +12,6 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
wireguard_template="""
[Interface]
ListenPort = 51820
PrivateKey = {privatekey}
"""
peer_template=""" peer_template="""
# {username} # {username}
@ -44,21 +39,6 @@ class Command(BaseCommand):
configs = [] configs = []
for pool in VPNPool.objects.filter(vpn_hostname=hostname): for pool in VPNPool.objects.filter(vpn_hostname=hostname):
pool_config = {
'private_key': pool.wireguard_private_key,
'subnetwork_size': pool.subnetwork_size,
'config_file': '/etc/wireguard/{}.conf'.format(pool.network),
'peers': []
}
for vpnnetwork in VPNNetworkReservation.objects.filter(vpnpool=pool):
pool_config['peers'].append({
'vpnnetwork': "{}/{}".format(vpnnetwork.address,
pool_config['subnetwork_size']),
'public_key': vpnnetwork.wireguard_public_key,
}
)
configs.append(pool_config) configs.append(pool_config)
print(configs) print(configs)

View file

@ -47,11 +47,15 @@ class VPNPool(UncloudModel):
maximum_networks = 2^(24-8) maximum_networks = 2^(24-8)
""" """
return 2**(subnetwork_size - network_size) return 2**(self.subnetwork_size - self.network_size)
@property @property
def used_networks(self): def used_networks(self):
return self.vpnnetworkreservation_set.objects.filter(vpnpool=self, status='used') return self.vpnnetworkreservation_set.filter(vpnpool=self, status='used')
@property
def free_networks(self):
return self.vpnnetworkreservation_set.filter(vpnpool=self, status='free')
@property @property
def num_used_networks(self): def num_used_networks(self):
@ -59,27 +63,18 @@ class VPNPool(UncloudModel):
@property @property
def num_free_networks(self): def num_free_networks(self):
return self.num_maximum_networks - self.num_used_networks return self.num_maximum_networks - self.num_used_networks + len(self.free_networks)
@property @property
def next_free_network(self): def next_free_network(self):
free_net = self.vpnnetworkreservation_set.objects.filter(vpnpool=self, if self.num_free_networks == 0:
status='free') # FIXME: use right exception
used_net = self.vpnnetworkreservation_set.objects.filter(vpnpool=self,
status='used')
this_net = ipaddress.ip_network("{}/{}".format(
self.network, self.network_size))
if num_free_networks == 0:
raise Exception("No free networks") raise Exception("No free networks")
if len(free_net) > 0: if len(self.free_networks) > 0:
return free_net[0].address return self.free_networks[0].address
if len(used_net) > 0: if len(self.used_networks) > 0:
""" """
sample: sample:
@ -89,7 +84,7 @@ class VPNPool(UncloudModel):
next: next:
""" """
last_net = ipaddress.ip_network(self.used_networks.last()) last_net = ipaddress.ip_network(self.used_networks.last().address)
last_net_ip = last_net[0] last_net_ip = last_net[0]
if last_net_ip.version == 6: if last_net_ip.version == 6:
@ -99,7 +94,52 @@ class VPNPool(UncloudModel):
next_net_ip = last_net_ip + offset_to_next next_net_ip = last_net_ip + offset_to_next
return next_net_ip return str(next_net_ip)
else:
# first network to be created
return self.network
@property
def wireguard_config_filename(self):
return '/etc/wireguard/{}.conf'.format(self.network)
@property
def wireguard_config(self):
wireguard_config = [
"""
[Interface]
ListenPort = 51820
PrivateKey = {privatekey}
""".format(privatekey=self.wireguard_private_key) ]
peers = []
for vpnnetwork in self.vpnnetworkreservation_set:
public_key = vpnnetwork.wireguard_public_key
peer_network = "{}/{}".format(vpnnetwork.address, self.subnetwork_size)
peers.append("""
[Peer]
PublicKey = {public_key}
AllowedIPs = {peer_network}
""")
wireguard_config.extend(peers)
return "\n".join(wireguard_config)
def configure_wireguard_vpnserver(self):
"""
This method is designed to run as a celery task and should
not be called directly from the web
"""
# subprocess, ssh
pass
@ -109,6 +149,7 @@ class VPNNetworkReservation(UncloudModel):
""" """
vpnpool = models.ForeignKey(VPNPool, vpnpool = models.ForeignKey(VPNPool,
on_delete=models.CASCADE) on_delete=models.CASCADE)
address = models.GenericIPAddressField(primary_key=True) address = models.GenericIPAddressField(primary_key=True)
status = models.CharField(max_length=256, status = models.CharField(max_length=256,

View file

@ -19,7 +19,8 @@ class VPNNetworkSerializer(serializers.ModelSerializer):
# This is required for finding the VPN pool, but does not # This is required for finding the VPN pool, but does not
# exist in the model # exist in the model
network_size = serializers.IntegerField(min_value=0, network_size = serializers.IntegerField(min_value=0,
max_value=128) max_value=128,
write_only=True)
def validate_wireguard_public_key(self, value): def validate_wireguard_public_key(self, value):
msg = _("Supplied key is not a valid wireguard public key") msg = _("Supplied key is not a valid wireguard public key")
@ -58,18 +59,37 @@ class VPNNetworkSerializer(serializers.ModelSerializer):
""" """
Creating a new vpnnetwork - there are a couple of race conditions, Creating a new vpnnetwork - there are a couple of race conditions,
especially when run in parallel. especially when run in parallel.
"""
pools = VPNPool.objects.filter(subnetwork_size=data['network_size'])
found_pool = False What we should be doing:
- create a reservation race free
- map the reservation to a network (?)
"""
pools = VPNPool.objects.filter(subnetwork_size=validated_data['network_size'])
vpn_network = None
for pool in pools: for pool in pools:
if pool.num_free_networks > 0: if pool.num_free_networks > 0:
found_pool = True next_address = pool.next_free_network
# address = pool.
# reservation = VPNNetworkReservation(vpnpool=pool, reservation, created = VPNNetworkReservation.objects.update_or_create(
vpnpool=pool, address=next_address,
defaults = {
'status': 'used'
})
vpn_network = VPNNetwork.objects.create(
owner=self.context['request'].user,
network=reservation,
wireguard_public_key=validated_data['wireguard_public_key']
)
break
if not vpn_network:
# FIXME: use correct exception
raise Exception("Did not find any free pool")
pool = VPNPool.objects.first(subnetwork_size=data['network_size']) return vpn_network
return VPNNetwork(**validated_data)