diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index b1f951541..b32934b24 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -577,6 +577,10 @@ class BaseInterface(models.Model):
def count_fhrp_groups(self):
return self.fhrp_group_assignments.count()
+ @property
+ def wireguard_config(self):
+ return self.wireguard_configs.first()
+
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin):
"""
@@ -734,6 +738,12 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
object_id_field='assigned_object_id',
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 = (
'device', 'module', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'mtu', 'mode', 'speed', 'duplex', 'rf_role',
diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py
index 9d8ffaaf8..afb32e42d 100644
--- a/netbox/netbox/navigation/menu.py
+++ b/netbox/netbox/navigation/menu.py
@@ -234,6 +234,7 @@ VPN_MENU = Menu(
get_model_item('vpn', 'ipsecproposal', _('IPSec Proposals')),
get_model_item('vpn', 'ipsecpolicy', _('IPSec Policies')),
get_model_item('vpn', 'ipsecprofile', _('IPSec Profiles')),
+ get_model_item('vpn', 'wireguardconfig', _('Wireguard Configs')),
),
),
),
diff --git a/netbox/templates/vpn/tunnel.html b/netbox/templates/vpn/tunnel.html
index b8e95cac3..68b5e4f6b 100644
--- a/netbox/templates/vpn/tunnel.html
+++ b/netbox/templates/vpn/tunnel.html
@@ -37,10 +37,12 @@
{% trans "Encapsulation" %} |
{{ object.get_encapsulation_display }} |
-
- {% trans "IPSec profile" %} |
- {{ object.ipsec_profile|linkify|placeholder }} |
-
+ {% if not object.is_wireguard %}
+
+ {% trans "IPSec profile" %} |
+ {{ object.ipsec_profile|linkify|placeholder }} |
+
+ {% endif %}
{% trans "Tunnel ID" %} |
{{ object.tunnel_id|placeholder }} |
diff --git a/netbox/templates/vpn/wireguardconfig.html b/netbox/templates/vpn/wireguardconfig.html
new file mode 100644
index 000000000..6caa2a104
--- /dev/null
+++ b/netbox/templates/vpn/wireguardconfig.html
@@ -0,0 +1,32 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+{% load i18n %}
+
+{# TODO: Fix this template.. #}
+{% block content %}
+
+
+
+
+
+
+ {% trans "Listen port" %} |
+ {{ object.listen_port }} |
+
+
+ {% trans "Allowed ips" %} |
+ {{ object.allowed_ips }} |
+
+
+
+ {% plugin_left_page object %}
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/tags.html' %}
+ {% include 'inc/panels/comments.html' %}
+ {% plugin_right_page object %}
+
+
+{% endblock %}
diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py
index 17929f476..406f92713 100644
--- a/netbox/virtualization/models/virtualmachines.py
+++ b/netbox/virtualization/models/virtualmachines.py
@@ -372,6 +372,12 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
object_id_field='assigned_object_id',
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):
verbose_name = _('interface')
diff --git a/netbox/vpn/api/serializers_/crypto.py b/netbox/vpn/api/serializers_/crypto.py
index c11b8de2b..b6a7c4eed 100644
--- a/netbox/vpn/api/serializers_/crypto.py
+++ b/netbox/vpn/api/serializers_/crypto.py
@@ -1,7 +1,8 @@
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from vpn.choices import *
-from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal
+from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal, \
+ WireguardConfig
__all__ = (
'IKEPolicySerializer',
@@ -9,6 +10,7 @@ __all__ = (
'IPSecPolicySerializer',
'IPSecProfileSerializer',
'IPSecProposalSerializer',
+ 'WireguardConfigSerializer',
)
@@ -119,3 +121,11 @@ class IPSecProfileSerializer(NetBoxModelSerializer):
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
)
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')
diff --git a/netbox/vpn/choices.py b/netbox/vpn/choices.py
index 751454049..0a1a67430 100644
--- a/netbox/vpn/choices.py
+++ b/netbox/vpn/choices.py
@@ -26,12 +26,14 @@ class TunnelEncapsulationChoices(ChoiceSet):
ENCAP_IP_IP = 'ip-ip'
ENCAP_IPSEC_TRANSPORT = 'ipsec-transport'
ENCAP_IPSEC_TUNNEL = 'ipsec-tunnel'
+ ENCAP_WIREGUARD = 'wireguard'
CHOICES = [
(ENCAP_IPSEC_TRANSPORT, _('IPsec - Transport')),
(ENCAP_IPSEC_TUNNEL, _('IPsec - Tunnel')),
(ENCAP_IP_IP, _('IP-in-IP')),
(ENCAP_GRE, _('GRE')),
+ (ENCAP_WIREGUARD, _('Wireguard')),
]
diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py
index a17ca9a5e..176f9dd7d 100644
--- a/netbox/vpn/forms/model_forms.py
+++ b/netbox/vpn/forms/model_forms.py
@@ -1,9 +1,13 @@
+from random import choices
+
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
+from dcim.choices import InterfaceTypeChoices
from dcim.models import Device, Interface
from ipam.models import IPAddress, RouteTarget, VLAN
+from netbox.api.fields import ChoiceField
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
@@ -20,6 +24,7 @@ __all__ = (
'IPSecPolicyForm',
'IPSecProfileForm',
'IPSecProposalForm',
+ 'WireguardConfigForm',
'L2VPNForm',
'L2VPNTerminationForm',
'TunnelCreateForm',
@@ -57,8 +62,7 @@ class TunnelForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
- FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')),
- FieldSet('ipsec_profile', name=_('Security')),
+ FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tags', name=_('Tunnel')),
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',
'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):
@@ -142,8 +155,7 @@ class TunnelCreateForm(TunnelForm):
)
fieldsets = (
- FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')),
- FieldSet('ipsec_profile', name=_('Security')),
+ FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tags', name=_('Tunnel')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
FieldSet(
'termination1_role', 'termination1_type', 'termination1_parent', 'termination1_termination',
@@ -264,12 +276,23 @@ class TunnelTerminationForm(NetBoxModelForm):
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.termination.parent_object, VirtualMachine)):
+ # Mimic HTMXSelect()
+ 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
# 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'].queryset = VirtualMachine.objects.all()
self.fields['parent'].widget.attrs['selector'] = 'virtualization.virtualmachine'
@@ -280,6 +303,10 @@ class TunnelTerminationForm(NetBoxModelForm):
self.fields['outside_ip'].widget.add_query_params({
'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:
self.fields['parent'].initial = self.instance.termination.parent_object
@@ -287,9 +314,15 @@ class TunnelTerminationForm(NetBoxModelForm):
def clean(self):
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
- self.instance.termination = self.cleaned_data.get('termination')
+ self.instance.termination = termination
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
#
diff --git a/netbox/vpn/migrations/0006_wireguardconfig_and_more.py b/netbox/vpn/migrations/0006_wireguardconfig_and_more.py
new file mode 100644
index 000000000..eb5549f45
--- /dev/null
+++ b/netbox/vpn/migrations/0006_wireguardconfig_and_more.py
@@ -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.'),
+ ),
+ ]
diff --git a/netbox/vpn/models/crypto.py b/netbox/vpn/models/crypto.py
index 2769430fd..5ba1ab9fb 100644
--- a/netbox/vpn/models/crypto.py
+++ b/netbox/vpn/models/crypto.py
@@ -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.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.urls import reverse
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 *
__all__ = (
@@ -12,9 +18,13 @@ __all__ = (
'IPSecPolicy',
'IPSecProfile',
'IPSecProposal',
+ 'WireguardConfig',
)
+WIREGUARD_DEFAULT_PORT = 51820
+
+
#
# IKE
#
@@ -255,3 +265,94 @@ class IPSecProfile(PrimaryModel):
def get_absolute_url(self):
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
diff --git a/netbox/vpn/models/tunnels.py b/netbox/vpn/models/tunnels.py
index 6f4fa4182..2ce98cf01 100644
--- a/netbox/vpn/models/tunnels.py
+++ b/netbox/vpn/models/tunnels.py
@@ -103,6 +103,10 @@ class Tunnel(PrimaryModel):
def get_status_color(self):
return TunnelStatusChoices.colors.get(self.status)
+ @property
+ def is_wireguard(self):
+ return self.encapsulation == TunnelEncapsulationChoices.ENCAP_WIREGUARD
+
class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLoggedModel):
tunnel = models.ForeignKey(
diff --git a/netbox/vpn/tables/crypto.py b/netbox/vpn/tables/crypto.py
index 474062b39..d71910c88 100644
--- a/netbox/vpn/tables/crypto.py
+++ b/netbox/vpn/tables/crypto.py
@@ -10,6 +10,7 @@ __all__ = (
'IPSecPolicyTable',
'IPSecProposalTable',
'IPSecProfileTable',
+ 'WireguardConfigTable',
)
@@ -183,3 +184,36 @@ class IPSecProfileTable(NetBoxTable):
'last_updated',
)
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',
+ )
diff --git a/netbox/vpn/urls.py b/netbox/vpn/urls.py
index 552f0e185..3b97b4d54 100644
--- a/netbox/vpn/urls.py
+++ b/netbox/vpn/urls.py
@@ -70,6 +70,14 @@ urlpatterns = [
path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'),
path('ipsec-profiles//', 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//', include(get_model_urls('vpn', 'wireguardconfig'))),
+
# L2VPN
path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'),
path('l2vpns/add/', views.L2VPNEditView.as_view(), name='l2vpn_add'),
diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py
index ac8ce3667..b8d40df25 100644
--- a/netbox/vpn/views.py
+++ b/netbox/vpn/views.py
@@ -392,6 +392,56 @@ class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
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
class L2VPNListView(generic.ObjectListView):