Initial commit

This commit is contained in:
Eduardo Silva 2024-02-14 16:36:01 -03:00
commit ed55724808
2505 changed files with 1136655 additions and 0 deletions

0
wireguard/__init__.py Normal file
View file

23
wireguard/admin.py Normal file
View file

@ -0,0 +1,23 @@
from django.contrib import admin
from .models import WireGuardInstance, Peer, PeerAllowedIP
class WireGuardInstanceAdmin(admin.ModelAdmin):
list_display = ('name', 'instance_id', 'private_key', 'hostname', 'listen_port', 'address', 'netmask', 'post_up', 'post_down', 'persistent_keepalive', 'created', 'updated', 'uuid')
search_fields = ('name', 'instance_id', 'private_key', 'hostname', 'listen_port', 'address', 'netmask', 'post_up', 'post_down', 'persistent_keepalive', 'created', 'updated', 'uuid')
admin.site.register(WireGuardInstance, WireGuardInstanceAdmin)
class PeerAdmin(admin.ModelAdmin):
list_display = ('name', 'public_key', 'pre_shared_key', 'persistent_keepalive', 'wireguard_instance', 'created', 'updated', 'uuid')
search_fields = ('name', 'public_key', 'pre_shared_key', 'persistent_keepalive', 'wireguard_instance', 'created', 'updated', 'uuid')
admin.site.register(Peer, PeerAdmin)
class PeerAllowedIPAdmin(admin.ModelAdmin):
list_display = ('peer', 'priority', 'allowed_ip', 'netmask', 'created', 'updated', 'uuid')
search_fields = ('peer', 'priority', 'allowed_ip', 'netmask', 'created', 'updated', 'uuid')
admin.site.register(PeerAllowedIP, PeerAllowedIPAdmin)

6
wireguard/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class WireguardConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'wireguard'

43
wireguard/forms.py Normal file
View file

@ -0,0 +1,43 @@
from django import forms
from .models import WireGuardInstance, NETMASK_CHOICES
from wgwadmlibrary.tools import is_valid_ip_or_hostname
import ipaddress
class WireGuardInstanceForm(forms.ModelForm):
name = forms.CharField(label='Display Name', required=False)
instance_id = forms.IntegerField(label='Instance ID')
private_key = forms.CharField(label='Private Key')
hostname = forms.CharField(label='Public Address')
listen_port = forms.IntegerField(label='Listen Port')
address = forms.GenericIPAddressField(label='VPN IP Address')
netmask = forms.ChoiceField(choices=NETMASK_CHOICES, label='Netmask')
post_up = forms.CharField(label='Post Up', required=False)
post_down = forms.CharField(label='Post Down', required=False)
persistent_keepalive = forms.IntegerField(label='Persistent Keepalive')
class Meta:
model = WireGuardInstance
fields = [
'name', 'instance_id', 'private_key', 'hostname', 'listen_port', 'address', 'netmask', 'post_up', 'post_down', 'persistent_keepalive'
]
def clean(self):
cleaned_data = super().clean()
hostname = cleaned_data.get('hostname')
address = cleaned_data.get('address')
netmask = cleaned_data.get('netmask')
if not is_valid_ip_or_hostname(hostname):
raise forms.ValidationError('Invalid hostname or IP Address')
current_network = ipaddress.ip_network(f"{address}/{netmask}", strict=False)
all_other_instances = WireGuardInstance.objects.all()
if self.instance:
all_other_instances = all_other_instances.exclude(uuid=self.instance.uuid)
for instance in all_other_instances:
other_network = ipaddress.ip_network(f"{instance.address}/{instance.netmask}", strict=False)
if current_network.overlaps(other_network):
raise forms.ValidationError(f"The network range {current_network} overlaps with another instance's network range {other_network}.")
return cleaned_data

View file

