mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-31 21:06:25 -06:00
Draft code for Wireguard support
This commit is contained in:
parent
9f7743e5da
commit
351ab1ecb0
@ -577,6 +577,10 @@ class BaseInterface(models.Model):
|
|||||||
def count_fhrp_groups(self):
|
def count_fhrp_groups(self):
|
||||||
return self.fhrp_group_assignments.count()
|
return self.fhrp_group_assignments.count()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wireguard_config(self):
|
||||||
|
return self.wireguard_configs.first()
|
||||||
|
|
||||||
|
|
||||||
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin):
|
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin):
|
||||||
"""
|
"""
|
||||||
@ -734,6 +738,12 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
related_query_name='interface',
|
related_query_name='interface',
|
||||||
)
|
)
|
||||||
|
wireguard_configs = GenericRelation(
|
||||||
|
to='vpn.WireguardConfig',
|
||||||
|
content_type_field='tunnel_interface_type',
|
||||||
|
object_id_field='tunnel_interface_id',
|
||||||
|
related_query_name='interface'
|
||||||
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'device', 'module', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'mtu', 'mode', 'speed', 'duplex', 'rf_role',
|
'device', 'module', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'mtu', 'mode', 'speed', 'duplex', 'rf_role',
|
||||||
|
@ -234,6 +234,7 @@ VPN_MENU = Menu(
|
|||||||
get_model_item('vpn', 'ipsecproposal', _('IPSec Proposals')),
|
get_model_item('vpn', 'ipsecproposal', _('IPSec Proposals')),
|
||||||
get_model_item('vpn', 'ipsecpolicy', _('IPSec Policies')),
|
get_model_item('vpn', 'ipsecpolicy', _('IPSec Policies')),
|
||||||
get_model_item('vpn', 'ipsecprofile', _('IPSec Profiles')),
|
get_model_item('vpn', 'ipsecprofile', _('IPSec Profiles')),
|
||||||
|
get_model_item('vpn', 'wireguardconfig', _('Wireguard Configs')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -37,10 +37,12 @@
|
|||||||
<th scope="row">{% trans "Encapsulation" %}</th>
|
<th scope="row">{% trans "Encapsulation" %}</th>
|
||||||
<td>{{ object.get_encapsulation_display }}</td>
|
<td>{{ object.get_encapsulation_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
{% if not object.is_wireguard %}
|
||||||
<th scope="row">{% trans "IPSec profile" %}</th>
|
<tr>
|
||||||
<td>{{ object.ipsec_profile|linkify|placeholder }}</td>
|
<th scope="row">{% trans "IPSec profile" %}</th>
|
||||||
</tr>
|
<td>{{ object.ipsec_profile|linkify|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Tunnel ID" %}</th>
|
<th scope="row">{% trans "Tunnel ID" %}</th>
|
||||||
<td>{{ object.tunnel_id|placeholder }}</td>
|
<td>{{ object.tunnel_id|placeholder }}</td>
|
||||||
|
32
netbox/templates/vpn/wireguardconfig.html
Normal file
32
netbox/templates/vpn/wireguardconfig.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{# TODO: Fix this template.. #}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "Tunnel" %}</h2>
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Listen port" %}</th>
|
||||||
|
<td>{{ object.listen_port }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Allowed ips" %}</th>
|
||||||
|
<td>{{ object.allowed_ips }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% include 'inc/panels/comments.html' %}
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -372,6 +372,12 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
|||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
related_query_name='vminterface',
|
related_query_name='vminterface',
|
||||||
)
|
)
|
||||||
|
wireguard_configs = GenericRelation(
|
||||||
|
to='vpn.WireguardConfig',
|
||||||
|
content_type_field='tunnel_interface_type',
|
||||||
|
object_id_field='tunnel_interface_id',
|
||||||
|
related_query_name='vminterface',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(ComponentModel.Meta):
|
class Meta(ComponentModel.Meta):
|
||||||
verbose_name = _('interface')
|
verbose_name = _('interface')
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
from vpn.choices import *
|
from vpn.choices import *
|
||||||
from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal
|
from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal, \
|
||||||
|
WireguardConfig
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'IKEPolicySerializer',
|
'IKEPolicySerializer',
|
||||||
@ -9,6 +10,7 @@ __all__ = (
|
|||||||
'IPSecPolicySerializer',
|
'IPSecPolicySerializer',
|
||||||
'IPSecProfileSerializer',
|
'IPSecProfileSerializer',
|
||||||
'IPSecProposalSerializer',
|
'IPSecProposalSerializer',
|
||||||
|
'WireguardConfigSerializer',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -119,3 +121,11 @@ class IPSecProfileSerializer(NetBoxModelSerializer):
|
|||||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Fix missing stuff
|
||||||
|
class WireguardConfigSerializer(NetBoxModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = WireguardConfig
|
||||||
|
fields = ('id', 'tunnel_interface_type')
|
||||||
|
brief_fields = ('id', 'tunnel_interface_type')
|
||||||
|
@ -26,12 +26,14 @@ class TunnelEncapsulationChoices(ChoiceSet):
|
|||||||
ENCAP_IP_IP = 'ip-ip'
|
ENCAP_IP_IP = 'ip-ip'
|
||||||
ENCAP_IPSEC_TRANSPORT = 'ipsec-transport'
|
ENCAP_IPSEC_TRANSPORT = 'ipsec-transport'
|
||||||
ENCAP_IPSEC_TUNNEL = 'ipsec-tunnel'
|
ENCAP_IPSEC_TUNNEL = 'ipsec-tunnel'
|
||||||
|
ENCAP_WIREGUARD = 'wireguard'
|
||||||
|
|
||||||
CHOICES = [
|
CHOICES = [
|
||||||
(ENCAP_IPSEC_TRANSPORT, _('IPsec - Transport')),
|
(ENCAP_IPSEC_TRANSPORT, _('IPsec - Transport')),
|
||||||
(ENCAP_IPSEC_TUNNEL, _('IPsec - Tunnel')),
|
(ENCAP_IPSEC_TUNNEL, _('IPsec - Tunnel')),
|
||||||
(ENCAP_IP_IP, _('IP-in-IP')),
|
(ENCAP_IP_IP, _('IP-in-IP')),
|
||||||
(ENCAP_GRE, _('GRE')),
|
(ENCAP_GRE, _('GRE')),
|
||||||
|
(ENCAP_WIREGUARD, _('Wireguard')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
|
from random import choices
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from dcim.choices import InterfaceTypeChoices
|
||||||
from dcim.models import Device, Interface
|
from dcim.models import Device, Interface
|
||||||
from ipam.models import IPAddress, RouteTarget, VLAN
|
from ipam.models import IPAddress, RouteTarget, VLAN
|
||||||
|
from netbox.api.fields import ChoiceField
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||||
@ -20,6 +24,7 @@ __all__ = (
|
|||||||
'IPSecPolicyForm',
|
'IPSecPolicyForm',
|
||||||
'IPSecProfileForm',
|
'IPSecProfileForm',
|
||||||
'IPSecProposalForm',
|
'IPSecProposalForm',
|
||||||
|
'WireguardConfigForm',
|
||||||
'L2VPNForm',
|
'L2VPNForm',
|
||||||
'L2VPNTerminationForm',
|
'L2VPNTerminationForm',
|
||||||
'TunnelCreateForm',
|
'TunnelCreateForm',
|
||||||
@ -57,8 +62,7 @@ class TunnelForm(TenancyForm, NetBoxModelForm):
|
|||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')),
|
FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tags', name=_('Tunnel')),
|
||||||
FieldSet('ipsec_profile', name=_('Security')),
|
|
||||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -68,6 +72,15 @@ class TunnelForm(TenancyForm, NetBoxModelForm):
|
|||||||
'name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tenant_group',
|
'name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tenant_group',
|
||||||
'tenant', 'comments', 'tags',
|
'tenant', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
widgets = {
|
||||||
|
'encapsulation': HTMXSelect(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, initial=None, **kwargs):
|
||||||
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
|
||||||
|
if get_field_value(self, 'encapsulation') == TunnelEncapsulationChoices.ENCAP_WIREGUARD:
|
||||||
|
del(self.fields['ipsec_profile'])
|
||||||
|
|
||||||
|
|
||||||
class TunnelCreateForm(TunnelForm):
|
class TunnelCreateForm(TunnelForm):
|
||||||
@ -142,8 +155,7 @@ class TunnelCreateForm(TunnelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')),
|
FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tags', name=_('Tunnel')),
|
||||||
FieldSet('ipsec_profile', name=_('Security')),
|
|
||||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'termination1_role', 'termination1_type', 'termination1_parent', 'termination1_termination',
|
'termination1_role', 'termination1_type', 'termination1_parent', 'termination1_termination',
|
||||||
@ -264,12 +276,23 @@ class TunnelTerminationForm(NetBoxModelForm):
|
|||||||
def __init__(self, *args, initial=None, **kwargs):
|
def __init__(self, *args, initial=None, **kwargs):
|
||||||
super().__init__(*args, initial=initial, **kwargs)
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
|
||||||
if (get_field_value(self, 'type') is None and
|
# Mimic HTMXSelect()
|
||||||
self.instance.pk and isinstance(self.instance.termination.parent_object, VirtualMachine)):
|
self.fields['tunnel'].widget.attrs.update({
|
||||||
|
'hx-get': '.',
|
||||||
|
'hx-include': '#form_fields',
|
||||||
|
'hx-target': '#form_fields',
|
||||||
|
})
|
||||||
|
|
||||||
|
tunnel_id = get_field_value(self, 'tunnel')
|
||||||
|
tunnel = Tunnel.objects.filter(id=tunnel_id).first() if tunnel_id else None
|
||||||
|
|
||||||
|
tt_type = get_field_value(self, 'type')
|
||||||
|
|
||||||
|
if tt_type is None and self.instance.pk and isinstance(self.instance.termination.parent_object, VirtualMachine):
|
||||||
self.fields['type'].initial = TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE
|
self.fields['type'].initial = TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE
|
||||||
|
|
||||||
# If initial or self.data is set and the type is a VIRTUALMACHINE type, swap the field querysets.
|
# If initial or self.data is set and the type is a VIRTUALMACHINE type, swap the field querysets.
|
||||||
if get_field_value(self, 'type') == TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE:
|
if tt_type == TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE:
|
||||||
self.fields['parent'].label = _('Virtual Machine')
|
self.fields['parent'].label = _('Virtual Machine')
|
||||||
self.fields['parent'].queryset = VirtualMachine.objects.all()
|
self.fields['parent'].queryset = VirtualMachine.objects.all()
|
||||||
self.fields['parent'].widget.attrs['selector'] = 'virtualization.virtualmachine'
|
self.fields['parent'].widget.attrs['selector'] = 'virtualization.virtualmachine'
|
||||||
@ -280,6 +303,10 @@ class TunnelTerminationForm(NetBoxModelForm):
|
|||||||
self.fields['outside_ip'].widget.add_query_params({
|
self.fields['outside_ip'].widget.add_query_params({
|
||||||
'virtual_machine_id': '$parent',
|
'virtual_machine_id': '$parent',
|
||||||
})
|
})
|
||||||
|
elif tunnel and tunnel.is_wireguard:
|
||||||
|
self.fields['termination'].help_text = _('As this is a Wireguard tunnel, only virtual interfaces are available for selection')
|
||||||
|
if tt_type == TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE:
|
||||||
|
self.fields['termination'].widget.add_query_params({'type': InterfaceTypeChoices.TYPE_VIRTUAL})
|
||||||
|
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
self.fields['parent'].initial = self.instance.termination.parent_object
|
self.fields['parent'].initial = self.instance.termination.parent_object
|
||||||
@ -287,9 +314,15 @@ class TunnelTerminationForm(NetBoxModelForm):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
termination = self.cleaned_data['termination']
|
||||||
|
|
||||||
|
# verify that interface is virtual
|
||||||
|
is_virtual_interface = termination.type == InterfaceTypeChoices.TYPE_VIRTUAL if self.cleaned_data['type'] == TunnelTerminationTypeChoices.TYPE_DEVICE else True
|
||||||
|
if self.cleaned_data['tunnel'].encapsulation == TunnelEncapsulationChoices.ENCAP_WIREGUARD and not is_virtual_interface:
|
||||||
|
raise forms.ValidationError(_('Interface must be virtual for Wireguard tunnels'))
|
||||||
|
|
||||||
# Set the terminated object
|
# Set the terminated object
|
||||||
self.instance.termination = self.cleaned_data.get('termination')
|
self.instance.termination = termination
|
||||||
|
|
||||||
|
|
||||||
class IKEProposalForm(NetBoxModelForm):
|
class IKEProposalForm(NetBoxModelForm):
|
||||||
@ -387,6 +420,71 @@ class IPSecProfileForm(NetBoxModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Wireguard Config
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
class WireguardConfigForm(NetBoxModelForm):
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=TunnelTerminationTypeChoices,
|
||||||
|
widget=HTMXSelect(),
|
||||||
|
label=_('Type')
|
||||||
|
)
|
||||||
|
parent = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
selector=True,
|
||||||
|
label=_('Device')
|
||||||
|
)
|
||||||
|
tunnel_interface = DynamicModelChoiceField(
|
||||||
|
queryset=Interface.objects.filter(type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||||
|
label=_('Tunnel interface'),
|
||||||
|
query_params={
|
||||||
|
'device_id': '$parent',
|
||||||
|
'type': InterfaceTypeChoices.TYPE_VIRTUAL,
|
||||||
|
},
|
||||||
|
help_text=_('Only virtual interfaces are shown'),
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('type', 'parent', 'tunnel_interface', 'tags', name=_('Wireguard config')),
|
||||||
|
FieldSet('private_key', 'public_key', name=_('Keys')),
|
||||||
|
FieldSet('listen_port', 'allowed_ips', 'fwmark', 'persistent_keepalive_interval', name=_('Parameters')),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WireguardConfig
|
||||||
|
fields = [
|
||||||
|
'type', 'parent', 'tunnel_interface', 'tags', 'private_key', 'public_key', 'listen_port', 'allowed_ips', 'fwmark', 'persistent_keepalive_interval',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, initial=None, **kwargs):
|
||||||
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
|
||||||
|
if (get_field_value(self, 'type') is None and
|
||||||
|
self.instance.pk and isinstance(self.instance.tunnel_interface.parent_object, VirtualMachine)):
|
||||||
|
self.fields['type'].initial = TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE
|
||||||
|
|
||||||
|
# If initial or self.data is set and the type is a VIRTUALMACHINE type, swap the field querysets.
|
||||||
|
if get_field_value(self, 'type') == TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE:
|
||||||
|
self.fields['parent'].label = _('Virtual Machine')
|
||||||
|
self.fields['parent'].queryset = VirtualMachine.objects.all()
|
||||||
|
self.fields['parent'].widget.attrs['selector'] = 'virtualization.virtualmachine'
|
||||||
|
self.fields['tunnel_interface'].queryset = VMInterface.objects.all()
|
||||||
|
self.fields['tunnel_interface'].widget.add_query_params({
|
||||||
|
'virtual_machine_id': '$parent',
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.instance.pk:
|
||||||
|
self.fields['parent'].initial = self.instance.tunnel_interface.parent_object
|
||||||
|
self.fields['tunnel_interface'].initial = self.instance.tunnel_interface
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Set the tunnel_interface object
|
||||||
|
self.instance.tunnel_interface = self.cleaned_data.get('tunnel_interface')
|
||||||
|
|
||||||
#
|
#
|
||||||
# L2VPN
|
# L2VPN
|
||||||
#
|
#
|
||||||
|
49
netbox/vpn/migrations/0006_wireguardconfig_and_more.py
Normal file
49
netbox/vpn/migrations/0006_wireguardconfig_and_more.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 5.0.7 on 2024-10-15 11:15
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ipam.fields
|
||||||
|
import taggit.managers
|
||||||
|
import utilities.json
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0121_customfield_related_object_filter'),
|
||||||
|
('vpn', '0005_rename_indexes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WireguardConfig',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||||
|
('tunnel_interface_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||||
|
('private_key', models.TextField(blank=True)),
|
||||||
|
('public_key', models.TextField(blank=True)),
|
||||||
|
('listen_port', models.PositiveIntegerField(default=51820, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)])),
|
||||||
|
('allowed_ips', django.contrib.postgres.fields.ArrayField(base_field=ipam.fields.IPNetworkField(), blank=True, null=True, size=None)),
|
||||||
|
('fwmark', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
('persistent_keepalive_interval', models.PositiveIntegerField(default=0)),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
('tunnel_interface_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'wireguard config',
|
||||||
|
'verbose_name_plural': 'wireguard configs',
|
||||||
|
'ordering': ('pk',),
|
||||||
|
'indexes': [models.Index(fields=['tunnel_interface_type', 'tunnel_interface_id'], name='vpn_wiregua_tunnel__5cdbbe_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='wireguardconfig',
|
||||||
|
constraint=models.UniqueConstraint(fields=('tunnel_interface_type', 'tunnel_interface_id'), name='vpn_wireguardconfig_tunnel_interface', violation_error_message='An tunnel_interface may only have one wireguard configration.'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,9 +1,15 @@
|
|||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.models import PrimaryModel
|
from ipam.constants import SERVICE_PORT_MIN, SERVICE_PORT_MAX
|
||||||
|
from ipam.fields import IPNetworkField
|
||||||
|
from netbox.models import PrimaryModel, CustomFieldsMixin, CustomLinksMixin, TagsMixin, \
|
||||||
|
ChangeLoggedModel
|
||||||
from vpn.choices import *
|
from vpn.choices import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -12,9 +18,13 @@ __all__ = (
|
|||||||
'IPSecPolicy',
|
'IPSecPolicy',
|
||||||
'IPSecProfile',
|
'IPSecProfile',
|
||||||
'IPSecProposal',
|
'IPSecProposal',
|
||||||
|
'WireguardConfig',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
WIREGUARD_DEFAULT_PORT = 51820
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# IKE
|
# IKE
|
||||||
#
|
#
|
||||||
@ -255,3 +265,94 @@ class IPSecProfile(PrimaryModel):
|
|||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('vpn:ipsecprofile', args=[self.pk])
|
return reverse('vpn:ipsecprofile', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
|
class WireguardConfig(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLoggedModel):
|
||||||
|
tunnel_interface_type = models.ForeignKey(
|
||||||
|
to='contenttypes.ContentType',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+'
|
||||||
|
)
|
||||||
|
tunnel_interface_id = models.PositiveBigIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
tunnel_interface = GenericForeignKey(
|
||||||
|
ct_field='tunnel_interface_type',
|
||||||
|
fk_field='tunnel_interface_id'
|
||||||
|
)
|
||||||
|
private_key = models.TextField(
|
||||||
|
verbose_name=_('private key'),
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
public_key = models.TextField(
|
||||||
|
verbose_name=_('public key'),
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
listen_port = models.PositiveIntegerField(
|
||||||
|
verbose_name=_('listen port'),
|
||||||
|
default=WIREGUARD_DEFAULT_PORT,
|
||||||
|
validators=[
|
||||||
|
MinValueValidator(SERVICE_PORT_MIN),
|
||||||
|
MaxValueValidator(SERVICE_PORT_MAX)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
allowed_ips = ArrayField(
|
||||||
|
base_field=IPNetworkField(),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('allowed ips'),
|
||||||
|
help_text=_(
|
||||||
|
"Represents the permissible IPv4/IPv6 networks for use by other peers in their "
|
||||||
|
"'allowed_ips' configuration while creating a tunnel with this peer. "
|
||||||
|
"Ex: '10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64'"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
fwmark = models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_('fwmark'),
|
||||||
|
help_text=_('Optional. Set a 32-bit integer firewall mark (fwmark) for outgoing packets'),
|
||||||
|
)
|
||||||
|
persistent_keepalive_interval = models.PositiveIntegerField(
|
||||||
|
default=0,
|
||||||
|
verbose_name=_('persistent keepalive interval'),
|
||||||
|
help_text=_('Persistant keepalive interval in seconds, 0 disables this feature'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('pk',)
|
||||||
|
indexes = (
|
||||||
|
models.Index(fields=('tunnel_interface_type', 'tunnel_interface_id')),
|
||||||
|
)
|
||||||
|
constraints = (
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('tunnel_interface_type', 'tunnel_interface_id'),
|
||||||
|
name='%(app_label)s_%(class)s_tunnel_interface',
|
||||||
|
violation_error_message=_("An tunnel_interface may only have one wireguard configration.")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
verbose_name = _('wireguard config')
|
||||||
|
verbose_name_plural = _('wireguard configs')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.tunnel_interface.name}: Wireguard config'
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('vpn:wireguardconfig', args=[self.pk])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Check that the selected termination object is not already
|
||||||
|
if getattr(self.tunnel_interface, 'wireguard_config', None) and self.tunnel_interface.wireguard_config.pk != self.pk:
|
||||||
|
raise ValidationError({
|
||||||
|
'tunnel_interface': _("{name} already has a Wireguard config").format(
|
||||||
|
name=self.tunnel_interface.name,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
def to_objectchange(self, action):
|
||||||
|
objectchange = super().to_objectchange(action)
|
||||||
|
objectchange.related_object = self.tunnel_interface
|
||||||
|
return objectchange
|
||||||
|
@ -103,6 +103,10 @@ class Tunnel(PrimaryModel):
|
|||||||
def get_status_color(self):
|
def get_status_color(self):
|
||||||
return TunnelStatusChoices.colors.get(self.status)
|
return TunnelStatusChoices.colors.get(self.status)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_wireguard(self):
|
||||||
|
return self.encapsulation == TunnelEncapsulationChoices.ENCAP_WIREGUARD
|
||||||
|
|
||||||
|
|
||||||
class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLoggedModel):
|
class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLoggedModel):
|
||||||
tunnel = models.ForeignKey(
|
tunnel = models.ForeignKey(
|
||||||
|
@ -10,6 +10,7 @@ __all__ = (
|
|||||||
'IPSecPolicyTable',
|
'IPSecPolicyTable',
|
||||||
'IPSecProposalTable',
|
'IPSecProposalTable',
|
||||||
'IPSecProfileTable',
|
'IPSecProfileTable',
|
||||||
|
'WireguardConfigTable',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -183,3 +184,36 @@ class IPSecProfileTable(NetBoxTable):
|
|||||||
'last_updated',
|
'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description')
|
default_columns = ('pk', 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Fix..
|
||||||
|
class WireguardConfigTable(NetBoxTable):
|
||||||
|
tunnel_interface_parent = tables.Column(
|
||||||
|
accessor='tunnel_interface__parent_object',
|
||||||
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name=_('Host')
|
||||||
|
)
|
||||||
|
tunnel_interface = tables.Column(
|
||||||
|
verbose_name=_('Tunnel interface'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
ip_addresses = columns.ManyToManyColumn(
|
||||||
|
accessor=tables.A('tunnel_interface__ip_addresses'),
|
||||||
|
orderable=False,
|
||||||
|
linkify_item=True,
|
||||||
|
verbose_name=_('IP Addresses')
|
||||||
|
)
|
||||||
|
tags = columns.TagColumn(
|
||||||
|
url_name='vpn:tunneltermination_list'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = WireguardConfig
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'tunnel', 'role', 'tunnel_interface_parent', 'tunnel_interface', 'ip_addresses', 'tags',
|
||||||
|
'created', 'last_updated',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'id', 'tunnel_interface_parent', 'tunnel_interface', 'ip_addresses',
|
||||||
|
)
|
||||||
|
@ -70,6 +70,14 @@ urlpatterns = [
|
|||||||
path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'),
|
path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'),
|
||||||
path('ipsec-profiles/<int:pk>/', include(get_model_urls('vpn', 'ipsecprofile'))),
|
path('ipsec-profiles/<int:pk>/', include(get_model_urls('vpn', 'ipsecprofile'))),
|
||||||
|
|
||||||
|
# Wireguard configs
|
||||||
|
path('wireguard-configs/', views.WireguardConfigListView.as_view(), name='wireguardconfig_list'),
|
||||||
|
path('wireguard-configs/add/', views.WireguardConfigEditView.as_view(), name='wireguardconfig_add'),
|
||||||
|
path('wireguard-configs/import/', views.WireguardConfigBulkImportView.as_view(), name='wireguardconfig_import'),
|
||||||
|
path('wireguard-configs/edit/', views.WireguardConfigBulkEditView.as_view(), name='wireguardconfig_bulk_edit'),
|
||||||
|
path('wireguard-configs/delete/', views.WireguardConfigBulkDeleteView.as_view(), name='wireguardconfig_bulk_delete'),
|
||||||
|
path('wireguard-configs/<int:pk>/', include(get_model_urls('vpn', 'wireguardconfig'))),
|
||||||
|
|
||||||
# L2VPN
|
# L2VPN
|
||||||
path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'),
|
path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'),
|
||||||
path('l2vpns/add/', views.L2VPNEditView.as_view(), name='l2vpn_add'),
|
path('l2vpns/add/', views.L2VPNEditView.as_view(), name='l2vpn_add'),
|
||||||
|
@ -392,6 +392,56 @@ class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.IPSecProfileTable
|
table = tables.IPSecProfileTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Wireguard configs
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Fix..
|
||||||
|
class WireguardConfigListView(generic.ObjectListView):
|
||||||
|
queryset = WireguardConfig.objects.all()
|
||||||
|
# filterset = filtersets.WireguardConfigFilterSet
|
||||||
|
# filterset_form = forms.WireguardConfigFilterForm
|
||||||
|
table = tables.WireguardConfigTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(WireguardConfig)
|
||||||
|
class WireguardConfigView(generic.ObjectView):
|
||||||
|
queryset = WireguardConfig.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(WireguardConfig, 'edit')
|
||||||
|
class WireguardConfigEditView(generic.ObjectEditView):
|
||||||
|
queryset = WireguardConfig.objects.all()
|
||||||
|
form = forms.WireguardConfigForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(WireguardConfig, 'delete')
|
||||||
|
class WireguardConfigDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = WireguardConfig.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Fix..
|
||||||
|
class WireguardConfigBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = WireguardConfig.objects.all()
|
||||||
|
# model_form = forms.WireguardConfigImportForm
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Fix..
|
||||||
|
class WireguardConfigBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = WireguardConfig.objects.all()
|
||||||
|
# filterset = filtersets.WireguardConfigFilterSet
|
||||||
|
table = tables.WireguardConfigTable
|
||||||
|
# form = forms.WireguardConfigBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Fix..
|
||||||
|
class WireguardConfigBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = WireguardConfig.objects.all()
|
||||||
|
# filterset = filtersets.WireguardConfigFilterSet
|
||||||
|
table = tables.WireguardConfigTable
|
||||||
|
|
||||||
|
|
||||||
# L2VPN
|
# L2VPN
|
||||||
|
|
||||||
class L2VPNListView(generic.ObjectListView):
|
class L2VPNListView(generic.ObjectListView):
|
||||||
|
Loading…
Reference in New Issue
Block a user