@ -0,0 +1,63 @@
# Generated by Django 5.0.1 on 2024-02-12 14:51
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Peer',
fields=[
('name', models.CharField(blank=True, max_length=100, null=True)),
('public_key', models.CharField(max_length=100)),
('pre_shared_key', models.CharField(max_length=100)),
('persistent_keepalive', models.IntegerField(default=25)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
],
),
migrations.CreateModel(
name='WireGuardInstance',
fields=[
('name', models.CharField(blank=True, max_length=100, null=True)),
('instance_id', models.PositiveIntegerField(default=0, unique=True)),
('private_key', models.CharField(max_length=100)),
('hostname', models.CharField(max_length=100)),
('listen_port', models.IntegerField(default=51820, unique=True)),
('address', models.GenericIPAddressField(protocol='IPv4', unique=True)),
('netmask', models.IntegerField(choices=[(8, '/8 (255.0.0.0)'), (9, '/9 (255.128.0.0)'), (10, '/10 (255.192.0.0)'), (11, '/11 (255.224.0.0)'), (12, '/12 (255.240.0.0)'), (13, '/13 (255.248.0.0)'), (14, '/14 (255.252.0.0)'), (15, '/15 (255.254.0.0)'), (16, '/16 (255.255.0.0)'), (17, '/17 (255.255.128.0)'), (18, '/18 (255.255.192.0)'), (19, '/19 (255.255.224.0)'), (20, '/20 (255.255.240.0)'), (21, '/21 (255.255.248.0)'), (22, '/22 (255.255.252.0)'), (23, '/23 (255.255.254.0)'), (24, '/24 (255.255.255.0)'), (25, '/25 (255.255.255.128)'), (26, '/26 (255.255.255.192)'), (27, '/27 (255.255.255.224)'), (28, '/28 (255.255.255.240)'), (29, '/29 (255.255.255.248)'), (30, '/30 (255.255.255.252)'), (32, '/32 (255.255.255.255)')], default=24)),
('post_up', models.TextField(blank=True, null=True)),
('post_down', models.TextField(blank=True, null=True)),
('persistent_keepalive', models.IntegerField(default=25)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
],
),
migrations.CreateModel(
name='PeerAllowedIP',
fields=[
('priority', models.PositiveBigIntegerField(default=1)),
('allowed_ip', models.GenericIPAddressField(protocol='IPv4')),
('netmask', models.IntegerField(choices=[(8, '/8 (255.0.0.0)'), (9, '/9 (255.128.0.0)'), (10, '/10 (255.192.0.0)'), (11, '/11 (255.224.0.0)'), (12, '/12 (255.240.0.0)'), (13, '/13 (255.248.0.0)'), (14, '/14 (255.252.0.0)'), (15, '/15 (255.254.0.0)'), (16, '/16 (255.255.0.0)'), (17, '/17 (255.255.128.0)'), (18, '/18 (255.255.192.0)'), (19, '/19 (255.255.224.0)'), (20, '/20 (255.255.240.0)'), (21, '/21 (255.255.248.0)'), (22, '/22 (255.255.252.0)'), (23, '/23 (255.255.254.0)'), (24, '/24 (255.255.255.0)'), (25, '/25 (255.255.255.128)'), (26, '/26 (255.255.255.192)'), (27, '/27 (255.255.255.224)'), (28, '/28 (255.255.255.240)'), (29, '/29 (255.255.255.248)'), (30, '/30 (255.255.255.252)'), (32, '/32 (255.255.255.255)')], default=32)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('peer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wireguard.peer')),
],
),
migrations.AddField(
model_name='peer',
name='wireguard_instance',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wireguard.wireguardinstance'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-02-14 00:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wireguard', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='peer',
name='private_key',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-02-14 14:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wireguard', '0002_peer_private_key'),
]
operations = [
migrations.AddField(
model_name='peerallowedip',
name='missing_from_wireguard',
field=models.BooleanField(default=False),
),
]

View file

87
wireguard/models.py Normal file
View file

@ -0,0 +1,87 @@
from django.db import models
import uuid
NETMASK_CHOICES = (
(8, '/8 (255.0.0.0)'),
(9, '/9 (255.128.0.0)'),
(10, '/10 (255.192.0.0)'),
(11, '/11 (255.224.0.0)'),
(12, '/12 (255.240.0.0)'),
(13, '/13 (255.248.0.0)'),
(14, '/14 (255.252.0.0)'),
(15, '/15 (255.254.0.0)'),
(16, '/16 (255.255.0.0)'),
(17, '/17 (255.255.128.0)'),
(18, '/18 (255.255.192.0)'),
(19, '/19 (255.255.224.0)'),
(20, '/20 (255.255.240.0)'),
(21, '/21 (255.255.248.0)'),
(22, '/22 (255.255.252.0)'),
(23, '/23 (255.255.254.0)'),
(24, '/24 (255.255.255.0)'),
(25, '/25 (255.255.255.128)'),
(26, '/26 (255.255.255.192)'),
(27, '/27 (255.255.255.224)'),
(28, '/28 (255.255.255.240)'),
(29, '/29 (255.255.255.248)'),
(30, '/30 (255.255.255.252)'),
(32, '/32 (255.255.255.255)'),
)
class WireGuardInstance(models.Model):
name = models.CharField(max_length=100, blank=True, null=True)
instance_id = models.PositiveIntegerField(unique=True, default=0)
private_key = models.CharField(max_length=100)
hostname = models.CharField(max_length=100)
listen_port = models.IntegerField(default=51820, unique=True)
address = models.GenericIPAddressField(unique=True, protocol='IPv4')
netmask = models.IntegerField(default=24, choices=NETMASK_CHOICES)
post_up = models.TextField(blank=True, null=True)
post_down = models.TextField(blank=True, null=True)
persistent_keepalive = models.IntegerField(default=25)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
def __str__(self):
if self.name:
return self.name
else:
return 'wg' + str(self.instance_id)
class Peer(models.Model):
name = models.CharField(max_length=100, blank=True, null=True)
public_key = models.CharField(max_length=100)
pre_shared_key = models.CharField(max_length=100)
private_key = models.CharField(max_length=100, blank=True, null=True)
persistent_keepalive = models.IntegerField(default=25)
wireguard_instance = models.ForeignKey(WireGuardInstance, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
def __str__(self):
if self.name:
return self.name
else:
return self.public_key
class PeerAllowedIP(models.Model):
peer = models.ForeignKey(Peer, on_delete=models.CASCADE)
priority = models.PositiveBigIntegerField(default=1)
allowed_ip = models.GenericIPAddressField(protocol='IPv4')
netmask = models.IntegerField(default=32, choices=NETMASK_CHOICES)
missing_from_wireguard = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
def __str__(self):
return str(self.allowed_ip) + '/' + str(self.netmask)

3
wireguard/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

123
wireguard/views.py Normal file
View file

@ -0,0 +1,123 @@
from django.shortcuts import render, get_object_or_404, redirect
from wireguard.forms import WireGuardInstanceForm
from .models import WireGuardInstance
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.db import models
import os
import subprocess
def generate_instance_defaults():
max_instance_id = WireGuardInstance.objects.all().aggregate(models.Max('instance_id'))['instance_id__max']
new_instance_id = (max_instance_id + 1) if max_instance_id is not None else 0
max_listen_port = WireGuardInstance.objects.all().aggregate(models.Max('listen_port'))['listen_port__max']
new_listen_port = (max_listen_port + 1) if max_listen_port is not None else 51820
new_private_key = subprocess.check_output('wg genkey', shell=True).decode('utf-8').strip()
new_address = f'10.188.{new_instance_id}.1'
address_parts = new_address.split('.')
if len(address_parts) == 4:
network = f"10.{address_parts[1]}.{address_parts[2]}.0/24"
port = new_listen_port
instance_id = new_instance_id
interface_name = f"wg{instance_id}"
post_up_script = (
f"iptables -t nat -A POSTROUTING -s {network} -o eth0 -j MASQUERADE\n"
f"iptables -A INPUT -p udp -m udp --dport {port} -j ACCEPT\n"
f"iptables -A FORWARD -i {interface_name} -o eth0 -d 10.0.0.0/8 -j REJECT\n"
f"iptables -A FORWARD -i {interface_name} -o eth0 -d 172.16.0.0/12 -j REJECT\n"
f"iptables -A FORWARD -i {interface_name} -o eth0 -d 192.168.0.0/16 -j REJECT\n"
f"iptables -A FORWARD -i {interface_name} -j ACCEPT\n"
f"iptables -A FORWARD -o {interface_name} -j ACCEPT"
)
post_down_script = (
f"iptables -t nat -D POSTROUTING -s {network} -o eth0 -j MASQUERADE\n"
f"iptables -D INPUT -p udp -m udp --dport {port} -j ACCEPT\n"
f"iptables -D FORWARD -i {interface_name} -o eth0 -d 10.0.0.0/8 -j REJECT\n"
f"iptables -D FORWARD -i {interface_name} -o eth0 -d 172.16.0.0/12 -j REJECT\n"
f"iptables -D FORWARD -i {interface_name} -o eth0 -d 192.168.0.0/16 -j REJECT\n"
f"iptables -D FORWARD -i {interface_name} -j ACCEPT\n"
f"iptables -D FORWARD -o {interface_name} -j ACCEPT"
)
return {
'name': '',
'instance_id': new_instance_id,
'listen_port': new_listen_port,
'private_key': new_private_key,
'address': new_address,
'netmask': 24,
'persistent_keepalive': 25,
'hostname': 'myserver.example.com',
'post_up': post_up_script,
'post_down': post_down_script,
}
@login_required
def view_welcome(request):
page_title = 'Welcome'
breadcrumb = {'level2': {'title': 'Place holder', 'href': '/blabla'}}
context = {'page_title': page_title, 'breadcrumb': breadcrumb}
return render(request, 'wireguard/welcome.html', context)
@login_required
def view_wireguard_status(request):
page_title = 'WireGuard Status'
context = {'page_title': page_title}
return render(request, 'wireguard/wireguard_status.html', context)
@login_required
def view_wireguard_manage_instance(request):
wireguard_instances = WireGuardInstance.objects.all().order_by('instance_id')
if request.GET.get('uuid'):
current_instance = get_object_or_404(WireGuardInstance, uuid=request.GET.get('uuid'))
else:
if request.GET.get('action') == 'create':
current_instance = None
else:
current_instance = wireguard_instances.first()
if current_instance:
page_title = f'wg{current_instance.instance_id}'
message_title = 'Update WireGuard Instance'
if current_instance.name:
page_title += f' ({current_instance.name})'
if request.GET.get('action') == 'delete':
message_title = 'Delete WireGuard Instance'
if request.GET.get('confirmation') == 'delete wg' + str(current_instance.instance_id):
if current_instance.peer_set.all().count() > 0:
messages.warning(request, 'Error removing wg' +str(current_instance.instance_id) + '|Cannot delete WireGuard instance wg' + str(current_instance.instance_id) + '. There are still peers associated with this instance.')
return redirect('/server/manage/?uuid=' + str(current_instance.uuid))
current_instance.delete()
messages.success(request, message_title + '|WireGuard instance wg' + str(current_instance.instance_id) + ' deleted successfully.')
return redirect('/server/manage/')
else:
messages.warning(request, 'Invalid confirmation' + '|Please confirm deletion of WireGuard instance wg' + str(current_instance.instance_id))
return redirect('/server/manage/?uuid=' + str(current_instance.uuid))
else:
page_title = 'Create a new WireGuard Instance'
message_title = 'New WireGuard Instance'
if request.method == 'POST':
form = WireGuardInstanceForm(request.POST, instance=current_instance)
if form.is_valid():
form.save()
messages.success(request, message_title + '|WireGuard instance wg' + str(form.instance.instance_id) + ' saved successfully.')
return redirect('/server/manage/?uuid=' + str(form.instance.uuid))
else:
if not current_instance:
form = WireGuardInstanceForm(initial=generate_instance_defaults())
else:
form = WireGuardInstanceForm(instance=current_instance)
context = {'page_title': page_title, 'wireguard_instances': wireguard_instances, 'current_instance': current_instance, 'form': form}
return render(request, 'wireguard/wireguard_manage_server.html', context)