diff --git a/docs/features/vpn-tunnels.md b/docs/features/vpn-tunnels.md
new file mode 100644
index 00000000000..ae6df70c84e
--- /dev/null
+++ b/docs/features/vpn-tunnels.md
@@ -0,0 +1,49 @@
+# Tunnels
+
+NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces.
+
+```mermaid
+flowchart TD
+ Termination1[TunnelTermination]
+ Termination2[TunnelTermination]
+ Interface1[Interface]
+ Interface2[Interface]
+ Tunnel --> Termination1 & Termination2
+ Termination1 --> Interface1
+ Termination2 --> Interface2
+ Interface1 --> Device
+ Interface2 --> VirtualMachine
+
+click Tunnel "../../models/vpn/tunnel/"
+click TunnelTermination1 "../../models/vpn/tunneltermination/"
+click TunnelTermination2 "../../models/vpn/tunneltermination/"
+```
+
+# IPSec & IKE
+
+NetBox includes robust support for modeling IPSec & IKE policies. These are used to define encryption and authentication parameters for IPSec tunnels.
+
+```mermaid
+flowchart TD
+ subgraph IKEProposals[Proposals]
+ IKEProposal1[IKEProposal]
+ IKEProposal2[IKEProposal]
+ end
+ subgraph IPSecProposals[Proposals]
+ IPSecProposal1[IPSecProposal]
+ IPSecProposal2[IPSecProposal]
+ end
+ IKEProposals --> IKEPolicy
+ IPSecProposals --> IPSecPolicy
+ IKEPolicy & IPSecPolicy--> IPSecProfile
+ IPSecProfile --> Tunnel
+
+click IKEProposal1 "../../models/vpn/ikeproposal/"
+click IKEProposal2 "../../models/vpn/ikeproposal/"
+click IKEPolicy "../../models/vpn/ikepolicy/"
+click IPSecProposal1 "../../models/vpn/ipsecproposal/"
+click IPSecProposal2 "../../models/vpn/ipsecproposal/"
+click IPSecPolicy "../../models/vpn/ipsecpolicy/"
+click IPSecProfile "../../models/vpn/ipsecprofile/"
+click Tunnel "../../models/vpn/tunnel/"
+```
diff --git a/docs/models/vpn/ikepolicy.md b/docs/models/vpn/ikepolicy.md
new file mode 100644
index 00000000000..7b739072b34
--- /dev/null
+++ b/docs/models/vpn/ikepolicy.md
@@ -0,0 +1,25 @@
+# IKE Policies
+
+An [Internet Key Exhcnage (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) policy defines an IKE version, mode, and set of [proposals](./ikeproposal.md) to be used in IKE negotiation. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
+
+## Fields
+
+### Name
+
+The unique user-assigned name for the policy.
+
+### Version
+
+The IKE version employed (v1 or v2).
+
+### Mode
+
+The IKE mode employed (main or aggressive).
+
+### Proposals
+
+One or more [IKE proposals](./ikeproposal.md) supported for use by this policy.
+
+### Pre-shared Key
+
+A pre-shared secret key associated with this policy (optional).
diff --git a/docs/models/vpn/ikeproposal.md b/docs/models/vpn/ikeproposal.md
new file mode 100644
index 00000000000..dd8d7533065
--- /dev/null
+++ b/docs/models/vpn/ikeproposal.md
@@ -0,0 +1,39 @@
+# IKE Proposals
+
+An [Internet Key Exhcnage (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) proposal defines a set of parameters used to establish a secure bidirectional connection across an untrusted medium, such as the Internet. IKE proposals defined in NetBox can be referenced by [IKE policies](./ikepolicy.md), which are in turn employed by [IPSec profiles](./ipsecprofile.md).
+
+!!! note
+ Some platforms refer to IKE proposals as [ISAKMP](https://en.wikipedia.org/wiki/Internet_Security_Association_and_Key_Management_Protocol), which is a framework for authentication and key exchange which employs IKE.
+
+## Fields
+
+### Name
+
+The unique user-assigned name for the proposal.
+
+### Authentication Method
+
+The strategy employed for authenticating the IKE peer. Available options are listed below.
+
+| Name |
+|----------------|
+| Pre-shared key |
+| Certificate |
+| RSA signature |
+| DSA signature |
+
+### Encryption Algorithm
+
+The protocol employed for data encryption. Options include DES, 3DES, and various flavors of AES.
+
+### Authentication Algorithm
+
+The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations.
+
+### Group
+
+The [Diffie-Hellman group](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) supported by the proposal. Group IDs are [managed by IANA](https://www.iana.org/assignments/ikev2-parameters/ikev2-parameters.xhtml#ikev2-parameters-8).
+
+### SA Lifetime
+
+The maximum lifetime for the IKE security association (SA), in seconds.
diff --git a/docs/models/vpn/ipsecpolicy.md b/docs/models/vpn/ipsecpolicy.md
new file mode 100644
index 00000000000..3283d3b23be
--- /dev/null
+++ b/docs/models/vpn/ipsecpolicy.md
@@ -0,0 +1,17 @@
+# IPSec Policy
+
+An [IPSec](https://en.wikipedia.org/wiki/IPsec) policy defines a set of [proposals](./ikeproposal.md) to be used in the formation of IPSec tunnels. A perfect forward secrecy (PFS) group may optionally also be defined. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
+
+## Fields
+
+### Name
+
+The unique user-assigned name for the policy.
+
+### Proposals
+
+One or more [IPSec proposals](./ipsecproposal.md) supported for use by this policy.
+
+### PFS Group
+
+The [perfect forward secrecy (PFS)](https://en.wikipedia.org/wiki/Forward_secrecy) group supported by this policy (optional).
diff --git a/docs/models/vpn/ipsecprofile.md b/docs/models/vpn/ipsecprofile.md
new file mode 100644
index 00000000000..1ad1ce7d537
--- /dev/null
+++ b/docs/models/vpn/ipsecprofile.md
@@ -0,0 +1,21 @@
+# IPSec Profile
+
+An [IPSec](https://en.wikipedia.org/wiki/IPsec) profile defines an [IKE policy](./ikepolicy.md), [IPSec policy](./ipsecpolicy.md), and IPSec mode used for establishing an IPSec tunnel.
+
+## Fields
+
+### Name
+
+The unique user-assigned name for the profile.
+
+### Mode
+
+The IPSec mode employed by the profile: Encapsulating Security Payload (ESP) or Authentication Header (AH).
+
+### IKE Policy
+
+The [IKE policy](./ikepolicy.md) associated with the profile.
+
+### IPSec Policy
+
+The [IPSec policy](./ipsecpolicy.md) associated with the profile.
diff --git a/docs/models/vpn/ipsecproposal.md b/docs/models/vpn/ipsecproposal.md
new file mode 100644
index 00000000000..d061b153543
--- /dev/null
+++ b/docs/models/vpn/ipsecproposal.md
@@ -0,0 +1,25 @@
+# IPSec Proposal
+
+An [IPSec](https://en.wikipedia.org/wiki/IPsec) proposal defines a set of parameters used in negotiating security associations for IPSec tunnels. IPSec proposals defined in NetBox can be referenced by [IPSec policies](./ipsecpolicy.md), which are in turn employed by [IPSec profiles](./ipsecprofile.md).
+
+## Fields
+
+### Name
+
+The unique user-assigned name for the proposal.
+
+### Encryption Algorithm
+
+The protocol employed for data encryption. Options include DES, 3DES, and various flavors of AES.
+
+### Authentication Algorithm
+
+The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations.
+
+### SA Lifetime (Seconds)
+
+The maximum amount of time for which the security association (SA) may be active, in seconds.
+
+### SA Lifetime (Data)
+
+The maximum amount of data which can be transferred within the security association (SA) before it must be rebuilt, in kilobytes.
diff --git a/docs/models/vpn/tunnel.md b/docs/models/vpn/tunnel.md
new file mode 100644
index 00000000000..ebe004da103
--- /dev/null
+++ b/docs/models/vpn/tunnel.md
@@ -0,0 +1,36 @@
+# Tunnels
+
+A tunnel represents a private virtual connection established among two or more endpoints across a shared infrastructure by employing protocol encapsulation. Common encapsulation techniques include [Generic Routing Encapsulation (GRE)](https://en.wikipedia.org/wiki/Generic_Routing_Encapsulation), [IP-in-IP](https://en.wikipedia.org/wiki/IP_in_IP), and [IPSec](https://en.wikipedia.org/wiki/IPsec). NetBox supports modeling both peer-to-peer and hub-and-spoke tunnel topologies.
+
+Device and virtual machine interfaces are associated to tunnels by creating [tunnel terminations](./tunneltermination.md).
+
+## Fields
+
+### Name
+
+A unique name assigned to the tunnel for identification.
+
+### Status
+
+The operational status of the tunnel. By default, the following statuses are available:
+
+| Name |
+|----------------|
+| Planned |
+| Active |
+| Disabled |
+
+!!! tip "Custom tunnel statuses"
+ Additional tunnel statuses may be defined by setting `Tunnel.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
+
+### Encapsulation
+
+The encapsulation protocol or technique employed to effect the tunnel. NetBox supports GRE, IP-in-IP, and IPSec encapsulations.
+
+### Tunnel ID
+
+An optional numeric identifier for the tunnel.
+
+### IPSec Profile
+
+For IPSec tunnels, this is the [IPSec Profile](./ipsecprofile.md) employed to negotiate security associations.
diff --git a/docs/models/vpn/tunneltermination.md b/docs/models/vpn/tunneltermination.md
new file mode 100644
index 00000000000..8400eaa8639
--- /dev/null
+++ b/docs/models/vpn/tunneltermination.md
@@ -0,0 +1,30 @@
+# Tunnel Terminations
+
+A tunnel termination connects a device or virtual machine interface to a [tunnel](./tunnel.md). The tunnel must be created before any terminations may be added.
+
+## Fields
+
+### Tunnel
+
+The [tunnel](./tunnel.md) to which this termination is made.
+
+### Role
+
+The functional role of the attached interface. The following options are available:
+
+| Name | Description |
+|-------|--------------------------------------------------|
+| Peer | An endpoint in a point-to-point or mesh topology |
+| Hub | A central point in a hub-and-spoke topology |
+| Spoke | An edge point in a hub-and-spoke topology |
+
+!!! note
+ Multiple hub terminations may be attached to a tunnel.
+
+### Termination
+
+The device or virtual machine interface terminated to the tunnel.
+
+### Outside IP
+
+The public or underlay IP address with which this termination is associated. This is the IP to which peers will route tunneled traffic.
diff --git a/mkdocs.yml b/mkdocs.yml
index 3e61f922ae6..f927bf38665 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -74,6 +74,7 @@ nav:
- Circuits: 'features/circuits.md'
- Wireless: 'features/wireless.md'
- Virtualization: 'features/virtualization.md'
+ - VPN Tunnels: 'features/vpn-tunnels.md'
- Tenancy: 'features/tenancy.md'
- Contacts: 'features/contacts.md'
- Search: 'features/search.md'
@@ -252,6 +253,14 @@ nav:
- ClusterType: 'models/virtualization/clustertype.md'
- VMInterface: 'models/virtualization/vminterface.md'
- VirtualMachine: 'models/virtualization/virtualmachine.md'
+ - VPN:
+ - IKEPolicy: 'models/vpn/ikepolicy.md'
+ - IKEProposal: 'models/vpn/ikeproposal.md'
+ - IPSecPolicy: 'models/vpn/ipsecpolicy.md'
+ - IPSecProfile: 'models/vpn/ipsecprofile.md'
+ - IPSecProposal: 'models/vpn/ipsecproposal.md'
+ - Tunnel: 'models/vpn/tunnel.md'
+ - TunnelTermination: 'models/vpn/tunneltermination.md'
- Wireless:
- WirelessLAN: 'models/wireless/wirelesslan.md'
- WirelessLANGroup: 'models/wireless/wirelesslangroup.md'
diff --git a/netbox/core/management/commands/nbshell.py b/netbox/core/management/commands/nbshell.py
index 674a878c754..fd86627d273 100644
--- a/netbox/core/management/commands/nbshell.py
+++ b/netbox/core/management/commands/nbshell.py
@@ -9,7 +9,7 @@
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand
-APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'wireless')
+APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
BANNER_TEXT = """### NetBox interactive shell ({node})
### Python {python} | Django {django} | NetBox {netbox}
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 94568459e80..1df07bb9b5d 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -567,6 +567,10 @@ def save(self, *args, **kwargs):
return super().save(*args, **kwargs)
+ @property
+ def tunnel_termination(self):
+ return self.tunnel_terminations.first()
+
@property
def count_ipaddresses(self):
return self.ip_addresses.count()
@@ -720,6 +724,12 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
object_id_field='interface_id',
related_query_name='+'
)
+ tunnel_terminations = GenericRelation(
+ to='vpn.TunnelTermination',
+ content_type_field='termination_type',
+ object_id_field='termination_id',
+ related_query_name='interface'
+ )
l2vpn_terminations = GenericRelation(
to='ipam.L2VPNTermination',
content_type_field='assigned_object_type',
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index b72c37daa4d..60e203697f4 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -584,6 +584,12 @@ class BaseInterfaceTable(NetBoxTable):
orderable=False,
verbose_name=_('L2VPN')
)
+ tunnel = tables.Column(
+ accessor=tables.A('tunnel_termination__tunnel'),
+ linkify=True,
+ orderable=False,
+ verbose_name=_('Tunnel')
+ )
untagged_vlan = tables.Column(
verbose_name=_('Untagged VLAN'),
linkify=True
@@ -646,7 +652,8 @@ class Meta(DeviceComponentTable.Meta):
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn',
- 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'inventory_items', 'created', 'last_updated',
+ 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'inventory_items', 'created',
+ 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
@@ -682,8 +689,8 @@ class Meta(DeviceComponentTable.Meta):
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
- 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups',
- 'untagged_vlan', 'tagged_vlans', 'actions',
+ 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses',
+ 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index e0f38afefe3..a24f9ea6d34 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -359,6 +359,16 @@
{% endif %}
+{% elif record.type == 'virtual' %}
+ {% if perms.vpn.add_tunnel and not record.tunnel_termination %}
+
+
+
+ {% elif perms.vpn.delete_tunneltermination and record.tunnel_termination %}
+
+
+
+ {% endif %}
{% elif record.is_wired and perms.dcim.add_cable %}
diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py
index 4e71ca193ce..cfbe82f14f2 100644
--- a/netbox/netbox/api/views.py
+++ b/netbox/netbox/api/views.py
@@ -39,6 +39,7 @@ def get(self, request, format=None):
'tenancy': reverse('tenancy-api:api-root', request=request, format=format),
'users': reverse('users-api:api-root', request=request, format=format),
'virtualization': reverse('virtualization-api:api-root', request=request, format=format),
+ 'vpn': reverse('vpn-api:api-root', request=request, format=format),
'wireless': reverse('wireless-api:api-root', request=request, format=format),
})
diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py
index 7224f3c38b6..021d6d902cc 100644
--- a/netbox/netbox/graphql/schema.py
+++ b/netbox/netbox/graphql/schema.py
@@ -9,6 +9,7 @@
from tenancy.graphql.schema import TenancyQuery
from users.graphql.schema import UsersQuery
from virtualization.graphql.schema import VirtualizationQuery
+from vpn.graphql.schema import VPNQuery
from wireless.graphql.schema import WirelessQuery
@@ -21,6 +22,7 @@ class Query(
IPAMQuery,
TenancyQuery,
VirtualizationQuery,
+ VPNQuery,
WirelessQuery,
*registry['plugins']['graphql_schemas'], # Append plugin schemas
graphene.ObjectType
diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py
index 961fd2035ac..6a9f2fa0815 100644
--- a/netbox/netbox/navigation/menu.py
+++ b/netbox/netbox/navigation/menu.py
@@ -195,17 +195,34 @@
),
)
-OVERLAY_MENU = Menu(
- label=_('Overlay'),
+VPN_MENU = Menu(
+ label=_('VPN'),
icon_class='mdi mdi-graph-outline',
groups=(
MenuGroup(
- label='L2VPNs',
+ label=_('Tunnels'),
+ items=(
+ get_model_item('vpn', 'tunnel', _('Tunnels')),
+ get_model_item('vpn', 'tunneltermination', _('Tunnel Terminations')),
+ ),
+ ),
+ MenuGroup(
+ label=_('L2VPNs'),
items=(
get_model_item('ipam', 'l2vpn', _('L2VPNs')),
get_model_item('ipam', 'l2vpntermination', _('Terminations')),
),
),
+ MenuGroup(
+ label=_('Security'),
+ items=(
+ get_model_item('vpn', 'ikeproposal', _('IKE Proposals')),
+ get_model_item('vpn', 'ikepolicy', _('IKE Policies')),
+ get_model_item('vpn', 'ipsecproposal', _('IPSec Proposals')),
+ get_model_item('vpn', 'ipsecpolicy', _('IPSec Policies')),
+ get_model_item('vpn', 'ipsecprofile', _('IPSec Profiles')),
+ ),
+ ),
),
)
@@ -443,7 +460,7 @@
CONNECTIONS_MENU,
WIRELESS_MENU,
IPAM_MENU,
- OVERLAY_MENU,
+ VPN_MENU,
VIRTUALIZATION_MENU,
CIRCUITS_MENU,
POWER_MENU,
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index 465389a1129..ce8ab5876fd 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -379,6 +379,7 @@ def _setting(name, default=None):
'users',
'utilities',
'virtualization',
+ 'vpn',
'wireless',
'django_rq', # Must come after extras to allow overriding management commands
'drf_spectacular',
diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py
index 6955426a8df..9843589113b 100644
--- a/netbox/netbox/urls.py
+++ b/netbox/netbox/urls.py
@@ -33,6 +33,7 @@
path('tenancy/', include('tenancy.urls')),
path('users/', include('users.urls')),
path('virtualization/', include('virtualization.urls')),
+ path('vpn/', include('vpn.urls')),
path('wireless/', include('wireless.urls')),
# Current user views
@@ -51,6 +52,7 @@
path('api/tenancy/', include('tenancy.api.urls')),
path('api/users/', include('users.api.urls')),
path('api/virtualization/', include('virtualization.api.urls')),
+ path('api/vpn/', include('vpn.api.urls')),
path('api/wireless/', include('wireless.api.urls')),
path('api/status/', StatusView.as_view(), name='api-status'),
diff --git a/netbox/templates/vpn/ikepolicy.html b/netbox/templates/vpn/ikepolicy.html
new file mode 100644
index 00000000000..559ba6d17bf
--- /dev/null
+++ b/netbox/templates/vpn/ikepolicy.html
@@ -0,0 +1,67 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
+ {% plugin_full_width_page object %}
+
+
+{% endblock %}
diff --git a/netbox/templates/vpn/ipsecprofile.html b/netbox/templates/vpn/ipsecprofile.html
new file mode 100644
index 00000000000..08fa3074ee9
--- /dev/null
+++ b/netbox/templates/vpn/ipsecprofile.html
@@ -0,0 +1,112 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
+
+
+
+ {% trans "Name" %} |
+ {{ object.name }} |
+
+
+ {% trans "Description" %} |
+ {{ object.description|placeholder }} |
+
+
+ {% trans "Mode" %} |
+ {{ object.get_mode_display }} |
+
+
+
+
+ {% include 'inc/panels/tags.html' %}
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/comments.html' %}
+ {% plugin_left_page object %}
+
+
+
+
+
+
+
+ {% trans "Name" %} |
+ {{ object.ike_policy|linkify }} |
+
+
+ {% trans "Description" %} |
+ {{ object.ike_policy.description|placeholder }} |
+
+
+ {% trans "Version" %} |
+ {{ object.ike_policy.get_version_display }} |
+
+
+ {% trans "Mode" %} |
+ {{ object.ike_policy.get_mode_display }} |
+
+
+ {% trans "Proposals" %} |
+
+
+ {% for proposal in object.ike_policy.proposals.all %}
+ -
+ {{ proposal }}
+
+ {% endfor %}
+
+ |
+
+
+ {% trans "Pre-Shared Key" %} |
+ {% checkmark object.ike_policy.preshared_key %} |
+
+
+
+
+
+
+
+
+
+ {% trans "Name" %} |
+ {{ object.ipsec_policy|linkify }} |
+
+
+ {% trans "Description" %} |
+ {{ object.ipsec_policy.description|placeholder }} |
+
+
+ {% trans "Proposals" %} |
+
+
+ {% for proposal in object.ipsec_policy.proposals.all %}
+ -
+ {{ proposal }}
+
+ {% endfor %}
+
+ |
+
+
+ {% trans "PFS Group" %} |
+ {{ object.ipsec_policy.get_pfs_group_display }} |
+
+
+
+
+ {% plugin_right_page object %}
+
+
+
+
+ {% plugin_full_width_page object %}
+
+
+{% endblock %}
diff --git a/netbox/templates/vpn/ipsecproposal.html b/netbox/templates/vpn/ipsecproposal.html
new file mode 100644
index 00000000000..7425eef4345
--- /dev/null
+++ b/netbox/templates/vpn/ipsecproposal.html
@@ -0,0 +1,59 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
+
+
+
+ {% trans "Name" %} |
+ {{ object.name }} |
+
+
+ {% trans "Description" %} |
+ {{ object.description|placeholder }} |
+
+
+ {% trans "Encryption algorithm" %} |
+ {{ object.get_encryption_algorithm_display }} |
+
+
+ {% trans "Authentication algorithm" %} |
+ {{ object.get_authentication_algorithm_display }} |
+
+
+ {% trans "SA lifetime (seconds)" %} |
+ {{ object.sa_lifetime_seconds|placeholder }} |
+
+
+ {% trans "SA lifetime (KB)" %} |
+ {{ object.sa_lifetime_data|placeholder }} |
+
+
+ {% trans "IPSec Policies" %} |
+
+ {{ object.ipsec_policies.count }}
+ |
+
+
+
+
+ {% plugin_left_page object %}
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/tags.html' %}
+ {% plugin_right_page object %}
+
+
+
+
+ {% plugin_full_width_page object %}
+
+
+{% endblock %}
diff --git a/netbox/templates/vpn/tunnel.html b/netbox/templates/vpn/tunnel.html
new file mode 100644
index 00000000000..544ffadae32
--- /dev/null
+++ b/netbox/templates/vpn/tunnel.html
@@ -0,0 +1,85 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+{% load i18n %}
+
+{% block extra_controls %}
+ {% if perms.vpn.add_tunneltermination %}
+
+ {% trans "Add Termination" %}
+
+ {% endif %}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+ {% trans "Name" %} |
+ {{ object.name }} |
+
+
+ {% trans "Status" %} |
+ {% badge object.get_status_display bg_color=object.get_status_color %} |
+
+
+ {% trans "Description" %} |
+ {{ object.description|placeholder }} |
+
+
+ {% trans "Encapsulation" %} |
+ {{ object.get_encapsulation_display }} |
+
+
+ {% trans "IPSec profile" %} |
+ {{ object.ipsec_profile|linkify|placeholder }} |
+
+
+ {% trans "Tunnel ID" %} |
+ {{ object.tunnel_id|placeholder }} |
+
+
+ {% trans "Tenant" %} |
+
+ {% if object.tenant.group %}
+ {{ object.tenant.group|linkify }} /
+ {% endif %}
+ {{ object.tenant|linkify|placeholder }}
+ |
+
+
+
+
+ {% plugin_left_page object %}
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/tags.html' %}
+ {% include 'inc/panels/comments.html' %}
+ {% plugin_right_page object %}
+
+
+
+
+
+
+
+ {% if perms.vpn.add_tunneltermination %}
+
+ {% endif %}
+
+ {% plugin_full_width_page object %}
+
+
+{% endblock %}
diff --git a/netbox/templates/vpn/tunneltermination.html b/netbox/templates/vpn/tunneltermination.html
new file mode 100644
index 00000000000..6f4e83ce071
--- /dev/null
+++ b/netbox/templates/vpn/tunneltermination.html
@@ -0,0 +1,62 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
+
+
+
+ {% trans "Tunnel" %} |
+ {{ object.tunnel|linkify }} |
+
+
+ {% trans "Role" %} |
+ {% badge object.get_role_display bg_color=object.get_role_color %} |
+
+
+
+ {% if object.termination.device %}
+ {% trans "Device" %}
+ {% elif object.termination.virtual_machine %}
+ {% trans "Virtual Machine" %}
+ {% endif %}
+ |
+ {{ object.termination.parent_object|linkify }} |
+
+
+ {% trans "Interface" %} |
+ {{ object.termination|linkify }} |
+
+
+ {% trans "Outside IP" %} |
+ {{ object.outside_ip|linkify|placeholder }} |
+
+
+
+
+ {% plugin_left_page object %}
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/tags.html' %}
+ {% plugin_right_page object %}
+
+
+
+
+
+ {% plugin_full_width_page object %}
+
+
+{% endblock %}
diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py
index eb6c2a8b0dd..ac7489f859a 100644
--- a/netbox/virtualization/models/virtualmachines.py
+++ b/netbox/virtualization/models/virtualmachines.py
@@ -291,6 +291,12 @@ class VMInterface(NetBoxModel, BaseInterface, TrackingModelMixin):
object_id_field='interface_id',
related_query_name='+'
)
+ tunnel_terminations = GenericRelation(
+ to='vpn.TunnelTermination',
+ content_type_field='termination_type',
+ object_id_field='termination_id',
+ related_query_name='vminterface',
+ )
l2vpn_terminations = GenericRelation(
to='ipam.L2VPNTermination',
content_type_field='assigned_object_type',
diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py
index f8473df1e8a..193176ada5f 100644
--- a/netbox/virtualization/tables/virtualmachines.py
+++ b/netbox/virtualization/tables/virtualmachines.py
@@ -126,7 +126,8 @@ class Meta(NetBoxTable.Meta):
model = VMInterface
fields = (
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
- 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
+ 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created',
+ 'last_updated',
)
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
@@ -149,7 +150,7 @@ class Meta(NetBoxTable.Meta):
model = VMInterface
fields = (
'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
- 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
+ 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
)
default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
row_attrs = {
diff --git a/netbox/vpn/__init__.py b/netbox/vpn/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/netbox/vpn/admin.py b/netbox/vpn/admin.py
new file mode 100644
index 00000000000..8c38f3f3dad
--- /dev/null
+++ b/netbox/vpn/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/netbox/vpn/api/__init__.py b/netbox/vpn/api/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/netbox/vpn/api/nested_serializers.py b/netbox/vpn/api/nested_serializers.py
new file mode 100644
index 00000000000..c9c92d30892
--- /dev/null
+++ b/netbox/vpn/api/nested_serializers.py
@@ -0,0 +1,84 @@
+from rest_framework import serializers
+
+from netbox.api.serializers import WritableNestedSerializer
+from vpn import models
+
+__all__ = (
+ 'NestedIKEPolicySerializer',
+ 'NestedIKEProposalSerializer',
+ 'NestedIPSecPolicySerializer',
+ 'NestedIPSecProfileSerializer',
+ 'NestedIPSecProposalSerializer',
+ 'NestedTunnelSerializer',
+ 'NestedTunnelTerminationSerializer',
+)
+
+
+class NestedTunnelSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:tunnel-detail'
+ )
+
+ class Meta:
+ model = models.Tunnel
+ fields = ('id', 'url', 'display', 'name')
+
+
+class NestedTunnelTerminationSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:tunneltermination-detail'
+ )
+
+ class Meta:
+ model = models.TunnelTermination
+ fields = ('id', 'url', 'display')
+
+
+class NestedIKEProposalSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:ikeproposal-detail'
+ )
+
+ class Meta:
+ model = models.IKEProposal
+ fields = ('id', 'url', 'display', 'name')
+
+
+class NestedIKEPolicySerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:ikepolicy-detail'
+ )
+
+ class Meta:
+ model = models.IKEPolicy
+ fields = ('id', 'url', 'display', 'name')
+
+
+class NestedIPSecProposalSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:ipsecproposal-detail'
+ )
+
+ class Meta:
+ model = models.IPSecProposal
+ fields = ('id', 'url', 'display', 'name')
+
+
+class NestedIPSecPolicySerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:ipsecpolicy-detail'
+ )
+
+ class Meta:
+ model = models.IPSecPolicy
+ fields = ('id', 'url', 'display', 'name')
+
+
+class NestedIPSecProfileSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:ipsecprofile-detail'
+ )
+
+ class Meta:
+ model = models.IPSecProfile
+ fields = ('id', 'url', 'display', 'name')
diff --git a/netbox/vpn/api/serializers.py b/netbox/vpn/api/serializers.py
new file mode 100644
index 00000000000..1a517fe5916
--- /dev/null
+++ b/netbox/vpn/api/serializers.py
@@ -0,0 +1,193 @@
+from django.contrib.contenttypes.models import ContentType
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from ipam.api.nested_serializers import NestedIPAddressSerializer
+from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
+from netbox.api.serializers import NetBoxModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
+from tenancy.api.nested_serializers import NestedTenantSerializer
+from utilities.api import get_serializer_for_model
+from vpn.choices import *
+from vpn.models import *
+from .nested_serializers import *
+
+__all__ = (
+ 'IKEPolicySerializer',
+ 'IKEProposalSerializer',
+ 'IPSecPolicySerializer',
+ 'IPSecProfileSerializer',
+ 'IPSecProposalSerializer',
+ 'TunnelSerializer',
+ 'TunnelTerminationSerializer',
+)
+
+
+class TunnelSerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:tunnel-detail'
+ )
+ status = ChoiceField(
+ choices=TunnelStatusChoices
+ )
+ encapsulation = ChoiceField(
+ choices=TunnelEncapsulationChoices
+ )
+ ipsec_profile = NestedIPSecProfileSerializer(
+ required=False,
+ allow_null=True
+ )
+ tenant = NestedTenantSerializer(
+ required=False,
+ allow_null=True
+ )
+
+ class Meta:
+ model = Tunnel
+ fields = (
+ 'id', 'url', 'display', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
+ 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+ )
+
+
+class TunnelTerminationSerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:tunneltermination-detail'
+ )
+ tunnel = NestedTunnelSerializer()
+ role = ChoiceField(
+ choices=TunnelTerminationRoleChoices
+ )
+ termination_type = ContentTypeField(
+ queryset=ContentType.objects.all()
+ )
+ termination = serializers.SerializerMethodField(
+ read_only=True
+ )
+ outside_ip = NestedIPAddressSerializer(
+ required=False,
+ allow_null=True
+ )
+
+ class Meta:
+ model = TunnelTermination
+ fields = (
+ 'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip',
+ 'tags', 'custom_fields', 'created', 'last_updated',
+ )
+
+ @extend_schema_field(serializers.JSONField(allow_null=True))
+ def get_termination(self, obj):
+ serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
+ context = {'request': self.context['request']}
+ return serializer(obj.termination, context=context).data
+
+
+class IKEProposalSerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:ikeproposal-detail'
+ )
+ authentication_method = ChoiceField(
+ choices=AuthenticationMethodChoices
+ )
+ encryption_algorithm = ChoiceField(
+ choices=EncryptionAlgorithmChoices
+ )
+ authentication_algorithm = ChoiceField(
+ choices=AuthenticationAlgorithmChoices
+ )
+ group = ChoiceField(
+ choices=DHGroupChoices
+ )
+
+ class Meta:
+ model = IKEProposal
+ fields = (
+ 'id', 'url', 'display', 'name', 'description', 'authentication_method', 'encryption_algorithm',
+ 'authentication_algorithm', 'group', 'sa_lifetime', 'tags', 'custom_fields', 'created', 'last_updated',
+ )
+
+
+class IKEPolicySerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:ikepolicy-detail'
+ )
+ version = ChoiceField(
+ choices=IKEVersionChoices
+ )
+ mode = ChoiceField(
+ choices=IKEModeChoices
+ )
+ proposals = SerializedPKRelatedField(
+ queryset=IKEProposal.objects.all(),
+ serializer=NestedIKEProposalSerializer,
+ required=False,
+ many=True
+ )
+
+ class Meta:
+ model = IKEPolicy
+ fields = (
+ 'id', 'url', 'display', 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'tags',
+ 'custom_fields', 'created', 'last_updated',
+ )
+
+
+class IPSecProposalSerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:ipsecproposal-detail'
+ )
+ encryption_algorithm = ChoiceField(
+ choices=EncryptionAlgorithmChoices
+ )
+ authentication_algorithm = ChoiceField(
+ choices=AuthenticationAlgorithmChoices
+ )
+
+ class Meta:
+ model = IPSecProposal
+ fields = (
+ 'id', 'url', 'display', 'name', 'description', 'encryption_algorithm', 'authentication_algorithm',
+ 'sa_lifetime_seconds', 'sa_lifetime_data', 'tags', 'custom_fields', 'created', 'last_updated',
+ )
+
+
+class IPSecPolicySerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:ipsecpolicy-detail'
+ )
+ proposals = SerializedPKRelatedField(
+ queryset=IPSecProposal.objects.all(),
+ serializer=NestedIPSecProposalSerializer,
+ required=False,
+ many=True
+ )
+ pfs_group = ChoiceField(
+ choices=DHGroupChoices,
+ required=False
+ )
+
+ class Meta:
+ model = IPSecPolicy
+ fields = (
+ 'id', 'url', 'display', 'name', 'description', 'proposals', 'pfs_group', 'tags', 'custom_fields', 'created',
+ 'last_updated',
+ )
+
+
+class IPSecProfileSerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='vpn-api:ipsecprofile-detail'
+ )
+ mode = ChoiceField(
+ choices=IPSecModeChoices
+ )
+ ike_policy = NestedIKEPolicySerializer()
+ ipsec_policy = NestedIPSecPolicySerializer()
+
+ class Meta:
+ model = IPSecProfile
+ fields = (
+ 'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags',
+ 'custom_fields', 'created', 'last_updated',
+ )
diff --git a/netbox/vpn/api/urls.py b/netbox/vpn/api/urls.py
new file mode 100644
index 00000000000..f646174d507
--- /dev/null
+++ b/netbox/vpn/api/urls.py
@@ -0,0 +1,15 @@
+from netbox.api.routers import NetBoxRouter
+from . import views
+
+router = NetBoxRouter()
+router.APIRootView = views.VPNRootView
+router.register('ike-policies', views.IKEPolicyViewSet)
+router.register('ike-proposals', views.IKEProposalViewSet)
+router.register('ipsec-policies', views.IPSecPolicyViewSet)
+router.register('ipsec-proposals', views.IPSecProposalViewSet)
+router.register('ipsec-profiles', views.IPSecProfileViewSet)
+router.register('tunnels', views.TunnelViewSet)
+router.register('tunnel-terminations', views.TunnelTerminationViewSet)
+
+app_name = 'vpn-api'
+urlpatterns = router.urls
diff --git a/netbox/vpn/api/views.py b/netbox/vpn/api/views.py
new file mode 100644
index 00000000000..c0ccab7ab74
--- /dev/null
+++ b/netbox/vpn/api/views.py
@@ -0,0 +1,74 @@
+from rest_framework.routers import APIRootView
+
+from netbox.api.viewsets import NetBoxModelViewSet
+from utilities.utils import count_related
+from vpn import filtersets
+from vpn.models import *
+from . import serializers
+
+__all__ = (
+ 'IKEPolicyViewSet',
+ 'IKEProposalViewSet',
+ 'IPSecPolicyViewSet',
+ 'IPSecProfileViewSet',
+ 'IPSecProposalViewSet',
+ 'TunnelTerminationViewSet',
+ 'TunnelViewSet',
+ 'VPNRootView',
+)
+
+
+class VPNRootView(APIRootView):
+ """
+ VPN API root view
+ """
+ def get_view_name(self):
+ return 'VPN'
+
+
+#
+# Viewsets
+#
+
+class TunnelViewSet(NetBoxModelViewSet):
+ queryset = Tunnel.objects.prefetch_related('ipsec_profile', 'tenant').annotate(
+ terminations_count=count_related(TunnelTermination, 'tunnel')
+ )
+ serializer_class = serializers.TunnelSerializer
+ filterset_class = filtersets.TunnelFilterSet
+
+
+class TunnelTerminationViewSet(NetBoxModelViewSet):
+ queryset = TunnelTermination.objects.prefetch_related('tunnel')
+ serializer_class = serializers.TunnelTerminationSerializer
+ filterset_class = filtersets.TunnelTerminationFilterSet
+
+
+class IKEProposalViewSet(NetBoxModelViewSet):
+ queryset = IKEProposal.objects.all()
+ serializer_class = serializers.IKEProposalSerializer
+ filterset_class = filtersets.IKEProposalFilterSet
+
+
+class IKEPolicyViewSet(NetBoxModelViewSet):
+ queryset = IKEPolicy.objects.all()
+ serializer_class = serializers.IKEPolicySerializer
+ filterset_class = filtersets.IKEPolicyFilterSet
+
+
+class IPSecProposalViewSet(NetBoxModelViewSet):
+ queryset = IPSecProposal.objects.all()
+ serializer_class = serializers.IPSecProposalSerializer
+ filterset_class = filtersets.IPSecProposalFilterSet
+
+
+class IPSecPolicyViewSet(NetBoxModelViewSet):
+ queryset = IPSecPolicy.objects.all()
+ serializer_class = serializers.IPSecPolicySerializer
+ filterset_class = filtersets.IPSecPolicyFilterSet
+
+
+class IPSecProfileViewSet(NetBoxModelViewSet):
+ queryset = IPSecProfile.objects.all()
+ serializer_class = serializers.IPSecProfileSerializer
+ filterset_class = filtersets.IPSecProfileFilterSet
diff --git a/netbox/vpn/apps.py b/netbox/vpn/apps.py
new file mode 100644
index 00000000000..2254befd3ac
--- /dev/null
+++ b/netbox/vpn/apps.py
@@ -0,0 +1,9 @@
+from django.apps import AppConfig
+
+
+class VPNConfig(AppConfig):
+ name = 'vpn'
+ verbose_name = 'VPN'
+
+ def ready(self):
+ from . import search
diff --git a/netbox/vpn/choices.py b/netbox/vpn/choices.py
new file mode 100644
index 00000000000..a932c5055e8
--- /dev/null
+++ b/netbox/vpn/choices.py
@@ -0,0 +1,201 @@
+from django.utils.translation import gettext_lazy as _
+
+from utilities.choices import ChoiceSet
+
+
+#
+# Tunnels
+#
+
+class TunnelStatusChoices(ChoiceSet):
+ key = 'Tunnel.status'
+
+ STATUS_PLANNED = 'planned'
+ STATUS_ACTIVE = 'active'
+ STATUS_DISABLED = 'disabled'
+
+ CHOICES = [
+ (STATUS_PLANNED, _('Planned'), 'cyan'),
+ (STATUS_ACTIVE, _('Active'), 'green'),
+ (STATUS_DISABLED, _('Disabled'), 'red'),
+ ]
+
+
+class TunnelEncapsulationChoices(ChoiceSet):
+ ENCAP_GRE = 'gre'
+ ENCAP_IP_IP = 'ip-ip'
+ ENCAP_IPSEC_TRANSPORT = 'ipsec-transport'
+ ENCAP_IPSEC_TUNNEL = 'ipsec-tunnel'
+
+ CHOICES = [
+ (ENCAP_IPSEC_TRANSPORT, _('IPsec - Transport')),
+ (ENCAP_IPSEC_TUNNEL, _('IPsec - Tunnel')),
+ (ENCAP_IP_IP, _('IP-in-IP')),
+ (ENCAP_GRE, _('GRE')),
+ ]
+
+
+class TunnelTerminationTypeChoices(ChoiceSet):
+ # For TunnelCreateForm
+ TYPE_DEVICE = 'dcim.device'
+ TYPE_VIRUTALMACHINE = 'virtualization.virtualmachine'
+
+ CHOICES = (
+ (TYPE_DEVICE, _('Device')),
+ (TYPE_VIRUTALMACHINE, _('Virtual Machine')),
+ )
+
+
+class TunnelTerminationRoleChoices(ChoiceSet):
+ ROLE_PEER = 'peer'
+ ROLE_HUB = 'hub'
+ ROLE_SPOKE = 'spoke'
+
+ CHOICES = [
+ (ROLE_PEER, _('Peer'), 'green'),
+ (ROLE_HUB, _('Hub'), 'blue'),
+ (ROLE_SPOKE, _('Spoke'), 'orange'),
+ ]
+
+
+#
+# Crypto
+#
+
+class IKEVersionChoices(ChoiceSet):
+ VERSION_1 = 1
+ VERSION_2 = 2
+
+ CHOICES = (
+ (VERSION_1, 'IKEv1'),
+ (VERSION_2, 'IKEv2'),
+ )
+
+
+class IKEModeChoices(ChoiceSet):
+ AGGRESSIVE = 'aggressive'
+ MAIN = 'main'
+
+ CHOICES = (
+ (AGGRESSIVE, _('Aggressive')),
+ (MAIN, _('Main')),
+ )
+
+
+class AuthenticationMethodChoices(ChoiceSet):
+ PRESHARED_KEYS = 'preshared-keys'
+ CERTIFICATES = 'certificates'
+ RSA_SIGNATURES = 'rsa-signatures'
+ DSA_SIGNATURES = 'dsa-signatures'
+
+ CHOICES = (
+ (PRESHARED_KEYS, _('Pre-shared keys')),
+ (CERTIFICATES, _('Certificates')),
+ (RSA_SIGNATURES, _('RSA signatures')),
+ (DSA_SIGNATURES, _('DSA signatures')),
+ )
+
+
+class IPSecModeChoices(ChoiceSet):
+ ESP = 'esp'
+ AH = 'ah'
+
+ CHOICES = (
+ (ESP, 'ESP'),
+ (AH, 'AH'),
+ )
+
+
+class EncryptionAlgorithmChoices(ChoiceSet):
+ ENCRYPTION_AES128_CBC = 'aes-128-cbc'
+ ENCRYPTION_AES128_GCM = 'aes-128-gcm'
+ ENCRYPTION_AES192_CBC = 'aes-192-cbc'
+ ENCRYPTION_AES192_GCM = 'aes-192-gcm'
+ ENCRYPTION_AES256_CBC = 'aes-256-cbc'
+ ENCRYPTION_AES256_GCM = 'aes-256-gcm'
+ ENCRYPTION_3DES = '3des-cbc'
+ ENCRYPTION_DES = 'des-cbc'
+
+ CHOICES = (
+ (ENCRYPTION_AES128_CBC, '128-bit AES (CBC)'),
+ (ENCRYPTION_AES128_GCM, '128-bit AES (GCM)'),
+ (ENCRYPTION_AES192_CBC, '192-bit AES (CBC)'),
+ (ENCRYPTION_AES192_GCM, '192-bit AES (GCM)'),
+ (ENCRYPTION_AES256_CBC, '256-bit AES (CBC)'),
+ (ENCRYPTION_AES256_GCM, '256-bit AES (GCM)'),
+ (ENCRYPTION_3DES, '3DES'),
+ (ENCRYPTION_3DES, 'DES'),
+ )
+
+
+class AuthenticationAlgorithmChoices(ChoiceSet):
+ AUTH_HMAC_SHA1 = 'hmac-sha1'
+ AUTH_HMAC_SHA256 = 'hmac-sha256'
+ AUTH_HMAC_SHA384 = 'hmac-sha384'
+ AUTH_HMAC_SHA512 = 'hmac-sha512'
+ AUTH_HMAC_MD5 = 'hmac-md5'
+
+ CHOICES = (
+ (AUTH_HMAC_SHA1, 'SHA-1 HMAC'),
+ (AUTH_HMAC_SHA256, 'SHA-256 HMAC'),
+ (AUTH_HMAC_SHA384, 'SHA-384 HMAC'),
+ (AUTH_HMAC_SHA512, 'SHA-512 HMAC'),
+ (AUTH_HMAC_MD5, 'MD5 HMAC'),
+ )
+
+
+class DHGroupChoices(ChoiceSet):
+ # https://www.iana.org/assignments/ikev2-parameters/ikev2-parameters.xhtml#ikev2-parameters-8
+ GROUP_1 = 1 # 768-bit MODP
+ GROUP_2 = 2 # 1024-but MODP
+ # Groups 3-4 reserved
+ GROUP_5 = 5 # 1536-bit MODP
+ # Groups 6-13 unassigned
+ GROUP_14 = 14 # 2048-bit MODP
+ GROUP_15 = 15 # 3072-bit MODP
+ GROUP_16 = 16 # 4096-bit MODP
+ GROUP_17 = 17 # 6144-bit MODP
+ GROUP_18 = 18 # 8192-bit MODP
+ GROUP_19 = 19 # 256-bit random ECP
+ GROUP_20 = 20 # 384-bit random ECP
+ GROUP_21 = 21 # 521-bit random ECP (521 is not a typo)
+ GROUP_22 = 22 # 1024-bit MODP w/160-bit prime
+ GROUP_23 = 23 # 2048-bit MODP w/224-bit prime
+ GROUP_24 = 24 # 2048-bit MODP w/256-bit prime
+ GROUP_25 = 25 # 192-bit ECP
+ GROUP_26 = 26 # 224-bit ECP
+ GROUP_27 = 27 # brainpoolP224r1
+ GROUP_28 = 28 # brainpoolP256r1
+ GROUP_29 = 29 # brainpoolP384r1
+ GROUP_30 = 30 # brainpoolP512r1
+ GROUP_31 = 31 # Curve25519
+ GROUP_32 = 32 # Curve448
+ GROUP_33 = 33 # GOST3410_2012_256
+ GROUP_34 = 34 # GOST3410_2012_512
+
+ CHOICES = (
+ # Strings are formatted in this manner to optimize translations
+ (GROUP_1, _('Group {n}').format(n=1)),
+ (GROUP_2, _('Group {n}').format(n=2)),
+ (GROUP_5, _('Group {n}').format(n=5)),
+ (GROUP_14, _('Group {n}').format(n=14)),
+ (GROUP_16, _('Group {n}').format(n=16)),
+ (GROUP_17, _('Group {n}').format(n=17)),
+ (GROUP_18, _('Group {n}').format(n=18)),
+ (GROUP_19, _('Group {n}').format(n=19)),
+ (GROUP_20, _('Group {n}').format(n=20)),
+ (GROUP_21, _('Group {n}').format(n=21)),
+ (GROUP_22, _('Group {n}').format(n=22)),
+ (GROUP_23, _('Group {n}').format(n=23)),
+ (GROUP_24, _('Group {n}').format(n=24)),
+ (GROUP_25, _('Group {n}').format(n=25)),
+ (GROUP_26, _('Group {n}').format(n=26)),
+ (GROUP_27, _('Group {n}').format(n=27)),
+ (GROUP_28, _('Group {n}').format(n=28)),
+ (GROUP_29, _('Group {n}').format(n=29)),
+ (GROUP_30, _('Group {n}').format(n=30)),
+ (GROUP_31, _('Group {n}').format(n=31)),
+ (GROUP_32, _('Group {n}').format(n=32)),
+ (GROUP_33, _('Group {n}').format(n=33)),
+ (GROUP_34, _('Group {n}').format(n=34)),
+ )
diff --git a/netbox/vpn/filtersets.py b/netbox/vpn/filtersets.py
new file mode 100644
index 00000000000..c0bd140c326
--- /dev/null
+++ b/netbox/vpn/filtersets.py
@@ -0,0 +1,241 @@
+import django_filters
+from django.db.models import Q
+from django.utils.translation import gettext as _
+
+from dcim.models import Interface
+from ipam.models import IPAddress
+from netbox.filtersets import NetBoxModelFilterSet
+from tenancy.filtersets import TenancyFilterSet
+from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
+from virtualization.models import VMInterface
+from .choices import *
+from .models import *
+
+__all__ = (
+ 'IKEPolicyFilterSet',
+ 'IKEProposalFilterSet',
+ 'IPSecPolicyFilterSet',
+ 'IPSecProfileFilterSet',
+ 'IPSecProposalFilterSet',
+ 'TunnelFilterSet',
+ 'TunnelTerminationFilterSet',
+)
+
+
+class TunnelFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+ status = django_filters.MultipleChoiceFilter(
+ choices=TunnelStatusChoices
+ )
+ encapsulation = django_filters.MultipleChoiceFilter(
+ choices=TunnelEncapsulationChoices
+ )
+ ipsec_profile_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=IPSecProfile.objects.all(),
+ label=_('IPSec profile (ID)'),
+ )
+ ipsec_profile = django_filters.ModelMultipleChoiceFilter(
+ field_name='ipsec_profile__name',
+ queryset=IPSecProfile.objects.all(),
+ to_field_name='name',
+ label=_('IPSec profile (name)'),
+ )
+
+ class Meta:
+ model = Tunnel
+ fields = ['id', 'name', 'tunnel_id']
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(name__icontains=value) |
+ Q(description__icontains=value) |
+ Q(comments__icontains=value)
+ )
+
+
+class TunnelTerminationFilterSet(NetBoxModelFilterSet):
+ tunnel_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='tunnel',
+ queryset=Tunnel.objects.all(),
+ label=_('Tunnel (ID)'),
+ )
+ tunnel = django_filters.ModelMultipleChoiceFilter(
+ field_name='tunnel__name',
+ queryset=Tunnel.objects.all(),
+ to_field_name='name',
+ label=_('Tunnel (name)'),
+ )
+ role = django_filters.MultipleChoiceFilter(
+ choices=TunnelTerminationRoleChoices
+ )
+ termination_type = ContentTypeFilter()
+ interface = django_filters.ModelMultipleChoiceFilter(
+ field_name='interface__name',
+ queryset=Interface.objects.all(),
+ to_field_name='name',
+ label=_('Interface (name)'),
+ )
+ interface_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='interface',
+ queryset=Interface.objects.all(),
+ label=_('Interface (ID)'),
+ )
+ vminterface = django_filters.ModelMultipleChoiceFilter(
+ field_name='vminterface__name',
+ queryset=VMInterface.objects.all(),
+ to_field_name='name',
+ label=_('VM interface (name)'),
+ )
+ vminterface_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='vminterface',
+ queryset=VMInterface.objects.all(),
+ label=_('VM interface (ID)'),
+ )
+ outside_ip_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='outside_ip',
+ queryset=IPAddress.objects.all(),
+ label=_('Outside IP (ID)'),
+ )
+
+ class Meta:
+ model = TunnelTermination
+ fields = ['id']
+
+
+class IKEProposalFilterSet(NetBoxModelFilterSet):
+ authentication_method = django_filters.MultipleChoiceFilter(
+ choices=AuthenticationMethodChoices
+ )
+ encryption_algorithm = django_filters.MultipleChoiceFilter(
+ choices=EncryptionAlgorithmChoices
+ )
+ authentication_algorithm = django_filters.MultipleChoiceFilter(
+ choices=AuthenticationAlgorithmChoices
+ )
+ group = django_filters.MultipleChoiceFilter(
+ choices=DHGroupChoices
+ )
+
+ class Meta:
+ model = IKEProposal
+ fields = ['id', 'name', 'sa_lifetime']
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(name__icontains=value) |
+ Q(description__icontains=value)
+ )
+
+
+class IKEPolicyFilterSet(NetBoxModelFilterSet):
+ version = django_filters.MultipleChoiceFilter(
+ choices=IKEVersionChoices
+ )
+ mode = django_filters.MultipleChoiceFilter(
+ choices=IKEModeChoices
+ )
+ proposal_id = MultiValueNumberFilter(
+ field_name='proposals__id'
+ )
+ proposal = MultiValueCharFilter(
+ field_name='proposals__name'
+ )
+
+ class Meta:
+ model = IKEPolicy
+ fields = ['id', 'name', 'preshared_key']
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(name__icontains=value) |
+ Q(description__icontains=value)
+ )
+
+
+class IPSecProposalFilterSet(NetBoxModelFilterSet):
+ encryption_algorithm = django_filters.MultipleChoiceFilter(
+ choices=EncryptionAlgorithmChoices
+ )
+ authentication_algorithm = django_filters.MultipleChoiceFilter(
+ choices=AuthenticationAlgorithmChoices
+ )
+
+ class Meta:
+ model = IPSecProposal
+ fields = ['id', 'name', 'sa_lifetime_seconds', 'sa_lifetime_data']
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(name__icontains=value) |
+ Q(description__icontains=value)
+ )
+
+
+class IPSecPolicyFilterSet(NetBoxModelFilterSet):
+ pfs_group = django_filters.MultipleChoiceFilter(
+ choices=DHGroupChoices
+ )
+ proposal_id = MultiValueNumberFilter(
+ field_name='proposals__id'
+ )
+ proposal = MultiValueCharFilter(
+ field_name='proposals__name'
+ )
+
+ class Meta:
+ model = IPSecPolicy
+ fields = ['id', 'name']
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(name__icontains=value) |
+ Q(description__icontains=value)
+ )
+
+
+class IPSecProfileFilterSet(NetBoxModelFilterSet):
+ mode = django_filters.MultipleChoiceFilter(
+ choices=IPSecModeChoices
+ )
+ ike_policy_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=IKEPolicy.objects.all(),
+ label=_('IKE policy (ID)'),
+ )
+ ike_policy = django_filters.ModelMultipleChoiceFilter(
+ field_name='ike_policy__name',
+ queryset=IKEPolicy.objects.all(),
+ to_field_name='name',
+ label=_('IKE policy (name)'),
+ )
+ ipsec_policy_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=IPSecPolicy.objects.all(),
+ label=_('IPSec policy (ID)'),
+ )
+ ipsec_policy = django_filters.ModelMultipleChoiceFilter(
+ field_name='ipsec_policy__name',
+ queryset=IPSecPolicy.objects.all(),
+ to_field_name='name',
+ label=_('IPSec policy (name)'),
+ )
+
+ class Meta:
+ model = IPSecProfile
+ fields = ['id', 'name']
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(name__icontains=value) |
+ Q(description__icontains=value) |
+ Q(comments__icontains=value)
+ )
diff --git a/netbox/vpn/forms/__init__.py b/netbox/vpn/forms/__init__.py
new file mode 100644
index 00000000000..1499f98b281
--- /dev/null
+++ b/netbox/vpn/forms/__init__.py
@@ -0,0 +1,4 @@
+from .bulk_edit import *
+from .bulk_import import *
+from .filtersets import *
+from .model_forms import *
diff --git a/netbox/vpn/forms/bulk_edit.py b/netbox/vpn/forms/bulk_edit.py
new file mode 100644
index 00000000000..a7b097b5c94
--- /dev/null
+++ b/netbox/vpn/forms/bulk_edit.py
@@ -0,0 +1,243 @@
+from django import forms
+from django.utils.translation import gettext_lazy as _
+
+from netbox.forms import NetBoxModelBulkEditForm
+from tenancy.models import Tenant
+from utilities.forms import add_blank_choice
+from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from vpn.choices import *
+from vpn.models import *
+
+__all__ = (
+ 'IKEPolicyBulkEditForm',
+ 'IKEProposalBulkEditForm',
+ 'IPSecPolicyBulkEditForm',
+ 'IPSecProfileBulkEditForm',
+ 'IPSecProposalBulkEditForm',
+ 'TunnelBulkEditForm',
+ 'TunnelTerminationBulkEditForm',
+)
+
+
+class TunnelBulkEditForm(NetBoxModelBulkEditForm):
+ status = forms.ChoiceField(
+ label=_('Status'),
+ choices=add_blank_choice(TunnelStatusChoices),
+ required=False
+ )
+ encapsulation = forms.ChoiceField(
+ label=_('Encapsulation'),
+ choices=add_blank_choice(TunnelEncapsulationChoices),
+ required=False
+ )
+ ipsec_profile = DynamicModelMultipleChoiceField(
+ queryset=IPSecProfile.objects.all(),
+ label=_('IPSec profile'),
+ required=False
+ )
+ tenant = DynamicModelChoiceField(
+ label=_('Tenant'),
+ queryset=Tenant.objects.all(),
+ required=False
+ )
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+ tunnel_id = forms.IntegerField(
+ label=_('Tunnel ID'),
+ required=False
+ )
+ comments = CommentField()
+
+ model = Tunnel
+ fieldsets = (
+ (_('Tunnel'), ('status', 'encapsulation', 'tunnel_id', 'description')),
+ (_('Security'), ('ipsec_profile',)),
+ (_('Tenancy'), ('tenant',)),
+ )
+ nullable_fields = (
+ 'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments',
+ )
+
+
+class TunnelTerminationBulkEditForm(NetBoxModelBulkEditForm):
+ role = forms.ChoiceField(
+ label=_('Role'),
+ choices=add_blank_choice(TunnelTerminationRoleChoices),
+ required=False
+ )
+
+ model = TunnelTermination
+
+
+class IKEProposalBulkEditForm(NetBoxModelBulkEditForm):
+ authentication_method = forms.ChoiceField(
+ label=_('Authentication method'),
+ choices=add_blank_choice(AuthenticationMethodChoices),
+ required=False
+ )
+ encryption_algorithm = forms.ChoiceField(
+ label=_('Encryption algorithm'),
+ choices=add_blank_choice(EncryptionAlgorithmChoices),
+ required=False
+ )
+ authentication_algorithm = forms.ChoiceField(
+ label=_('Authentication algorithm'),
+ choices=add_blank_choice(AuthenticationAlgorithmChoices),
+ required=False
+ )
+ group = forms.ChoiceField(
+ label=_('Group'),
+ choices=add_blank_choice(DHGroupChoices),
+ required=False
+ )
+ sa_lifetime = forms.IntegerField(
+ label=_('SA lifetime'),
+ required=False
+ )
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+ comments = CommentField()
+
+ model = IKEProposal
+ fieldsets = (
+ (None, (
+ 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime',
+ 'description',
+ )),
+ )
+ nullable_fields = (
+ 'sa_lifetime', 'description', 'comments',
+ )
+
+
+class IKEPolicyBulkEditForm(NetBoxModelBulkEditForm):
+ version = forms.ChoiceField(
+ label=_('Version'),
+ choices=add_blank_choice(IKEVersionChoices),
+ required=False
+ )
+ mode = forms.ChoiceField(
+ label=_('Mode'),
+ choices=add_blank_choice(IKEModeChoices),
+ required=False
+ )
+ preshared_key = forms.CharField(
+ label=_('Pre-shared key'),
+ required=False
+ )
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+ comments = CommentField()
+
+ model = IKEPolicy
+ fieldsets = (
+ (None, (
+ 'version', 'mode', 'preshared_key', 'description',
+ )),
+ )
+ nullable_fields = (
+ 'preshared_key', 'description', 'comments',
+ )
+
+
+class IPSecProposalBulkEditForm(NetBoxModelBulkEditForm):
+ encryption_algorithm = forms.ChoiceField(
+ label=_('Encryption algorithm'),
+ choices=add_blank_choice(EncryptionAlgorithmChoices),
+ required=False
+ )
+ authentication_algorithm = forms.ChoiceField(
+ label=_('Authentication algorithm'),
+ choices=add_blank_choice(AuthenticationAlgorithmChoices),
+ required=False
+ )
+ sa_lifetime_seconds = forms.IntegerField(
+ label=_('SA lifetime (seconds)'),
+ required=False
+ )
+ sa_lifetime_data = forms.IntegerField(
+ label=_('SA lifetime (KB)'),
+ required=False
+ )
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+ comments = CommentField()
+
+ model = IPSecProposal
+ fieldsets = (
+ (None, (
+ 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data',
+ 'description',
+ )),
+ )
+ nullable_fields = (
+ 'sa_lifetime_seconds', 'sa_lifetime_data', 'description', 'comments',
+ )
+
+
+class IPSecPolicyBulkEditForm(NetBoxModelBulkEditForm):
+ pfs_group = forms.ChoiceField(
+ label=_('PFS group'),
+ choices=add_blank_choice(DHGroupChoices),
+ required=False
+ )
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+ comments = CommentField()
+
+ model = IPSecPolicy
+ fieldsets = (
+ (None, ('pfs_group', 'description',)),
+ )
+ nullable_fields = (
+ 'pfs_group', 'description', 'comments',
+ )
+
+
+class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm):
+ mode = forms.ChoiceField(
+ label=_('Mode'),
+ choices=add_blank_choice(IPSecModeChoices),
+ required=False
+ )
+ ike_policy = DynamicModelChoiceField(
+ label=_('IKE policy'),
+ queryset=IKEPolicy.objects.all(),
+ required=False
+ )
+ ipsec_policy = DynamicModelChoiceField(
+ label=_('IPSec policy'),
+ queryset=IPSecPolicy.objects.all(),
+ required=False
+ )
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+ comments = CommentField()
+
+ model = IPSecProfile
+ fieldsets = (
+ (_('Profile'), (
+ 'mode', 'ike_policy', 'ipsec_policy', 'description',
+ )),
+ )
+ nullable_fields = (
+ 'description', 'comments',
+ )
diff --git a/netbox/vpn/forms/bulk_import.py b/netbox/vpn/forms/bulk_import.py
new file mode 100644
index 00000000000..5b42cc761e4
--- /dev/null
+++ b/netbox/vpn/forms/bulk_import.py
@@ -0,0 +1,230 @@
+from django.utils.translation import gettext_lazy as _
+
+from dcim.models import Device, Interface
+from ipam.models import IPAddress
+from netbox.forms import NetBoxModelImportForm
+from tenancy.models import Tenant
+from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField
+from virtualization.models import VirtualMachine, VMInterface
+from vpn.choices import *
+from vpn.models import *
+
+__all__ = (
+ 'IKEPolicyImportForm',
+ 'IKEProposalImportForm',
+ 'IPSecPolicyImportForm',
+ 'IPSecProfileImportForm',
+ 'IPSecProposalImportForm',
+ 'TunnelImportForm',
+ 'TunnelTerminationImportForm',
+)
+
+
+class TunnelImportForm(NetBoxModelImportForm):
+ status = CSVChoiceField(
+ label=_('Status'),
+ choices=TunnelStatusChoices,
+ help_text=_('Operational status')
+ )
+ encapsulation = CSVChoiceField(
+ label=_('Encapsulation'),
+ choices=TunnelEncapsulationChoices,
+ help_text=_('Tunnel encapsulation')
+ )
+ ipsec_profile = CSVModelChoiceField(
+ label=_('IPSec profile'),
+ queryset=IPSecProfile.objects.all(),
+ required=False,
+ to_field_name='name'
+ )
+ tenant = CSVModelChoiceField(
+ label=_('Tenant'),
+ queryset=Tenant.objects.all(),
+ required=False,
+ to_field_name='name',
+ help_text=_('Assigned tenant')
+ )
+
+ class Meta:
+ model = Tunnel
+ fields = (
+ 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'description', 'comments',
+ 'tags',
+ )
+
+
+class TunnelTerminationImportForm(NetBoxModelImportForm):
+ tunnel = CSVModelChoiceField(
+ label=_('Tunnel'),
+ queryset=Tunnel.objects.all(),
+ to_field_name='name'
+ )
+ role = CSVChoiceField(
+ label=_('Role'),
+ choices=TunnelTerminationRoleChoices,
+ help_text=_('Operational role')
+ )
+ device = CSVModelChoiceField(
+ label=_('Device'),
+ queryset=Device.objects.all(),
+ required=False,
+ to_field_name='name',
+ help_text=_('Parent device of assigned interface')
+ )
+ virtual_machine = CSVModelChoiceField(
+ label=_('Virtual machine'),
+ queryset=VirtualMachine.objects.all(),
+ required=False,
+ to_field_name='name',
+ help_text=_('Parent VM of assigned interface')
+ )
+ termination = CSVModelChoiceField(
+ label=_('Termination'),
+ queryset=Interface.objects.none(), # Can also refer to VMInterface
+ required=False,
+ to_field_name='name',
+ help_text=_('Device or virtual machine interface')
+ )
+ outside_ip = CSVModelChoiceField(
+ label=_('Outside IP'),
+ queryset=IPAddress.objects.all(),
+ required=False,
+ to_field_name='name'
+ )
+
+ class Meta:
+ model = TunnelTermination
+ fields = (
+ 'tunnel', 'role', 'outside_ip', 'tags',
+ )
+
+ def __init__(self, data=None, *args, **kwargs):
+ super().__init__(data, *args, **kwargs)
+
+ if data:
+
+ # Limit termination queryset by assigned device/VM
+ if data.get('device'):
+ self.fields['termination'].queryset = Interface.objects.filter(
+ **{f"device__{self.fields['device'].to_field_name}": data['device']}
+ )
+ elif data.get('virtual_machine'):
+ self.fields['termination'].queryset = VMInterface.objects.filter(
+ **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
+ )
+
+ def save(self, *args, **kwargs):
+
+ # Assign termination object
+ if self.cleaned_data.get('termination'):
+ self.instance.termination = self.cleaned_data['termination']
+
+ return super().save(*args, **kwargs)
+
+
+class IKEProposalImportForm(NetBoxModelImportForm):
+ authentication_method = CSVChoiceField(
+ label=_('Authentication method'),
+ choices=AuthenticationMethodChoices
+ )
+ encryption_algorithm = CSVChoiceField(
+ label=_('Encryption algorithm'),
+ choices=EncryptionAlgorithmChoices
+ )
+ authentication_algorithm = CSVChoiceField(
+ label=_('Authentication algorithm'),
+ choices=AuthenticationAlgorithmChoices
+ )
+ group = CSVChoiceField(
+ label=_('Group'),
+ choices=DHGroupChoices
+ )
+
+ class Meta:
+ model = IKEProposal
+ fields = (
+ 'name', 'description', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm',
+ 'group', 'sa_lifetime', 'tags',
+ )
+
+
+class IKEPolicyImportForm(NetBoxModelImportForm):
+ version = CSVChoiceField(
+ label=_('Version'),
+ choices=IKEVersionChoices
+ )
+ mode = CSVChoiceField(
+ label=_('Mode'),
+ choices=IKEModeChoices
+ )
+ proposals = CSVModelMultipleChoiceField(
+ queryset=IKEProposal.objects.all(),
+ to_field_name='name',
+ help_text=_('IKE proposal(s)'),
+ )
+
+ class Meta:
+ model = IKEPolicy
+ fields = (
+ 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'tags',
+ )
+
+
+class IPSecProposalImportForm(NetBoxModelImportForm):
+ encryption_algorithm = CSVChoiceField(
+ label=_('Encryption algorithm'),
+ choices=EncryptionAlgorithmChoices
+ )
+ authentication_algorithm = CSVChoiceField(
+ label=_('Authentication algorithm'),
+ choices=AuthenticationAlgorithmChoices
+ )
+
+ class Meta:
+ model = IPSecProposal
+ fields = (
+ 'name', 'description', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds',
+ 'sa_lifetime_data', 'tags',
+ )
+
+
+class IPSecPolicyImportForm(NetBoxModelImportForm):
+ pfs_group = CSVChoiceField(
+ label=_('Diffie-Hellman group for Perfect Forward Secrecy'),
+ choices=DHGroupChoices
+ )
+ proposals = CSVModelMultipleChoiceField(
+ queryset=IPSecProposal.objects.all(),
+ to_field_name='name',
+ help_text=_('IPSec proposal(s)'),
+ )
+
+ class Meta:
+ model = IPSecPolicy
+ fields = (
+ 'name', 'description', 'proposals', 'pfs_group', 'tags',
+ )
+
+
+class IPSecProfileImportForm(NetBoxModelImportForm):
+ mode = CSVChoiceField(
+ label=_('Mode'),
+ choices=IPSecModeChoices,
+ help_text=_('IPSec protocol')
+ )
+ ike_policy = CSVModelChoiceField(
+ label=_('IKE policy'),
+ queryset=IKEPolicy.objects.all(),
+ to_field_name='name'
+ )
+ ipsec_policy = CSVModelChoiceField(
+ label=_('IPSec policy'),
+ queryset=IPSecPolicy.objects.all(),
+ to_field_name='name'
+ )
+
+ class Meta:
+ model = IPSecProfile
+ fields = (
+ 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags',
+ )
diff --git a/netbox/vpn/forms/filtersets.py b/netbox/vpn/forms/filtersets.py
new file mode 100644
index 00000000000..ec146919a70
--- /dev/null
+++ b/netbox/vpn/forms/filtersets.py
@@ -0,0 +1,182 @@
+from django import forms
+from django.utils.translation import gettext as _
+
+from netbox.forms import NetBoxModelFilterSetForm
+from tenancy.forms import TenancyFilterForm
+from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
+from vpn.choices import *
+from vpn.models import *
+
+__all__ = (
+ 'IKEPolicyFilterForm',
+ 'IKEProposalFilterForm',
+ 'IPSecPolicyFilterForm',
+ 'IPSecProfileFilterForm',
+ 'IPSecProposalFilterForm',
+ 'TunnelFilterForm',
+ 'TunnelTerminationFilterForm',
+)
+
+
+class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+ model = Tunnel
+ fieldsets = (
+ (None, ('q', 'filter_id', 'tag')),
+ (_('Tunnel'), ('status', 'encapsulation', 'tunnel_id')),
+ (_('Security'), ('ipsec_profile_id',)),
+ (_('Tenancy'), ('tenant_group_id', 'tenant_id')),
+ )
+ status = forms.MultipleChoiceField(
+ label=_('Status'),
+ choices=TunnelStatusChoices,
+ required=False
+ )
+ encapsulation = forms.MultipleChoiceField(
+ label=_('Encapsulation'),
+ choices=TunnelEncapsulationChoices,
+ required=False
+ )
+ ipsec_profile_id = DynamicModelMultipleChoiceField(
+ queryset=IPSecProfile.objects.all(),
+ required=False,
+ label=_('IPSec profile')
+ )
+ tunnel_id = forms.IntegerField(
+ required=False,
+ label=_('Tunnel ID')
+ )
+ tag = TagFilterField(model)
+
+
+class TunnelTerminationFilterForm(NetBoxModelFilterSetForm):
+ model = TunnelTermination
+ fieldsets = (
+ (None, ('q', 'filter_id', 'tag')),
+ (_('Termination'), ('tunnel_id', 'role')),
+ )
+ tunnel_id = DynamicModelMultipleChoiceField(
+ queryset=Tunnel.objects.all(),
+ required=False,
+ label=_('Tunnel')
+ )
+ role = forms.MultipleChoiceField(
+ label=_('Role'),
+ choices=TunnelTerminationRoleChoices,
+ required=False
+ )
+ tag = TagFilterField(model)
+
+
+class IKEProposalFilterForm(NetBoxModelFilterSetForm):
+ model = IKEProposal
+ fieldsets = (
+ (None, ('q', 'filter_id', 'tag')),
+ (_('Parameters'), ('authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group')),
+ )
+ authentication_method = forms.MultipleChoiceField(
+ label=_('Authentication method'),
+ choices=AuthenticationMethodChoices,
+ required=False
+ )
+ encryption_algorithm = forms.MultipleChoiceField(
+ label=_('Encryption algorithm'),
+ choices=EncryptionAlgorithmChoices,
+ required=False
+ )
+ authentication_algorithm = forms.MultipleChoiceField(
+ label=_('Authentication algorithm'),
+ choices=AuthenticationAlgorithmChoices,
+ required=False
+ )
+ group = forms.MultipleChoiceField(
+ label=_('Group'),
+ choices=DHGroupChoices,
+ required=False
+ )
+ tag = TagFilterField(model)
+
+
+class IKEPolicyFilterForm(NetBoxModelFilterSetForm):
+ model = IKEPolicy
+ fieldsets = (
+ (None, ('q', 'filter_id', 'tag')),
+ (_('Parameters'), ('version', 'mode', 'proposal_id')),
+ )
+ version = forms.MultipleChoiceField(
+ label=_('IKE version'),
+ choices=IKEVersionChoices,
+ required=False
+ )
+ mode = forms.MultipleChoiceField(
+ label=_('Mode'),
+ choices=IKEModeChoices,
+ required=False
+ )
+ proposal_id = DynamicModelMultipleChoiceField(
+ queryset=IKEProposal.objects.all(),
+ required=False,
+ label=_('Proposal')
+ )
+ tag = TagFilterField(model)
+
+
+class IPSecProposalFilterForm(NetBoxModelFilterSetForm):
+ model = IPSecProposal
+ fieldsets = (
+ (None, ('q', 'filter_id', 'tag')),
+ (_('Parameters'), ('encryption_algorithm', 'authentication_algorithm')),
+ )
+ encryption_algorithm = forms.MultipleChoiceField(
+ label=_('Encryption algorithm'),
+ choices=EncryptionAlgorithmChoices,
+ required=False
+ )
+ authentication_algorithm = forms.MultipleChoiceField(
+ label=_('Authentication algorithm'),
+ choices=AuthenticationAlgorithmChoices,
+ required=False
+ )
+ tag = TagFilterField(model)
+
+
+class IPSecPolicyFilterForm(NetBoxModelFilterSetForm):
+ model = IPSecPolicy
+ fieldsets = (
+ (None, ('q', 'filter_id', 'tag')),
+ (_('Parameters'), ('proposal_id', 'pfs_group')),
+ )
+ proposal_id = DynamicModelMultipleChoiceField(
+ queryset=IKEProposal.objects.all(),
+ required=False,
+ label=_('Proposal')
+ )
+ pfs_group = forms.MultipleChoiceField(
+ label=_('Mode'),
+ choices=DHGroupChoices,
+ required=False
+ )
+ tag = TagFilterField(model)
+
+
+class IPSecProfileFilterForm(NetBoxModelFilterSetForm):
+ model = IPSecProfile
+ fieldsets = (
+ (None, ('q', 'filter_id', 'tag')),
+ (_('Profile'), ('mode', 'ike_policy_id', 'ipsec_policy_id')),
+ )
+ mode = forms.MultipleChoiceField(
+ label=_('Mode'),
+ choices=IPSecModeChoices,
+ required=False
+ )
+ ike_policy_id = DynamicModelMultipleChoiceField(
+ queryset=IKEPolicy.objects.all(),
+ required=False,
+ label=_('IKE policy')
+ )
+ ipsec_policy_id = DynamicModelMultipleChoiceField(
+ queryset=IPSecPolicy.objects.all(),
+ required=False,
+ label=_('IPSec policy')
+ )
+ tag = TagFilterField(model)
diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py
new file mode 100644
index 00000000000..35fa2cad3ae
--- /dev/null
+++ b/netbox/vpn/forms/model_forms.py
@@ -0,0 +1,357 @@
+from django import forms
+from django.utils.translation import gettext_lazy as _
+
+from dcim.models import Device, Interface
+from ipam.models import IPAddress
+from netbox.forms import NetBoxModelForm
+from tenancy.forms import TenancyForm
+from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.utils import add_blank_choice
+from utilities.forms.widgets import HTMXSelect
+from virtualization.models import VirtualMachine, VMInterface
+from vpn.choices import *
+from vpn.models import *
+
+__all__ = (
+ 'IKEPolicyForm',
+ 'IKEProposalForm',
+ 'IPSecPolicyForm',
+ 'IPSecProfileForm',
+ 'IPSecProposalForm',
+ 'TunnelCreateForm',
+ 'TunnelForm',
+ 'TunnelTerminationForm',
+)
+
+
+class TunnelForm(TenancyForm, NetBoxModelForm):
+ ipsec_profile = DynamicModelChoiceField(
+ queryset=IPSecProfile.objects.all(),
+ label=_('IPSec Profile'),
+ required=False
+ )
+ comments = CommentField()
+
+ fieldsets = (
+ (_('Tunnel'), ('name', 'status', 'encapsulation', 'description', 'tunnel_id', 'tags')),
+ (_('Security'), ('ipsec_profile',)),
+ (_('Tenancy'), ('tenant_group', 'tenant')),
+ )
+
+ class Meta:
+ model = Tunnel
+ fields = [
+ 'name', 'status', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tenant_group', 'tenant',
+ 'comments', 'tags',
+ ]
+
+
+class TunnelCreateForm(TunnelForm):
+ # First termination
+ termination1_role = forms.ChoiceField(
+ choices=add_blank_choice(TunnelTerminationRoleChoices),
+ required=False,
+ label=_('Role')
+ )
+ termination1_type = forms.ChoiceField(
+ choices=TunnelTerminationTypeChoices,
+ required=False,
+ widget=HTMXSelect(),
+ label=_('Type')
+ )
+ termination1_parent = DynamicModelChoiceField(
+ queryset=Device.objects.all(),
+ required=False,
+ selector=True,
+ label=_('Device')
+ )
+ termination1_termination = DynamicModelChoiceField(
+ queryset=Interface.objects.all(),
+ required=False,
+ label=_('Interface'),
+ query_params={
+ 'device_id': '$termination1_parent',
+ }
+ )
+ termination1_outside_ip = DynamicModelChoiceField(
+ queryset=IPAddress.objects.all(),
+ label=_('Outside IP'),
+ required=False,
+ query_params={
+ 'device_id': '$termination1_parent',
+ }
+ )
+
+ # Second termination
+ termination2_role = forms.ChoiceField(
+ choices=add_blank_choice(TunnelTerminationRoleChoices),
+ required=False,
+ label=_('Role')
+ )
+ termination2_type = forms.ChoiceField(
+ choices=TunnelTerminationTypeChoices,
+ required=False,
+ widget=HTMXSelect(),
+ label=_('Type')
+ )
+ termination2_parent = DynamicModelChoiceField(
+ queryset=Device.objects.all(),
+ required=False,
+ selector=True,
+ label=_('Device')
+ )
+ termination2_termination = DynamicModelChoiceField(
+ queryset=Interface.objects.all(),
+ required=False,
+ label=_('Interface'),
+ query_params={
+ 'device_id': '$termination2_parent',
+ }
+ )
+ termination2_outside_ip = DynamicModelChoiceField(
+ queryset=IPAddress.objects.all(),
+ required=False,
+ label=_('Outside IP'),
+ query_params={
+ 'device_id': '$termination2_parent',
+ }
+ )
+
+ fieldsets = (
+ (_('Tunnel'), ('name', 'status', 'encapsulation', 'description', 'tunnel_id', 'tags')),
+ (_('Security'), ('ipsec_profile',)),
+ (_('Tenancy'), ('tenant_group', 'tenant')),
+ (_('First Termination'), (
+ 'termination1_role', 'termination1_type', 'termination1_parent', 'termination1_termination',
+ 'termination1_outside_ip',
+ )),
+ (_('Second Termination'), (
+ 'termination2_role', 'termination2_type', 'termination2_parent', 'termination2_termination',
+ 'termination2_outside_ip',
+ )),
+ )
+
+ def __init__(self, *args, initial=None, **kwargs):
+ super().__init__(*args, initial=initial, **kwargs)
+
+ if initial and initial.get('termination1_type') == TunnelTerminationTypeChoices.TYPE_VIRUTALMACHINE:
+ self.fields['termination1_parent'].label = _('Virtual Machine')
+ self.fields['termination1_parent'].queryset = VirtualMachine.objects.all()
+ self.fields['termination1_termination'].queryset = VMInterface.objects.all()
+ self.fields['termination1_termination'].widget.add_query_params({
+ 'virtual_machine_id': '$termination1_parent',
+ })
+ self.fields['termination1_outside_ip'].widget.add_query_params({
+ 'virtual_machine_id': '$termination1_parent',
+ })
+
+ if initial and initial.get('termination2_type') == TunnelTerminationTypeChoices.TYPE_VIRUTALMACHINE:
+ self.fields['termination2_parent'].label = _('Virtual Machine')
+ self.fields['termination2_parent'].queryset = VirtualMachine.objects.all()
+ self.fields['termination2_termination'].queryset = VMInterface.objects.all()
+ self.fields['termination2_termination'].widget.add_query_params({
+ 'virtual_machine_id': '$termination2_parent',
+ })
+ self.fields['termination2_outside_ip'].widget.add_query_params({
+ 'virtual_machine_id': '$termination2_parent',
+ })
+
+ def clean(self):
+ super().clean()
+
+ # Validate attributes for each termination (if any)
+ for term in ('termination1', 'termination2'):
+ required_parameters = (
+ f'{term}_role', f'{term}_parent', f'{term}_termination',
+ )
+ parameters = (
+ *required_parameters,
+ f'{term}_outside_ip',
+ )
+ if any([self.cleaned_data[param] for param in parameters]):
+ for param in required_parameters:
+ if not self.cleaned_data[param]:
+ raise forms.ValidationError({
+ param: _("This parameter is required when defining a termination.")
+ })
+
+ def save(self, *args, **kwargs):
+ instance = super().save(*args, **kwargs)
+
+ # Create first termination
+ if self.cleaned_data['termination1_termination']:
+ TunnelTermination.objects.create(
+ tunnel=instance,
+ role=self.cleaned_data['termination1_role'],
+ termination=self.cleaned_data['termination1_termination'],
+ outside_ip=self.cleaned_data['termination1_outside_ip'],
+ )
+
+ # Create second termination, if defined
+ if self.cleaned_data['termination2_termination']:
+ TunnelTermination.objects.create(
+ tunnel=instance,
+ role=self.cleaned_data['termination2_role'],
+ termination=self.cleaned_data['termination2_termination'],
+ outside_ip=self.cleaned_data.get('termination1_outside_ip'),
+ )
+
+ return instance
+
+
+class TunnelTerminationForm(NetBoxModelForm):
+ tunnel = DynamicModelChoiceField(
+ queryset=Tunnel.objects.all()
+ )
+ type = forms.ChoiceField(
+ choices=TunnelTerminationTypeChoices,
+ widget=HTMXSelect(),
+ label=_('Type')
+ )
+ parent = DynamicModelChoiceField(
+ queryset=Device.objects.all(),
+ selector=True,
+ label=_('Device')
+ )
+ termination = DynamicModelChoiceField(
+ queryset=Interface.objects.all(),
+ label=_('Interface'),
+ query_params={
+ 'device_id': '$parent',
+ }
+ )
+ outside_ip = DynamicModelChoiceField(
+ queryset=IPAddress.objects.all(),
+ label=_('Outside IP'),
+ required=False,
+ query_params={
+ 'device_id': '$parent',
+ }
+ )
+
+ fieldsets = (
+ (None, ('tunnel', 'role', 'type', 'parent', 'termination', 'outside_ip', 'tags')),
+ )
+
+ class Meta:
+ model = TunnelTermination
+ fields = [
+ 'tunnel', 'role', 'termination', 'outside_ip', 'tags',
+ ]
+
+ def __init__(self, *args, initial=None, **kwargs):
+ super().__init__(*args, initial=initial, **kwargs)
+
+ if initial and initial.get('type') == TunnelTerminationTypeChoices.TYPE_VIRUTALMACHINE:
+ self.fields['parent'].label = _('Virtual Machine')
+ self.fields['parent'].queryset = VirtualMachine.objects.all()
+ self.fields['termination'].queryset = VMInterface.objects.all()
+ self.fields['termination'].widget.add_query_params({
+ 'virtual_machine_id': '$parent',
+ })
+ self.fields['outside_ip'].widget.add_query_params({
+ 'virtual_machine_id': '$parent',
+ })
+
+ if self.instance.pk:
+ self.fields['parent'].initial = self.instance.termination.parent_object
+ self.fields['termination'].initial = self.instance.termination
+
+ def clean(self):
+ super().clean()
+
+ # Set the terminated object
+ self.instance.termination = self.cleaned_data.get('termination')
+
+
+class IKEProposalForm(NetBoxModelForm):
+
+ fieldsets = (
+ (_('Proposal'), ('name', 'description', 'tags')),
+ (_('Parameters'), (
+ 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime',
+ )),
+ )
+
+ class Meta:
+ model = IKEProposal
+ fields = [
+ 'name', 'description', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group',
+ 'sa_lifetime', 'tags',
+ ]
+
+
+class IKEPolicyForm(NetBoxModelForm):
+ proposals = DynamicModelMultipleChoiceField(
+ queryset=IKEProposal.objects.all(),
+ label=_('Proposals')
+ )
+
+ fieldsets = (
+ (_('Policy'), ('name', 'description', 'tags')),
+ (_('Parameters'), ('version', 'mode', 'proposals', 'preshared_key')),
+ )
+
+ class Meta:
+ model = IKEPolicy
+ fields = [
+ 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'tags',
+ ]
+
+
+class IPSecProposalForm(NetBoxModelForm):
+
+ fieldsets = (
+ (_('Proposal'), ('name', 'description', 'tags')),
+ (_('Parameters'), (
+ 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data',
+ )),
+ )
+
+ class Meta:
+ model = IPSecProposal
+ fields = [
+ 'name', 'description', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds',
+ 'sa_lifetime_data', 'tags',
+ ]
+
+
+class IPSecPolicyForm(NetBoxModelForm):
+ proposals = DynamicModelMultipleChoiceField(
+ queryset=IPSecProposal.objects.all(),
+ label=_('Proposals')
+ )
+
+ fieldsets = (
+ (_('Policy'), ('name', 'description', 'tags')),
+ (_('Parameters'), ('proposals', 'pfs_group')),
+ )
+
+ class Meta:
+ model = IPSecPolicy
+ fields = [
+ 'name', 'description', 'proposals', 'pfs_group', 'tags',
+ ]
+
+
+class IPSecProfileForm(NetBoxModelForm):
+ ike_policy = DynamicModelChoiceField(
+ queryset=IKEPolicy.objects.all(),
+ label=_('IKE policy')
+ )
+ ipsec_policy = DynamicModelChoiceField(
+ queryset=IPSecPolicy.objects.all(),
+ label=_('IPSec policy')
+ )
+ comments = CommentField()
+
+ fieldsets = (
+ (_('Profile'), ('name', 'description', 'tags')),
+ (_('Parameters'), ('mode', 'ike_policy', 'ipsec_policy')),
+ )
+
+ class Meta:
+ model = IPSecProfile
+ fields = [
+ 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags',
+ ]
diff --git a/netbox/vpn/graphql/__init__.py b/netbox/vpn/graphql/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/netbox/vpn/graphql/schema.py b/netbox/vpn/graphql/schema.py
new file mode 100644
index 00000000000..64e6808823d
--- /dev/null
+++ b/netbox/vpn/graphql/schema.py
@@ -0,0 +1,51 @@
+import graphene
+
+from netbox.graphql.fields import ObjectField, ObjectListField
+from utilities.graphql_optimizer import gql_query_optimizer
+from vpn import models
+from .types import *
+
+
+class VPNQuery(graphene.ObjectType):
+
+ ike_policy = ObjectField(IKEPolicyType)
+ ike_policy_list = ObjectListField(IKEPolicyType)
+
+ def resolve_ike_policy_list(root, info, **kwargs):
+ return gql_query_optimizer(models.IKEPolicy.objects.all(), info)
+
+ ike_proposal = ObjectField(IKEProposalType)
+ ike_proposal_list = ObjectListField(IKEProposalType)
+
+ def resolve_ike_proposal_list(root, info, **kwargs):
+ return gql_query_optimizer(models.IKEProposal.objects.all(), info)
+
+ ipsec_policy = ObjectField(IPSecPolicyType)
+ ipsec_policy_list = ObjectListField(IPSecPolicyType)
+
+ def resolve_ipsec_policy_list(root, info, **kwargs):
+ return gql_query_optimizer(models.IPSecPolicy.objects.all(), info)
+
+ ipsec_profile = ObjectField(IPSecProfileType)
+ ipsec_profile_list = ObjectListField(IPSecProfileType)
+
+ def resolve_ipsec_profile_list(root, info, **kwargs):
+ return gql_query_optimizer(models.IPSecProfile.objects.all(), info)
+
+ ipsec_proposal = ObjectField(IPSecProposalType)
+ ipsec_proposal_list = ObjectListField(IPSecProposalType)
+
+ def resolve_ipsec_proposal_list(root, info, **kwargs):
+ return gql_query_optimizer(models.IPSecProposal.objects.all(), info)
+
+ tunnel = ObjectField(TunnelType)
+ tunnel_list = ObjectListField(TunnelType)
+
+ def resolve_tunnel_list(root, info, **kwargs):
+ return gql_query_optimizer(models.Tunnel.objects.all(), info)
+
+ tunnel_termination = ObjectField(TunnelTerminationType)
+ tunnel_termination_list = ObjectListField(TunnelTerminationType)
+
+ def resolve_tunnel_termination_list(root, info, **kwargs):
+ return gql_query_optimizer(models.TunnelTermination.objects.all(), info)
diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py
new file mode 100644
index 00000000000..f46e8b69702
--- /dev/null
+++ b/netbox/vpn/graphql/types.py
@@ -0,0 +1,69 @@
+from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
+from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
+from vpn import filtersets, models
+
+__all__ = (
+ 'IKEPolicyType',
+ 'IKEProposalType',
+ 'IPSecPolicyType',
+ 'IPSecProfileType',
+ 'IPSecProposalType',
+ 'TunnelTerminationType',
+ 'TunnelType',
+)
+
+
+class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
+
+ class Meta:
+ model = models.TunnelTermination
+ fields = '__all__'
+ filterset_class = filtersets.TunnelTerminationFilterSet
+
+
+class TunnelType(NetBoxObjectType):
+
+ class Meta:
+ model = models.Tunnel
+ fields = '__all__'
+ filterset_class = filtersets.TunnelFilterSet
+
+
+class IKEProposalType(OrganizationalObjectType):
+
+ class Meta:
+ model = models.IKEProposal
+ fields = '__all__'
+ filterset_class = filtersets.IKEProposalFilterSet
+
+
+class IKEPolicyType(OrganizationalObjectType):
+
+ class Meta:
+ model = models.IKEPolicy
+ fields = '__all__'
+ filterset_class = filtersets.IKEPolicyFilterSet
+
+
+class IPSecProposalType(OrganizationalObjectType):
+
+ class Meta:
+ model = models.IPSecProposal
+ fields = '__all__'
+ filterset_class = filtersets.IPSecProposalFilterSet
+
+
+class IPSecPolicyType(OrganizationalObjectType):
+
+ class Meta:
+ model = models.IPSecPolicy
+ fields = '__all__'
+ filterset_class = filtersets.IPSecPolicyFilterSet
+
+
+class IPSecProfileType(OrganizationalObjectType):
+
+ class Meta:
+ model = models.IPSecProfile
+ fields = '__all__'
+ filterset_class = filtersets.IPSecProfileFilterSet
diff --git a/netbox/vpn/migrations/0001_initial.py b/netbox/vpn/migrations/0001_initial.py
new file mode 100644
index 00000000000..f5d9ae0c18c
--- /dev/null
+++ b/netbox/vpn/migrations/0001_initial.py
@@ -0,0 +1,186 @@
+from django.db import migrations, models
+import django.db.models.deletion
+import taggit.managers
+import utilities.json
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('extras', '0099_cachedvalue_ordering'),
+ ('ipam', '0067_ipaddress_index_host'),
+ ('tenancy', '0012_contactassignment_custom_fields'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='IKEPolicy',
+ 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)),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('version', models.PositiveSmallIntegerField(default=2)),
+ ('mode', models.CharField()),
+ ('preshared_key', models.TextField(blank=True)),
+ ],
+ options={
+ 'verbose_name': 'IKE policy',
+ 'verbose_name_plural': 'IKE policies',
+ 'ordering': ('name',),
+ },
+ ),
+ migrations.CreateModel(
+ name='IPSecPolicy',
+ 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)),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('pfs_group', models.PositiveSmallIntegerField(blank=True, null=True)),
+ ],
+ options={
+ 'verbose_name': 'IPSec policy',
+ 'verbose_name_plural': 'IPSec policies',
+ 'ordering': ('name',),
+ },
+ ),
+ migrations.CreateModel(
+ name='IPSecProfile',
+ 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)),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('comments', models.TextField(blank=True)),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ('mode', models.CharField()),
+ ('ike_policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ipsec_profiles', to='vpn.ikepolicy')),
+ ('ipsec_policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ipsec_profiles', to='vpn.ipsecpolicy')),
+ ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+ ],
+ options={
+ 'verbose_name': 'IPSec profile',
+ 'verbose_name_plural': 'IPSec profiles',
+ 'ordering': ('name',),
+ },
+ ),
+ migrations.CreateModel(
+ name='Tunnel',
+ 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)),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('comments', models.TextField(blank=True)),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ('status', models.CharField(default='active', max_length=50)),
+ ('encapsulation', models.CharField(max_length=50)),
+ ('tunnel_id', models.PositiveBigIntegerField(blank=True, null=True)),
+ ('ipsec_profile', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tunnels', to='vpn.ipsecprofile')),
+ ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+ ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tunnels', to='tenancy.tenant')),
+ ],
+ options={
+ 'verbose_name': 'tunnel',
+ 'verbose_name_plural': 'tunnels',
+ 'ordering': ('name',),
+ },
+ ),
+ migrations.CreateModel(
+ name='TunnelTermination',
+ 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)),
+ ('role', models.CharField(default='peer', max_length=50)),
+ ('termination_id', models.PositiveBigIntegerField(blank=True, null=True)),
+ ('termination_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
+ ('outside_ip', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tunnel_termination', to='ipam.ipaddress')),
+ ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+ ('tunnel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='vpn.tunnel')),
+ ],
+ options={
+ 'verbose_name': 'tunnel termination',
+ 'verbose_name_plural': 'tunnel terminations',
+ 'ordering': ('tunnel', 'role', 'pk'),
+ },
+ ),
+ migrations.CreateModel(
+ name='IPSecProposal',
+ 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)),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('encryption_algorithm', models.CharField()),
+ ('authentication_algorithm', models.CharField()),
+ ('sa_lifetime_seconds', models.PositiveIntegerField(blank=True, null=True)),
+ ('sa_lifetime_data', models.PositiveIntegerField(blank=True, null=True)),
+ ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+ ],
+ options={
+ 'verbose_name': 'IPSec proposal',
+ 'verbose_name_plural': 'IPSec proposals',
+ 'ordering': ('name',),
+ },
+ ),
+ migrations.AddField(
+ model_name='ipsecpolicy',
+ name='proposals',
+ field=models.ManyToManyField(related_name='ipsec_policies', to='vpn.ipsecproposal'),
+ ),
+ migrations.AddField(
+ model_name='ipsecpolicy',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ migrations.CreateModel(
+ name='IKEProposal',
+ 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)),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('authentication_method', models.CharField()),
+ ('encryption_algorithm', models.CharField()),
+ ('authentication_algorithm', models.CharField()),
+ ('group', models.PositiveSmallIntegerField()),
+ ('sa_lifetime', models.PositiveIntegerField(blank=True, null=True)),
+ ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+ ],
+ options={
+ 'verbose_name': 'IKE proposal',
+ 'verbose_name_plural': 'IKE proposals',
+ 'ordering': ('name',),
+ },
+ ),
+ migrations.AddField(
+ model_name='ikepolicy',
+ name='proposals',
+ field=models.ManyToManyField(related_name='ike_policies', to='vpn.ikeproposal'),
+ ),
+ migrations.AddField(
+ model_name='ikepolicy',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ migrations.AddConstraint(
+ model_name='tunneltermination',
+ constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='vpn_tunneltermination_termination', violation_error_message='An object may be terminated to only one tunnel at a time.'),
+ ),
+ ]
diff --git a/netbox/vpn/migrations/__init__.py b/netbox/vpn/migrations/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/netbox/vpn/models/__init__.py b/netbox/vpn/models/__init__.py
new file mode 100644
index 00000000000..3b70eb41839
--- /dev/null
+++ b/netbox/vpn/models/__init__.py
@@ -0,0 +1,2 @@
+from .crypto import *
+from .tunnels import *
diff --git a/netbox/vpn/models/crypto.py b/netbox/vpn/models/crypto.py
new file mode 100644
index 00000000000..1954dc6a01d
--- /dev/null
+++ b/netbox/vpn/models/crypto.py
@@ -0,0 +1,254 @@
+from django.db import models
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+
+from netbox.models import NetBoxModel, PrimaryModel
+from vpn.choices import *
+
+__all__ = (
+ 'IKEPolicy',
+ 'IKEProposal',
+ 'IPSecPolicy',
+ 'IPSecProfile',
+ 'IPSecProposal',
+)
+
+
+#
+# IKE
+#
+
+class IKEProposal(NetBoxModel):
+ name = models.CharField(
+ verbose_name=_('name'),
+ max_length=100,
+ unique=True
+ )
+ description = models.CharField(
+ verbose_name=_('description'),
+ max_length=200,
+ blank=True
+ )
+ authentication_method = models.CharField(
+ verbose_name=('authentication method'),
+ choices=AuthenticationMethodChoices
+ )
+ encryption_algorithm = models.CharField(
+ verbose_name=_('encryption algorithm'),
+ choices=EncryptionAlgorithmChoices
+ )
+ authentication_algorithm = models.CharField(
+ verbose_name=_('authentication algorithm'),
+ choices=AuthenticationAlgorithmChoices
+ )
+ group = models.PositiveSmallIntegerField(
+ verbose_name=_('group'),
+ choices=DHGroupChoices,
+ help_text=_('Diffie-Hellman group ID')
+ )
+ sa_lifetime = models.PositiveIntegerField(
+ verbose_name=_('SA lifetime'),
+ blank=True,
+ null=True,
+ help_text=_('Security association lifetime (in seconds)')
+ )
+
+ clone_fields = (
+ 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime',
+ )
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _('IKE proposal')
+ verbose_name_plural = _('IKE proposals')
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('vpn:ikeproposal', args=[self.pk])
+
+
+class IKEPolicy(NetBoxModel):
+ name = models.CharField(
+ verbose_name=_('name'),
+ max_length=100,
+ unique=True
+ )
+ description = models.CharField(
+ verbose_name=_('description'),
+ max_length=200,
+ blank=True
+ )
+ version = models.PositiveSmallIntegerField(
+ verbose_name=_('version'),
+ choices=IKEVersionChoices,
+ default=IKEVersionChoices.VERSION_2
+ )
+ mode = models.CharField(
+ verbose_name=_('mode'),
+ choices=IKEModeChoices
+ )
+ proposals = models.ManyToManyField(
+ to='vpn.IKEProposal',
+ related_name='ike_policies',
+ verbose_name=_('proposals')
+ )
+ preshared_key = models.TextField(
+ verbose_name=_('pre-shared key'),
+ blank=True
+ )
+
+ clone_fields = (
+ 'version', 'mode', 'proposals',
+ )
+ prerequisite_models = (
+ 'vpn.IKEProposal',
+ )
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _('IKE policy')
+ verbose_name_plural = _('IKE policies')
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('vpn:ikepolicy', args=[self.pk])
+
+
+#
+# IPSec
+#
+
+class IPSecProposal(NetBoxModel):
+ name = models.CharField(
+ verbose_name=_('name'),
+ max_length=100,
+ unique=True
+ )
+ description = models.CharField(
+ verbose_name=_('description'),
+ max_length=200,
+ blank=True
+ )
+ encryption_algorithm = models.CharField(
+ verbose_name=_('encryption'),
+ choices=EncryptionAlgorithmChoices
+ )
+ authentication_algorithm = models.CharField(
+ verbose_name=_('authentication'),
+ choices=AuthenticationAlgorithmChoices
+ )
+ sa_lifetime_seconds = models.PositiveIntegerField(
+ verbose_name=_('SA lifetime (seconds)'),
+ blank=True,
+ null=True,
+ help_text=_('Security association lifetime (seconds)')
+ )
+ sa_lifetime_data = models.PositiveIntegerField(
+ verbose_name=_('SA lifetime (KB)'),
+ blank=True,
+ null=True,
+ help_text=_('Security association lifetime (in kilobytes)')
+ )
+
+ clone_fields = (
+ 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data',
+ )
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _('IPSec proposal')
+ verbose_name_plural = _('IPSec proposals')
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('vpn:ipsecproposal', args=[self.pk])
+
+
+class IPSecPolicy(NetBoxModel):
+ name = models.CharField(
+ verbose_name=_('name'),
+ max_length=100,
+ unique=True
+ )
+ description = models.CharField(
+ verbose_name=_('description'),
+ max_length=200,
+ blank=True
+ )
+ proposals = models.ManyToManyField(
+ to='vpn.IPSecProposal',
+ related_name='ipsec_policies',
+ verbose_name=_('proposals')
+ )
+ pfs_group = models.PositiveSmallIntegerField(
+ verbose_name=_('PFS group'),
+ choices=DHGroupChoices,
+ blank=True,
+ null=True,
+ help_text=_('Diffie-Hellman group for Perfect Forward Secrecy')
+ )
+
+ clone_fields = (
+ 'proposals', 'pfs_group',
+ )
+ prerequisite_models = (
+ 'vpn.IPSecProposal',
+ )
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _('IPSec policy')
+ verbose_name_plural = _('IPSec policies')
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('vpn:ipsecpolicy', args=[self.pk])
+
+
+class IPSecProfile(PrimaryModel):
+ name = models.CharField(
+ verbose_name=_('name'),
+ max_length=100,
+ unique=True
+ )
+ mode = models.CharField(
+ verbose_name=_('mode'),
+ choices=IPSecModeChoices
+ )
+ ike_policy = models.ForeignKey(
+ to='vpn.IKEPolicy',
+ on_delete=models.PROTECT,
+ related_name='ipsec_profiles'
+ )
+ ipsec_policy = models.ForeignKey(
+ to='vpn.IPSecPolicy',
+ on_delete=models.PROTECT,
+ related_name='ipsec_profiles'
+ )
+
+ clone_fields = (
+ 'mode', 'ike_policy', 'ipsec_policy',
+ )
+ prerequisite_models = (
+ 'vpn.IKEPolicy',
+ 'vpn.IPSecPolicy',
+ )
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _('IPSec profile')
+ verbose_name_plural = _('IPSec profiles')
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('vpn:ipsecprofile', args=[self.pk])
diff --git a/netbox/vpn/models/tunnels.py b/netbox/vpn/models/tunnels.py
new file mode 100644
index 00000000000..f7390d0b471
--- /dev/null
+++ b/netbox/vpn/models/tunnels.py
@@ -0,0 +1,146 @@
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+
+from netbox.models import ChangeLoggedModel, PrimaryModel
+from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin
+from vpn.choices import *
+
+__all__ = (
+ 'Tunnel',
+ 'TunnelTermination',
+)
+
+
+class Tunnel(PrimaryModel):
+ name = models.CharField(
+ verbose_name=_('name'),
+ max_length=100,
+ unique=True
+ )
+ status = models.CharField(
+ verbose_name=_('status'),
+ max_length=50,
+ choices=TunnelStatusChoices,
+ default=TunnelStatusChoices.STATUS_ACTIVE
+ )
+ encapsulation = models.CharField(
+ verbose_name=_('encapsulation'),
+ max_length=50,
+ choices=TunnelEncapsulationChoices
+ )
+ ipsec_profile = models.ForeignKey(
+ to='vpn.IPSecProfile',
+ on_delete=models.PROTECT,
+ related_name='tunnels',
+ blank=True,
+ null=True
+ )
+ tenant = models.ForeignKey(
+ to='tenancy.Tenant',
+ on_delete=models.PROTECT,
+ related_name='tunnels',
+ blank=True,
+ null=True
+ )
+ tunnel_id = models.PositiveBigIntegerField(
+ verbose_name=_('tunnel ID'),
+ blank=True,
+ null=True
+ )
+
+ clone_fields = (
+ 'status', 'encapsulation', 'ipsec_profile', 'tenant',
+ )
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _('tunnel')
+ verbose_name_plural = _('tunnels')
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('vpn:tunnel', args=[self.pk])
+
+ def get_status_color(self):
+ return TunnelStatusChoices.colors.get(self.status)
+
+
+class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLoggedModel):
+ tunnel = models.ForeignKey(
+ to='vpn.Tunnel',
+ on_delete=models.CASCADE,
+ related_name='terminations'
+ )
+ role = models.CharField(
+ verbose_name=_('role'),
+ max_length=50,
+ choices=TunnelTerminationRoleChoices,
+ default=TunnelTerminationRoleChoices.ROLE_PEER
+ )
+ termination_type = models.ForeignKey(
+ to='contenttypes.ContentType',
+ on_delete=models.PROTECT,
+ related_name='+'
+ )
+ termination_id = models.PositiveBigIntegerField(
+ blank=True,
+ null=True
+ )
+ termination = GenericForeignKey(
+ ct_field='termination_type',
+ fk_field='termination_id'
+ )
+ outside_ip = models.OneToOneField(
+ to='ipam.IPAddress',
+ on_delete=models.PROTECT,
+ related_name='tunnel_termination',
+ blank=True,
+ null=True
+ )
+
+ prerequisite_models = (
+ 'vpn.Tunnel',
+ )
+
+ class Meta:
+ ordering = ('tunnel', 'role', 'pk')
+ constraints = (
+ models.UniqueConstraint(
+ fields=('termination_type', 'termination_id'),
+ name='%(app_label)s_%(class)s_termination',
+ violation_error_message=_("An object may be terminated to only one tunnel at a time.")
+ ),
+ )
+ verbose_name = _('tunnel termination')
+ verbose_name_plural = _('tunnel terminations')
+
+ def __str__(self):
+ return f'{self.tunnel}: Termination {self.pk}'
+
+ def get_absolute_url(self):
+ return reverse('vpn:tunneltermination', args=[self.pk])
+
+ def get_role_color(self):
+ return TunnelTerminationRoleChoices.colors.get(self.role)
+
+ def clean(self):
+ super().clean()
+
+ # Check that the selected termination object is not already attached to a Tunnel
+ if getattr(self.termination, 'tunnel_termination', None) and self.termination.tunnel_termination.pk != self.pk:
+ raise ValidationError({
+ 'termination': _("{name} is already attached to a tunnel ({tunnel}).").format(
+ name=self.termination.name,
+ tunnel=self.termination.tunnel_termination.tunnel
+ )
+ })
+
+ def to_objectchange(self, action):
+ objectchange = super().to_objectchange(action)
+ objectchange.related_object = self.tunnel
+ return objectchange
diff --git a/netbox/vpn/search.py b/netbox/vpn/search.py
new file mode 100644
index 00000000000..70b0c644f52
--- /dev/null
+++ b/netbox/vpn/search.py
@@ -0,0 +1,65 @@
+from netbox.search import SearchIndex, register_search
+from . import models
+
+
+@register_search
+class TunnelIndex(SearchIndex):
+ model = models.Tunnel
+ fields = (
+ ('name', 100),
+ ('tunnel_id', 300),
+ ('description', 500),
+ ('comments', 5000),
+ )
+ display_attrs = ('status', 'encapsulation', 'tenant', 'description')
+
+
+@register_search
+class IKEProposalIndex(SearchIndex):
+ model = models.IKEProposal
+ fields = (
+ ('name', 100),
+ ('description', 500),
+ )
+ display_attrs = ('description',)
+
+
+@register_search
+class IKEPolicyIndex(SearchIndex):
+ model = models.IKEPolicy
+ fields = (
+ ('name', 100),
+ ('description', 500),
+ )
+ display_attrs = ('description',)
+
+
+@register_search
+class IPSecProposalIndex(SearchIndex):
+ model = models.IPSecProposal
+ fields = (
+ ('name', 100),
+ ('description', 500),
+ )
+ display_attrs = ('description',)
+
+
+@register_search
+class IPSecPolicyIndex(SearchIndex):
+ model = models.IPSecPolicy
+ fields = (
+ ('name', 100),
+ ('description', 500),
+ )
+ display_attrs = ('description',)
+
+
+@register_search
+class IPSecProfileIndex(SearchIndex):
+ model = models.IPSecProfile
+ fields = (
+ ('name', 100),
+ ('description', 500),
+ ('comments', 5000),
+ )
+ display_attrs = ('description',)
diff --git a/netbox/vpn/tables.py b/netbox/vpn/tables.py
new file mode 100644
index 00000000000..304467586e4
--- /dev/null
+++ b/netbox/vpn/tables.py
@@ -0,0 +1,254 @@
+import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
+from django_tables2.utils import Accessor
+
+from tenancy.tables import TenancyColumnsMixin
+from netbox.tables import NetBoxTable, columns
+from vpn.models import *
+
+__all__ = (
+ 'IKEPolicyTable',
+ 'IKEProposalTable',
+ 'IPSecPolicyTable',
+ 'IPSecProposalTable',
+ 'IPSecProfileTable',
+ 'TunnelTable',
+ 'TunnelTerminationTable',
+)
+
+
+class TunnelTable(TenancyColumnsMixin, NetBoxTable):
+ name = tables.Column(
+ verbose_name=_('Name'),
+ linkify=True
+ )
+ status = columns.ChoiceFieldColumn(
+ verbose_name=_('Status')
+ )
+ ipsec_profile = tables.Column(
+ verbose_name=_('IPSec profile'),
+ linkify=True
+ )
+ terminations_count = columns.LinkedCountColumn(
+ accessor=Accessor('count_terminations'),
+ viewname='vpn:tunneltermination_list',
+ url_params={'tunnel_id': 'pk'},
+ verbose_name=_('Terminations')
+ )
+ comments = columns.MarkdownColumn(
+ verbose_name=_('Comments'),
+ )
+ tags = columns.TagColumn(
+ url_name='vpn:tunnel_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = Tunnel
+ fields = (
+ 'pk', 'id', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tenant_group', 'tunnel_id',
+ 'termination_count', 'description', 'comments', 'tags', 'created', 'last_updated',
+ )
+ default_columns = ('pk', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'terminations_count')
+
+
+class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
+ tunnel = tables.Column(
+ verbose_name=_('Tunnel'),
+ linkify=True
+ )
+ role = columns.ChoiceFieldColumn(
+ verbose_name=_('Role')
+ )
+ termination_parent = tables.Column(
+ accessor='termination__parent_object',
+ linkify=True,
+ orderable=False,
+ verbose_name=_('Host')
+ )
+ termination = tables.Column(
+ verbose_name=_('Termination'),
+ linkify=True
+ )
+ ip_addresses = tables.ManyToManyColumn(
+ accessor=tables.A('termination__ip_addresses'),
+ orderable=False,
+ linkify_item=True,
+ verbose_name=_('IP Addresses')
+ )
+ outside_ip = tables.Column(
+ verbose_name=_('Outside IP'),
+ linkify=True
+ )
+ tags = columns.TagColumn(
+ url_name='vpn:tunneltermination_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = TunnelTermination
+ fields = (
+ 'pk', 'id', 'tunnel', 'role', 'termination_parent', 'termination', 'ip_addresses', 'outside_ip', 'tags',
+ 'created', 'last_updated',
+ )
+ default_columns = (
+ 'pk', 'id', 'tunnel', 'role', 'termination_parent', 'termination', 'ip_addresses', 'outside_ip',
+ )
+
+
+class IKEProposalTable(NetBoxTable):
+ name = tables.Column(
+ verbose_name=_('Name'),
+ linkify=True
+ )
+ authentication_method = tables.Column(
+ verbose_name=_('Authentication Method')
+ )
+ encryption_algorithm = tables.Column(
+ verbose_name=_('Encryption Algorithm')
+ )
+ authentication_algorithm = tables.Column(
+ verbose_name=_('Authentication Algorithm')
+ )
+ group = tables.Column(
+ verbose_name=_('Group')
+ )
+ sa_lifetime = tables.Column(
+ verbose_name=_('SA Lifetime')
+ )
+ tags = columns.TagColumn(
+ url_name='vpn:ikeproposal_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = IKEProposal
+ fields = (
+ 'pk', 'id', 'name', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm',
+ 'group', 'sa_lifetime', 'description', 'tags', 'created', 'last_updated',
+ )
+ default_columns = (
+ 'pk', 'name', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group',
+ 'sa_lifetime', 'description',
+ )
+
+
+class IKEPolicyTable(NetBoxTable):
+ name = tables.Column(
+ verbose_name=_('Name'),
+ linkify=True
+ )
+ version = tables.Column(
+ verbose_name=_('Version')
+ )
+ mode = tables.Column(
+ verbose_name=_('Mode')
+ )
+ proposals = tables.ManyToManyColumn(
+ linkify_item=True,
+ verbose_name=_('Proposals')
+ )
+ preshared_key = tables.Column(
+ verbose_name=_('Pre-shared Key')
+ )
+ tags = columns.TagColumn(
+ url_name='vpn:ikepolicy_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = IKEPolicy
+ fields = (
+ 'pk', 'id', 'name', 'version', 'mode', 'proposals', 'preshared_key', 'description', 'tags', 'created',
+ 'last_updated',
+ )
+ default_columns = (
+ 'pk', 'name', 'version', 'mode', 'proposals', 'description',
+ )
+
+
+class IPSecProposalTable(NetBoxTable):
+ name = tables.Column(
+ verbose_name=_('Name'),
+ linkify=True
+ )
+ encryption_algorithm = tables.Column(
+ verbose_name=_('Encryption Algorithm')
+ )
+ authentication_algorithm = tables.Column(
+ verbose_name=_('Authentication Algorithm')
+ )
+ sa_lifetime_seconds = tables.Column(
+ verbose_name=_('SA Lifetime (Seconds)')
+ )
+ sa_lifetime_data = tables.Column(
+ verbose_name=_('SA Lifetime (KB)')
+ )
+ tags = columns.TagColumn(
+ url_name='vpn:ipsecproposal_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = IPSecProposal
+ fields = (
+ 'pk', 'id', 'name', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds',
+ 'sa_lifetime_data', 'description', 'tags', 'created', 'last_updated',
+ )
+ default_columns = (
+ 'pk', 'name', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds',
+ 'sa_lifetime_data', 'description',
+ )
+
+
+class IPSecPolicyTable(NetBoxTable):
+ name = tables.Column(
+ verbose_name=_('Name'),
+ linkify=True
+ )
+ proposals = tables.ManyToManyColumn(
+ linkify_item=True,
+ verbose_name=_('Proposals')
+ )
+ pfs_group = tables.Column(
+ verbose_name=_('PFS Group')
+ )
+ tags = columns.TagColumn(
+ url_name='vpn:ipsecpolicy_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = IPSecPolicy
+ fields = (
+ 'pk', 'id', 'name', 'proposals', 'pfs_group', 'description', 'tags', 'created', 'last_updated',
+ )
+ default_columns = (
+ 'pk', 'name', 'proposals', 'pfs_group', 'description',
+ )
+
+
+class IPSecProfileTable(NetBoxTable):
+ name = tables.Column(
+ verbose_name=_('Name'),
+ linkify=True
+ )
+ mode = tables.Column(
+ verbose_name=_('Mode')
+ )
+ ike_policy = tables.Column(
+ linkify=True,
+ verbose_name=_('IKE Policy')
+ )
+ ipsec_policy = tables.Column(
+ linkify=True,
+ verbose_name=_('IPSec Policy')
+ )
+ comments = columns.MarkdownColumn(
+ verbose_name=_('Comments'),
+ )
+ tags = columns.TagColumn(
+ url_name='vpn:ipsecprofile_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = IPSecProfile
+ fields = (
+ 'pk', 'id', 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags', 'created',
+ 'last_updated',
+ )
+ default_columns = ('pk', 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description')
diff --git a/netbox/vpn/tests/__init__.py b/netbox/vpn/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/netbox/vpn/tests/test_api.py b/netbox/vpn/tests/test_api.py
new file mode 100644
index 00000000000..9bfa297ab45
--- /dev/null
+++ b/netbox/vpn/tests/test_api.py
@@ -0,0 +1,473 @@
+from django.urls import reverse
+
+from dcim.choices import InterfaceTypeChoices
+from dcim.models import Interface
+from utilities.testing import APITestCase, APIViewTestCases, create_test_device
+from vpn.choices import *
+from vpn.models import *
+
+
+class AppTest(APITestCase):
+
+ def test_root(self):
+ url = reverse('vpn-api:api-root')
+ response = self.client.get('{}?format=api'.format(url), **self.header)
+
+ self.assertEqual(response.status_code, 200)
+
+
+class TunnelTest(APIViewTestCases.APIViewTestCase):
+ model = Tunnel
+ brief_fields = ['display', 'id', 'name', 'url']
+ bulk_update_data = {
+ 'status': TunnelStatusChoices.STATUS_PLANNED,
+ 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
+ 'description': 'New description',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+
+ tunnels = (
+ Tunnel(
+ name='Tunnel 1',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ Tunnel(
+ name='Tunnel 2',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ Tunnel(
+ name='Tunnel 3',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ )
+ Tunnel.objects.bulk_create(tunnels)
+
+ cls.create_data = [
+ {
+ 'name': 'Tunnel 4',
+ 'status': TunnelStatusChoices.STATUS_DISABLED,
+ 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
+ },
+ {
+ 'name': 'Tunnel 5',
+ 'status': TunnelStatusChoices.STATUS_DISABLED,
+ 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
+ },
+ {
+ 'name': 'Tunnel 6',
+ 'status': TunnelStatusChoices.STATUS_DISABLED,
+ 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
+ },
+ ]
+
+
+class TunnelTerminationTest(APIViewTestCases.APIViewTestCase):
+ model = TunnelTermination
+ brief_fields = ['display', 'id', 'url']
+ bulk_update_data = {
+ 'role': TunnelTerminationRoleChoices.ROLE_PEER,
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+ device = create_test_device('Device 1')
+ interfaces = (
+ Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 4', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 5', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 6', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ )
+ Interface.objects.bulk_create(interfaces)
+
+ tunnel = Tunnel.objects.create(
+ name='Tunnel 1',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ )
+
+ tunnel_terminations = (
+ TunnelTermination(
+ tunnel=tunnel,
+ role=TunnelTerminationRoleChoices.ROLE_HUB,
+ termination=interfaces[0]
+ ),
+ TunnelTermination(
+ tunnel=tunnel,
+ role=TunnelTerminationRoleChoices.ROLE_HUB,
+ termination=interfaces[1]
+ ),
+ TunnelTermination(
+ tunnel=tunnel,
+ role=TunnelTerminationRoleChoices.ROLE_HUB,
+ termination=interfaces[2]
+ ),
+ )
+ TunnelTermination.objects.bulk_create(tunnel_terminations)
+
+ cls.create_data = [
+ {
+ 'tunnel': tunnel.pk,
+ 'role': TunnelTerminationRoleChoices.ROLE_PEER,
+ 'termination_type': 'dcim.interface',
+ 'termination_id': interfaces[3].pk,
+ },
+ {
+ 'tunnel': tunnel.pk,
+ 'role': TunnelTerminationRoleChoices.ROLE_PEER,
+ 'termination_type': 'dcim.interface',
+ 'termination_id': interfaces[4].pk,
+ },
+ {
+ 'tunnel': tunnel.pk,
+ 'role': TunnelTerminationRoleChoices.ROLE_PEER,
+ 'termination_type': 'dcim.interface',
+ 'termination_id': interfaces[5].pk,
+ },
+ ]
+
+
+class IKEProposalTest(APIViewTestCases.APIViewTestCase):
+ model = IKEProposal
+ brief_fields = ['display', 'id', 'name', 'url']
+ bulk_update_data = {
+ 'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_MD5,
+ 'group': DHGroupChoices.GROUP_19,
+ 'description': 'New description',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ike_proposals = (
+ IKEProposal(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 2',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 3',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IKEProposal.objects.bulk_create(ike_proposals)
+
+ cls.create_data = [
+ {
+ 'name': 'IKE Proposal 4',
+ 'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'group': DHGroupChoices.GROUP_19,
+ },
+ {
+ 'name': 'IKE Proposal 5',
+ 'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'group': DHGroupChoices.GROUP_19,
+ },
+ {
+ 'name': 'IKE Proposal 6',
+ 'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'group': DHGroupChoices.GROUP_19,
+ },
+ ]
+
+
+class IKEPolicyTest(APIViewTestCases.APIViewTestCase):
+ model = IKEPolicy
+ brief_fields = ['display', 'id', 'name', 'url']
+ bulk_update_data = {
+ 'version': IKEVersionChoices.VERSION_1,
+ 'mode': IKEModeChoices.AGGRESSIVE,
+ 'description': 'New description',
+ 'preshared_key': 'New key',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ike_proposals = (
+ IKEProposal(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 2',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IKEProposal.objects.bulk_create(ike_proposals)
+
+ ike_policies = (
+ IKEPolicy(
+ name='IKE Policy 1',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 2',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 3',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ )
+ IKEPolicy.objects.bulk_create(ike_policies)
+ for ike_policy in ike_policies:
+ ike_policy.proposals.set(ike_proposals)
+
+ cls.create_data = [
+ {
+ 'name': 'IKE Policy 4',
+ 'version': IKEVersionChoices.VERSION_1,
+ 'mode': IKEModeChoices.MAIN,
+ 'proposals': [ike_proposals[0].pk, ike_proposals[1].pk],
+ },
+ {
+ 'name': 'IKE Policy 5',
+ 'version': IKEVersionChoices.VERSION_1,
+ 'mode': IKEModeChoices.MAIN,
+ 'proposals': [ike_proposals[0].pk, ike_proposals[1].pk],
+ },
+ {
+ 'name': 'IKE Policy 6',
+ 'version': IKEVersionChoices.VERSION_1,
+ 'mode': IKEModeChoices.MAIN,
+ 'proposals': [ike_proposals[0].pk, ike_proposals[1].pk],
+ },
+ ]
+
+
+class IPSecProposalTest(APIViewTestCases.APIViewTestCase):
+ model = IPSecProposal
+ brief_fields = ['display', 'id', 'name', 'url']
+ bulk_update_data = {
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_MD5,
+ 'description': 'New description',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ipsec_proposals = (
+ IPSecProposal(
+ name='IPSec Proposal 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 2',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 3',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ )
+ IPSecProposal.objects.bulk_create(ipsec_proposals)
+
+ cls.create_data = [
+ {
+ 'name': 'IPSec Proposal 4',
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ },
+ {
+ 'name': 'IPSec Proposal 5',
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ },
+ {
+ 'name': 'IPSec Proposal 6',
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ },
+ ]
+
+
+class IPSecPolicyTest(APIViewTestCases.APIViewTestCase):
+ model = IPSecPolicy
+ brief_fields = ['display', 'id', 'name', 'url']
+ bulk_update_data = {
+ 'pfs_group': DHGroupChoices.GROUP_5,
+ 'description': 'New description',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ipsec_proposals = (
+ IPSecProposal(
+ name='IPSec Policy 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 2',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ )
+ IPSecProposal.objects.bulk_create(ipsec_proposals)
+
+ ipsec_policies = (
+ IPSecPolicy(
+ name='IPSec Policy 1',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 2',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 3',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IPSecPolicy.objects.bulk_create(ipsec_policies)
+ for ipsec_policy in ipsec_policies:
+ ipsec_policy.proposals.set(ipsec_proposals)
+
+ cls.create_data = [
+ {
+ 'name': 'IPSec Policy 4',
+ 'pfs_group': DHGroupChoices.GROUP_16,
+ 'proposals': [ipsec_proposals[0].pk, ipsec_proposals[1].pk],
+ },
+ {
+ 'name': 'IPSec Policy 5',
+ 'pfs_group': DHGroupChoices.GROUP_16,
+ 'proposals': [ipsec_proposals[0].pk, ipsec_proposals[1].pk],
+ },
+ {
+ 'name': 'IPSec Policy 6',
+ 'pfs_group': DHGroupChoices.GROUP_16,
+ 'proposals': [ipsec_proposals[0].pk, ipsec_proposals[1].pk],
+ },
+ ]
+
+
+class IPSecProfileTest(APIViewTestCases.APIViewTestCase):
+ model = IPSecProfile
+ brief_fields = ['display', 'id', 'name', 'url']
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ike_proposal = IKEProposal.objects.create(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ )
+
+ ipsec_proposal = IPSecProposal.objects.create(
+ name='IPSec Proposal 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ )
+
+ ike_policies = (
+ IKEPolicy(
+ name='IKE Policy 1',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 2',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ )
+ IKEPolicy.objects.bulk_create(ike_policies)
+ for ike_policy in ike_policies:
+ ike_policy.proposals.add(ike_proposal)
+
+ ipsec_policies = (
+ IPSecPolicy(
+ name='IPSec Policy 1',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 2',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IPSecPolicy.objects.bulk_create(ipsec_policies)
+ for ipsec_policy in ipsec_policies:
+ ipsec_policy.proposals.add(ipsec_proposal)
+
+ ipsec_profiles = (
+ IPSecProfile(
+ name='IPSec Profile 1',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[0],
+ ipsec_policy=ipsec_policies[0]
+ ),
+ IPSecProfile(
+ name='IPSec Profile 2',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[0],
+ ipsec_policy=ipsec_policies[0]
+ ),
+ IPSecProfile(
+ name='IPSec Profile 3',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[0],
+ ipsec_policy=ipsec_policies[0]
+ ),
+ )
+ IPSecProfile.objects.bulk_create(ipsec_profiles)
+
+ cls.create_data = [
+ {
+ 'name': 'IPSec Profile 4',
+ 'mode': IPSecModeChoices.AH,
+ 'ike_policy': ike_policies[1].pk,
+ 'ipsec_policy': ipsec_policies[1].pk,
+ },
+ ]
+
+ cls.bulk_update_data = {
+ 'mode': IPSecModeChoices.AH,
+ 'ike_policy': ike_policies[1].pk,
+ 'ipsec_policy': ipsec_policies[1].pk,
+ 'description': 'New description',
+ }
diff --git a/netbox/vpn/tests/test_filtersets.py b/netbox/vpn/tests/test_filtersets.py
new file mode 100644
index 00000000000..966717f4a99
--- /dev/null
+++ b/netbox/vpn/tests/test_filtersets.py
@@ -0,0 +1,592 @@
+from django.test import TestCase
+
+from dcim.choices import InterfaceTypeChoices
+from dcim.models import Interface
+from ipam.models import IPAddress
+from virtualization.models import VMInterface
+from vpn.choices import *
+from vpn.filtersets import *
+from vpn.models import *
+from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
+
+
+class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = Tunnel.objects.all()
+ filterset = TunnelFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ ike_proposal = IKEProposal.objects.create(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ )
+ ike_policy = IKEPolicy.objects.create(
+ name='IKE Policy 1',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ )
+ ike_policy.proposals.add(ike_proposal)
+ ipsec_proposal = IPSecProposal.objects.create(
+ name='IPSec Proposal 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ )
+ ipsec_policy = IPSecPolicy.objects.create(
+ name='IPSec Policy 1',
+ pfs_group=DHGroupChoices.GROUP_14
+ )
+ ipsec_policy.proposals.add(ipsec_proposal)
+ ipsec_profiles = (
+ IPSecProfile(
+ name='IPSec Profile 1',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policy,
+ ipsec_policy=ipsec_policy
+ ),
+ IPSecProfile(
+ name='IPSec Profile 2',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policy,
+ ipsec_policy=ipsec_policy
+ ),
+ )
+ IPSecProfile.objects.bulk_create(ipsec_profiles)
+
+ tunnels = (
+ Tunnel(
+ name='Tunnel 1',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_GRE,
+ ipsec_profile=ipsec_profiles[0],
+ tunnel_id=100
+ ),
+ Tunnel(
+ name='Tunnel 2',
+ status=TunnelStatusChoices.STATUS_PLANNED,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP,
+ ipsec_profile=ipsec_profiles[0],
+ tunnel_id=200
+ ),
+ Tunnel(
+ name='Tunnel 3',
+ status=TunnelStatusChoices.STATUS_DISABLED,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IPSEC_TUNNEL,
+ ipsec_profile=None,
+ tunnel_id=300
+ ),
+ )
+ Tunnel.objects.bulk_create(tunnels)
+
+ def test_name(self):
+ params = {'name': ['Tunnel 1', 'Tunnel 2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_status(self):
+ params = {'status': [TunnelStatusChoices.STATUS_ACTIVE, TunnelStatusChoices.STATUS_PLANNED]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_encapsulation(self):
+ params = {'encapsulation': [TunnelEncapsulationChoices.ENCAP_GRE, TunnelEncapsulationChoices.ENCAP_IP_IP]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_ipsec_profile(self):
+ ipsec_profiles = IPSecProfile.objects.all()[:2]
+ params = {'ipsec_profile_id': [ipsec_profiles[0].pk, ipsec_profiles[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'ipsec_profile': [ipsec_profiles[0].name, ipsec_profiles[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_tunnel_id(self):
+ params = {'tunnel_id': [100, 200]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
+class TunnelTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = TunnelTermination.objects.all()
+ filterset = TunnelTerminationFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ device = create_test_device('Device 1')
+ interfaces = (
+ Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ )
+ Interface.objects.bulk_create(interfaces)
+
+ virtual_machine = create_test_virtualmachine('Virtual Machine 1')
+ vm_interfaces = (
+ VMInterface(virtual_machine=virtual_machine, name='Interface 1'),
+ VMInterface(virtual_machine=virtual_machine, name='Interface 2'),
+ VMInterface(virtual_machine=virtual_machine, name='Interface 3'),
+ )
+ VMInterface.objects.bulk_create(vm_interfaces)
+
+ ip_addresses = (
+ IPAddress(address='192.168.0.1/32'),
+ IPAddress(address='192.168.0.2/32'),
+ IPAddress(address='192.168.0.3/32'),
+ IPAddress(address='192.168.0.4/32'),
+ IPAddress(address='192.168.0.5/32'),
+ IPAddress(address='192.168.0.6/32'),
+ )
+ IPAddress.objects.bulk_create(ip_addresses)
+
+ tunnels = (
+ Tunnel(
+ name='Tunnel 1',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ Tunnel(
+ name='Tunnel 2',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ Tunnel(
+ name='Tunnel 3',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ )
+ Tunnel.objects.bulk_create(tunnels)
+
+ tunnel_terminations = (
+ # Tunnel 1
+ TunnelTermination(
+ tunnel=tunnels[0],
+ role=TunnelTerminationRoleChoices.ROLE_HUB,
+ termination=interfaces[0],
+ outside_ip=ip_addresses[0]
+ ),
+ TunnelTermination(
+ tunnel=tunnels[0],
+ role=TunnelTerminationRoleChoices.ROLE_SPOKE,
+ termination=vm_interfaces[0],
+ outside_ip=ip_addresses[1]
+ ),
+ # Tunnel 2
+ TunnelTermination(
+ tunnel=tunnels[1],
+ role=TunnelTerminationRoleChoices.ROLE_HUB,
+ termination=interfaces[1],
+ outside_ip=ip_addresses[2]
+ ),
+ TunnelTermination(
+ tunnel=tunnels[1],
+ role=TunnelTerminationRoleChoices.ROLE_SPOKE,
+ termination=vm_interfaces[1],
+ outside_ip=ip_addresses[3]
+ ),
+ # Tunnel 3
+ TunnelTermination(
+ tunnel=tunnels[2],
+ role=TunnelTerminationRoleChoices.ROLE_PEER,
+ termination=interfaces[2],
+ outside_ip=ip_addresses[4]
+ ),
+ TunnelTermination(
+ tunnel=tunnels[2],
+ role=TunnelTerminationRoleChoices.ROLE_PEER,
+ termination=vm_interfaces[2],
+ outside_ip=ip_addresses[5]
+ ),
+ )
+ TunnelTermination.objects.bulk_create(tunnel_terminations)
+
+ def test_tunnel(self):
+ tunnels = Tunnel.objects.all()[:2]
+ params = {'tunnel_id': [tunnels[0].pk, tunnels[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ params = {'tunnel': [tunnels[0].name, tunnels[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+
+ def test_role(self):
+ params = {'role': [TunnelTerminationRoleChoices.ROLE_HUB, TunnelTerminationRoleChoices.ROLE_SPOKE]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+
+ def test_termination_type(self):
+ params = {'termination_type': 'dcim.interface'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'termination_type': 'virtualization.vminterface'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
+ def test_interface(self):
+ interfaces = Interface.objects.all()[:2]
+ params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'interface': [interfaces[0].name, interfaces[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_vminterface(self):
+ vm_interfaces = VMInterface.objects.all()[:2]
+ params = {'vminterface_id': [vm_interfaces[0].pk, vm_interfaces[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'vminterface': [vm_interfaces[0].name, vm_interfaces[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_outside_ip(self):
+ ip_addresses = IPAddress.objects.all()[:2]
+ params = {'outside_ip_id': [ip_addresses[0].pk, ip_addresses[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
+class IKEProposalTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = IKEProposal.objects.all()
+ filterset = IKEProposalFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ ike_proposals = (
+ IKEProposal(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_1,
+ sa_lifetime=1000
+ ),
+ IKEProposal(
+ name='IKE Proposal 2',
+ authentication_method=AuthenticationMethodChoices.CERTIFICATES,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ group=DHGroupChoices.GROUP_2,
+ sa_lifetime=2000
+ ),
+ IKEProposal(
+ name='IKE Proposal 3',
+ authentication_method=AuthenticationMethodChoices.RSA_SIGNATURES,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA512,
+ group=DHGroupChoices.GROUP_5,
+ sa_lifetime=3000
+ ),
+ )
+ IKEProposal.objects.bulk_create(ike_proposals)
+
+ def test_name(self):
+ params = {'name': ['IKE Proposal 1', 'IKE Proposal 2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_authentication_method(self):
+ params = {'authentication_method': [
+ AuthenticationMethodChoices.PRESHARED_KEYS, AuthenticationMethodChoices.CERTIFICATES
+ ]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_encryption_algorithm(self):
+ params = {'encryption_algorithm': [
+ EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC, EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC
+ ]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_authentication_algorithm(self):
+ params = {'authentication_algorithm': [
+ AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1, AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256
+ ]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_group(self):
+ params = {'group': [DHGroupChoices.GROUP_1, DHGroupChoices.GROUP_2]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_sa_lifetime(self):
+ params = {'sa_lifetime': [1000, 2000]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
+class IKEPolicyTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = IKEPolicy.objects.all()
+ filterset = IKEPolicyFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ ike_proposals = (
+ IKEProposal(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 2',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 3',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IKEProposal.objects.bulk_create(ike_proposals)
+
+ ike_policies = (
+ IKEPolicy(
+ name='IKE Policy 1',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 2',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 3',
+ version=IKEVersionChoices.VERSION_2,
+ mode=IKEModeChoices.AGGRESSIVE,
+ ),
+ )
+ IKEPolicy.objects.bulk_create(ike_policies)
+ ike_policies[0].proposals.add(ike_proposals[0])
+ ike_policies[1].proposals.add(ike_proposals[1])
+ ike_policies[2].proposals.add(ike_proposals[2])
+
+ def test_name(self):
+ params = {'name': ['IKE Policy 1', 'IKE Policy 2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_version(self):
+ params = {'version': [IKEVersionChoices.VERSION_1]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_mode(self):
+ params = {'mode': [IKEModeChoices.MAIN]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_proposal(self):
+ proposals = IKEProposal.objects.all()[:2]
+ params = {'proposal_id': [proposals[0].pk, proposals[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'proposal': [proposals[0].name, proposals[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
+class IPSecProposalTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = IPSecProposal.objects.all()
+ filterset = IPSecProposalFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ ipsec_proposals = (
+ IPSecProposal(
+ name='IPSec Proposal 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ sa_lifetime_seconds=1000,
+ sa_lifetime_data=1000
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 2',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ sa_lifetime_seconds=2000,
+ sa_lifetime_data=2000
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 3',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA512,
+ sa_lifetime_seconds=3000,
+ sa_lifetime_data=3000
+ ),
+ )
+ IPSecProposal.objects.bulk_create(ipsec_proposals)
+
+ def test_name(self):
+ params = {'name': ['IPSec Proposal 1', 'IPSec Proposal 2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_encryption_algorithm(self):
+ params = {'encryption_algorithm': [
+ EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC, EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC
+ ]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_authentication_algorithm(self):
+ params = {'authentication_algorithm': [
+ AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1, AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256
+ ]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_sa_lifetime_seconds(self):
+ params = {'sa_lifetime_seconds': [1000, 2000]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_sa_lifetime_data(self):
+ params = {'sa_lifetime_data': [1000, 2000]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
+class IPSecPolicyTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = IPSecPolicy.objects.all()
+ filterset = IPSecPolicyFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ ipsec_proposals = (
+ IPSecProposal(
+ name='IPSec Policy 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 2',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 3',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ )
+ IPSecProposal.objects.bulk_create(ipsec_proposals)
+
+ ipsec_policies = (
+ IPSecPolicy(
+ name='IPSec Policy 1',
+ pfs_group=DHGroupChoices.GROUP_1
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 2',
+ pfs_group=DHGroupChoices.GROUP_2
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 3',
+ pfs_group=DHGroupChoices.GROUP_5
+ ),
+ )
+ IPSecPolicy.objects.bulk_create(ipsec_policies)
+ ipsec_policies[0].proposals.add(ipsec_proposals[0])
+ ipsec_policies[1].proposals.add(ipsec_proposals[1])
+ ipsec_policies[2].proposals.add(ipsec_proposals[2])
+
+ def test_name(self):
+ params = {'name': ['IPSec Policy 1', 'IPSec Policy 2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_pfs_group(self):
+ params = {'pfs_group': [DHGroupChoices.GROUP_1, DHGroupChoices.GROUP_2]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_proposal(self):
+ proposals = IPSecProposal.objects.all()[:2]
+ params = {'proposal_id': [proposals[0].pk, proposals[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'proposal': [proposals[0].name, proposals[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
+class IPSecProfileTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = IPSecProfile.objects.all()
+ filterset = IPSecProfileFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ ike_proposal = IKEProposal.objects.create(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ )
+ ipsec_proposal = IPSecProposal.objects.create(
+ name='IPSec Proposal 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ )
+
+ ike_policies = (
+ IKEPolicy(
+ name='IKE Policy 1',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 2',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 3',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ )
+ IKEPolicy.objects.bulk_create(ike_policies)
+ for ike_policy in ike_policies:
+ ike_policy.proposals.add(ike_proposal)
+
+ ipsec_policies = (
+ IPSecPolicy(
+ name='IPSec Policy 1',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 2',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 3',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IPSecPolicy.objects.bulk_create(ipsec_policies)
+ for ipsec_policy in ipsec_policies:
+ ipsec_policy.proposals.add(ipsec_proposal)
+
+ ipsec_profiles = (
+ IPSecProfile(
+ name='IPSec Profile 1',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[0],
+ ipsec_policy=ipsec_policies[0]
+ ),
+ IPSecProfile(
+ name='IPSec Profile 2',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[1],
+ ipsec_policy=ipsec_policies[1]
+ ),
+ IPSecProfile(
+ name='IPSec Profile 3',
+ mode=IPSecModeChoices.AH,
+ ike_policy=ike_policies[2],
+ ipsec_policy=ipsec_policies[2]
+ ),
+ )
+ IPSecProfile.objects.bulk_create(ipsec_profiles)
+
+ def test_name(self):
+ params = {'name': ['IPSec Profile 1', 'IPSec Profile 2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_mode(self):
+ params = {'mode': [IPSecModeChoices.ESP]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_ike_policy(self):
+ ike_policies = IKEPolicy.objects.all()[:2]
+ params = {'ike_policy_id': [ike_policies[0].pk, ike_policies[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'ike_policy': [ike_policies[0].name, ike_policies[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_ipsec_policy(self):
+ ipsec_policies = IPSecPolicy.objects.all()[:2]
+ params = {'ipsec_policy_id': [ipsec_policies[0].pk, ipsec_policies[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'ipsec_policy': [ipsec_policies[0].name, ipsec_policies[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
diff --git a/netbox/vpn/tests/test_views.py b/netbox/vpn/tests/test_views.py
new file mode 100644
index 00000000000..433eca4679e
--- /dev/null
+++ b/netbox/vpn/tests/test_views.py
@@ -0,0 +1,508 @@
+from dcim.choices import InterfaceTypeChoices
+from dcim.models import Interface
+from vpn.choices import *
+from vpn.models import *
+from utilities.testing import ViewTestCases, create_tags, create_test_device
+
+
+class TunnelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = Tunnel
+
+ @classmethod
+ def setUpTestData(cls):
+
+ tunnels = (
+ Tunnel(
+ name='Tunnel 1',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ Tunnel(
+ name='Tunnel 2',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ Tunnel(
+ name='Tunnel 3',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ )
+ Tunnel.objects.bulk_create(tunnels)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'Tunnel X',
+ 'description': 'New tunnel',
+ 'status': TunnelStatusChoices.STATUS_PLANNED,
+ 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "name,status,encapsulation",
+ "Tunnel 4,planned,gre",
+ "Tunnel 5,planned,gre",
+ "Tunnel 6,planned,gre",
+ )
+
+ cls.csv_update_data = (
+ "id,status,encapsulation",
+ f"{tunnels[0].pk},active,ip-ip",
+ f"{tunnels[1].pk},active,ip-ip",
+ f"{tunnels[2].pk},active,ip-ip",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'status': TunnelStatusChoices.STATUS_DISABLED,
+ 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
+ }
+
+
+class TunnelTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = TunnelTermination
+ # TODO: Workaround for conflict between form field and GFK
+ validation_excluded_fields = ('termination',)
+
+ @classmethod
+ def setUpTestData(cls):
+ device = create_test_device('Device 1')
+ interfaces = (
+ Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 4', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 5', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 6', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 7', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ )
+ Interface.objects.bulk_create(interfaces)
+
+ tunnel = Tunnel.objects.create(
+ name='Tunnel 1',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ )
+
+ tunnel_terminations = (
+ TunnelTermination(
+ tunnel=tunnel,
+ role=TunnelTerminationRoleChoices.ROLE_HUB,
+ termination=interfaces[0]
+ ),
+ TunnelTermination(
+ tunnel=tunnel,
+ role=TunnelTerminationRoleChoices.ROLE_SPOKE,
+ termination=interfaces[1]
+ ),
+ TunnelTermination(
+ tunnel=tunnel,
+ role=TunnelTerminationRoleChoices.ROLE_SPOKE,
+ termination=interfaces[2]
+ ),
+ )
+ TunnelTermination.objects.bulk_create(tunnel_terminations)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'tunnel': tunnel.pk,
+ 'role': TunnelTerminationRoleChoices.ROLE_PEER,
+ 'type': TunnelTerminationTypeChoices.TYPE_DEVICE,
+ 'parent': device.pk,
+ 'termination': interfaces[6].pk,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "tunnel,role,device,termination",
+ "Tunnel 1,peer,Device 1,Interface 4",
+ "Tunnel 1,peer,Device 1,Interface 5",
+ "Tunnel 1,peer,Device 1,Interface 6",
+ )
+
+ cls.csv_update_data = (
+ "id,role",
+ f"{tunnel_terminations[0].pk},peer",
+ f"{tunnel_terminations[1].pk},peer",
+ f"{tunnel_terminations[2].pk},peer",
+ )
+
+ cls.bulk_edit_data = {
+ 'role': TunnelTerminationRoleChoices.ROLE_PEER,
+ }
+
+
+class IKEProposalTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = IKEProposal
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ike_proposals = (
+ IKEProposal(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 2',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 3',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IKEProposal.objects.bulk_create(ike_proposals)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'IKE Proposal X',
+ 'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'group': DHGroupChoices.GROUP_19,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "name,authentication_method,encryption_algorithm,authentication_algorithm,group",
+ "IKE Proposal 4,preshared-keys,aes-128-cbc,hmac-sha1,14",
+ "IKE Proposal 5,preshared-keys,aes-128-cbc,hmac-sha1,14",
+ "IKE Proposal 6,preshared-keys,aes-128-cbc,hmac-sha1,14",
+ )
+
+ cls.csv_update_data = (
+ "id,description",
+ f"{ike_proposals[0].pk},New description",
+ f"{ike_proposals[1].pk},New description",
+ f"{ike_proposals[2].pk},New description",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'group': DHGroupChoices.GROUP_19
+ }
+
+
+class IKEPolicyTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = IKEPolicy
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ike_proposals = (
+ IKEProposal(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 2',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IKEProposal.objects.bulk_create(ike_proposals)
+
+ ike_policies = (
+ IKEPolicy(
+ name='IKE Policy 1',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 2',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 3',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ )
+ IKEPolicy.objects.bulk_create(ike_policies)
+ for ike_policy in ike_policies:
+ ike_policy.proposals.set(ike_proposals)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'IKE Policy X',
+ 'version': IKEVersionChoices.VERSION_2,
+ 'mode': IKEModeChoices.AGGRESSIVE,
+ 'proposals': [p.pk for p in ike_proposals],
+ 'tags': [t.pk for t in tags],
+ }
+
+ ike_proposal_names = ','.join([p.name for p in ike_proposals])
+ cls.csv_data = (
+ "name,version,mode,proposals",
+ f"IKE Proposal 4,2,aggressive,\"{ike_proposal_names}\"",
+ f"IKE Proposal 5,2,aggressive,\"{ike_proposal_names}\"",
+ f"IKE Proposal 6,2,aggressive,\"{ike_proposal_names}\"",
+ )
+
+ cls.csv_update_data = (
+ "id,description",
+ f"{ike_policies[0].pk},New description",
+ f"{ike_policies[1].pk},New description",
+ f"{ike_policies[2].pk},New description",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'version': IKEVersionChoices.VERSION_2,
+ 'mode': IKEModeChoices.AGGRESSIVE,
+ }
+
+
+class IPSecProposalTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = IPSecProposal
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ipsec_proposals = (
+ IPSecProposal(
+ name='IPSec Proposal 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 2',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 3',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ ),
+ )
+ IPSecProposal.objects.bulk_create(ipsec_proposals)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'IPSec Proposal X',
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'sa_lifetime_seconds': 3600,
+ 'sa_lifetime_data': 1000000,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "name,encryption_algorithm,authentication_algorithm,sa_lifetime_seconds,sa_lifetime_data",
+ "IKE Proposal 4,aes-128-cbc,hmac-sha1,3600,1000000",
+ "IKE Proposal 5,aes-128-cbc,hmac-sha1,3600,1000000",
+ "IKE Proposal 6,aes-128-cbc,hmac-sha1,3600,1000000",
+ )
+
+ cls.csv_update_data = (
+ "id,description",
+ f"{ipsec_proposals[0].pk},New description",
+ f"{ipsec_proposals[1].pk},New description",
+ f"{ipsec_proposals[2].pk},New description",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'sa_lifetime_seconds': 3600,
+ 'sa_lifetime_data': 1000000,
+ }
+
+
+class IPSecPolicyTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = IPSecPolicy
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ipsec_proposals = (
+ IPSecProposal(
+ name='IPSec Policy 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 2',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ )
+ IPSecProposal.objects.bulk_create(ipsec_proposals)
+
+ ipsec_policies = (
+ IPSecPolicy(
+ name='IPSec Policy 1',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 2',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 3',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IPSecPolicy.objects.bulk_create(ipsec_policies)
+ for ipsec_policy in ipsec_policies:
+ ipsec_policy.proposals.set(ipsec_proposals)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'IPSec Policy X',
+ 'pfs_group': DHGroupChoices.GROUP_5,
+ 'proposals': [p.pk for p in ipsec_proposals],
+ 'tags': [t.pk for t in tags],
+ }
+
+ ipsec_proposal_names = ','.join([p.name for p in ipsec_proposals])
+ cls.csv_data = (
+ "name,pfs_group,proposals",
+ f"IKE Proposal 4,19,\"{ipsec_proposal_names}\"",
+ f"IKE Proposal 5,19,\"{ipsec_proposal_names}\"",
+ f"IKE Proposal 6,19,\"{ipsec_proposal_names}\"",
+ )
+
+ cls.csv_update_data = (
+ "id,description",
+ f"{ipsec_policies[0].pk},New description",
+ f"{ipsec_policies[1].pk},New description",
+ f"{ipsec_policies[2].pk},New description",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'pfs_group': DHGroupChoices.GROUP_5,
+ }
+
+
+class IPSecProfileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = IPSecProfile
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ike_proposal = IKEProposal.objects.create(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ )
+
+ ipsec_proposal = IPSecProposal.objects.create(
+ name='IPSec Proposal 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ )
+
+ ike_policies = (
+ IKEPolicy(
+ name='IKE Policy 1',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 2',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ )
+ IKEPolicy.objects.bulk_create(ike_policies)
+ for ike_policy in ike_policies:
+ ike_policy.proposals.add(ike_proposal)
+
+ ipsec_policies = (
+ IPSecPolicy(
+ name='IPSec Policy 1',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 2',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IPSecPolicy.objects.bulk_create(ipsec_policies)
+ for ipsec_policy in ipsec_policies:
+ ipsec_policy.proposals.add(ipsec_proposal)
+
+ ipsec_profiles = (
+ IPSecProfile(
+ name='IPSec Profile 1',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[0],
+ ipsec_policy=ipsec_policies[0]
+ ),
+ IPSecProfile(
+ name='IPSec Profile 2',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[0],
+ ipsec_policy=ipsec_policies[0]
+ ),
+ IPSecProfile(
+ name='IPSec Profile 3',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[0],
+ ipsec_policy=ipsec_policies[0]
+ ),
+ )
+ IPSecProfile.objects.bulk_create(ipsec_profiles)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'IPSec Profile X',
+ 'mode': IPSecModeChoices.AH,
+ 'ike_policy': ike_policies[1].pk,
+ 'ipsec_policy': ipsec_policies[1].pk,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "name,mode,ike_policy,ipsec_policy",
+ f"IKE Proposal 4,ah,IKE Policy 2,IPSec Policy 2",
+ f"IKE Proposal 5,ah,IKE Policy 2,IPSec Policy 2",
+ f"IKE Proposal 6,ah,IKE Policy 2,IPSec Policy 2",
+ )
+
+ cls.csv_update_data = (
+ "id,description",
+ f"{ipsec_profiles[0].pk},New description",
+ f"{ipsec_profiles[1].pk},New description",
+ f"{ipsec_profiles[2].pk},New description",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'mode': IPSecModeChoices.AH,
+ 'ike_policy': ike_policies[1].pk,
+ 'ipsec_policy': ipsec_policies[1].pk,
+ }
diff --git a/netbox/vpn/urls.py b/netbox/vpn/urls.py
new file mode 100644
index 00000000000..7fe54824548
--- /dev/null
+++ b/netbox/vpn/urls.py
@@ -0,0 +1,65 @@
+from django.urls import include, path
+
+from utilities.urls import get_model_urls
+from . import views
+
+app_name = 'vpn'
+urlpatterns = [
+
+ # Tunnels
+ path('tunnels/', views.TunnelListView.as_view(), name='tunnel_list'),
+ path('tunnels/add/', views.TunnelEditView.as_view(), name='tunnel_add'),
+ path('tunnels/import/', views.TunnelBulkImportView.as_view(), name='tunnel_import'),
+ path('tunnels/edit/', views.TunnelBulkEditView.as_view(), name='tunnel_bulk_edit'),
+ path('tunnels/delete/', views.TunnelBulkDeleteView.as_view(), name='tunnel_bulk_delete'),
+ path('tunnels/
/', include(get_model_urls('vpn', 'tunnel'))),
+
+ # Tunnel terminations
+ path('tunnel-terminations/', views.TunnelTerminationListView.as_view(), name='tunneltermination_list'),
+ path('tunnel-terminations/add/', views.TunnelTerminationEditView.as_view(), name='tunneltermination_add'),
+ path('tunnel-terminations/import/', views.TunnelTerminationBulkImportView.as_view(), name='tunneltermination_import'),
+ path('tunnel-terminations/edit/', views.TunnelTerminationBulkEditView.as_view(), name='tunneltermination_bulk_edit'),
+ path('tunnel-terminations/delete/', views.TunnelTerminationBulkDeleteView.as_view(), name='tunneltermination_bulk_delete'),
+ path('tunnel-terminations//', include(get_model_urls('vpn', 'tunneltermination'))),
+
+ # IKE proposals
+ path('ike-proposals/', views.IKEProposalListView.as_view(), name='ikeproposal_list'),
+ path('ike-proposals/add/', views.IKEProposalEditView.as_view(), name='ikeproposal_add'),
+ path('ike-proposals/import/', views.IKEProposalBulkImportView.as_view(), name='ikeproposal_import'),
+ path('ike-proposals/edit/', views.IKEProposalBulkEditView.as_view(), name='ikeproposal_bulk_edit'),
+ path('ike-proposals/delete/', views.IKEProposalBulkDeleteView.as_view(), name='ikeproposal_bulk_delete'),
+ path('ike-proposals//', include(get_model_urls('vpn', 'ikeproposal'))),
+
+ # IKE policies
+ path('ike-policys/', views.IKEPolicyListView.as_view(), name='ikepolicy_list'),
+ path('ike-policys/add/', views.IKEPolicyEditView.as_view(), name='ikepolicy_add'),
+ path('ike-policys/import/', views.IKEPolicyBulkImportView.as_view(), name='ikepolicy_import'),
+ path('ike-policys/edit/', views.IKEPolicyBulkEditView.as_view(), name='ikepolicy_bulk_edit'),
+ path('ike-policys/delete/', views.IKEPolicyBulkDeleteView.as_view(), name='ikepolicy_bulk_delete'),
+ path('ike-policys//', include(get_model_urls('vpn', 'ikepolicy'))),
+
+ # IPSec proposals
+ path('ipsec-proposals/', views.IPSecProposalListView.as_view(), name='ipsecproposal_list'),
+ path('ipsec-proposals/add/', views.IPSecProposalEditView.as_view(), name='ipsecproposal_add'),
+ path('ipsec-proposals/import/', views.IPSecProposalBulkImportView.as_view(), name='ipsecproposal_import'),
+ path('ipsec-proposals/edit/', views.IPSecProposalBulkEditView.as_view(), name='ipsecproposal_bulk_edit'),
+ path('ipsec-proposals/delete/', views.IPSecProposalBulkDeleteView.as_view(), name='ipsecproposal_bulk_delete'),
+ path('ipsec-proposals//', include(get_model_urls('vpn', 'ipsecproposal'))),
+
+ # IPSec policies
+ path('ipsec-policys/', views.IPSecPolicyListView.as_view(), name='ipsecpolicy_list'),
+ path('ipsec-policys/add/', views.IPSecPolicyEditView.as_view(), name='ipsecpolicy_add'),
+ path('ipsec-policys/import/', views.IPSecPolicyBulkImportView.as_view(), name='ipsecpolicy_import'),
+ path('ipsec-policys/edit/', views.IPSecPolicyBulkEditView.as_view(), name='ipsecpolicy_bulk_edit'),
+ path('ipsec-policys/delete/', views.IPSecPolicyBulkDeleteView.as_view(), name='ipsecpolicy_bulk_delete'),
+ path('ipsec-policys//', include(get_model_urls('vpn', 'ipsecpolicy'))),
+
+ # IPSec profiles
+ path('ipsec-profiles/', views.IPSecProfileListView.as_view(), name='ipsecprofile_list'),
+ path('ipsec-profiles/add/', views.IPSecProfileEditView.as_view(), name='ipsecprofile_add'),
+ path('ipsec-profiles/import/', views.IPSecProfileBulkImportView.as_view(), name='ipsecprofile_import'),
+ path('ipsec-profiles/edit/', views.IPSecProfileBulkEditView.as_view(), name='ipsecprofile_bulk_edit'),
+ path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'),
+ path('ipsec-profiles//', include(get_model_urls('vpn', 'ipsecprofile'))),
+
+]
diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py
new file mode 100644
index 00000000000..56eadc07715
--- /dev/null
+++ b/netbox/vpn/views.py
@@ -0,0 +1,334 @@
+from netbox.views import generic
+from utilities.utils import count_related
+from utilities.views import register_model_view
+from . import filtersets, forms, tables
+from .models import *
+
+
+#
+# Tunnels
+#
+
+class TunnelListView(generic.ObjectListView):
+ queryset = Tunnel.objects.annotate(
+ count_terminations=count_related(TunnelTermination, 'tunnel')
+ )
+ filterset = filtersets.TunnelFilterSet
+ filterset_form = forms.TunnelFilterForm
+ table = tables.TunnelTable
+
+
+@register_model_view(Tunnel)
+class TunnelView(generic.ObjectView):
+ queryset = Tunnel.objects.all()
+
+
+@register_model_view(Tunnel, 'edit')
+class TunnelEditView(generic.ObjectEditView):
+ queryset = Tunnel.objects.all()
+ form = forms.TunnelForm
+
+ def dispatch(self, request, *args, **kwargs):
+
+ # If creating a new Tunnel, use the creation form
+ if 'pk' not in kwargs:
+ self.form = forms.TunnelCreateForm
+
+ return super().dispatch(request, *args, **kwargs)
+
+
+@register_model_view(Tunnel, 'delete')
+class TunnelDeleteView(generic.ObjectDeleteView):
+ queryset = Tunnel.objects.all()
+
+
+class TunnelBulkImportView(generic.BulkImportView):
+ queryset = Tunnel.objects.all()
+ model_form = forms.TunnelImportForm
+
+
+class TunnelBulkEditView(generic.BulkEditView):
+ queryset = Tunnel.objects.annotate(
+ count_terminations=count_related(TunnelTermination, 'tunnel')
+ )
+ filterset = filtersets.TunnelFilterSet
+ table = tables.TunnelTable
+ form = forms.TunnelBulkEditForm
+
+
+class TunnelBulkDeleteView(generic.BulkDeleteView):
+ queryset = Tunnel.objects.annotate(
+ count_terminations=count_related(TunnelTermination, 'tunnel')
+ )
+ filterset = filtersets.TunnelFilterSet
+ table = tables.TunnelTable
+
+
+#
+# Tunnel terminations
+#
+
+class TunnelTerminationListView(generic.ObjectListView):
+ queryset = TunnelTermination.objects.all()
+ filterset = filtersets.TunnelTerminationFilterSet
+ filterset_form = forms.TunnelTerminationFilterForm
+ table = tables.TunnelTerminationTable
+
+
+@register_model_view(TunnelTermination)
+class TunnelTerminationView(generic.ObjectView):
+ queryset = TunnelTermination.objects.all()
+
+
+@register_model_view(TunnelTermination, 'edit')
+class TunnelTerminationEditView(generic.ObjectEditView):
+ queryset = TunnelTermination.objects.all()
+ form = forms.TunnelTerminationForm
+
+
+@register_model_view(TunnelTermination, 'delete')
+class TunnelTerminationDeleteView(generic.ObjectDeleteView):
+ queryset = TunnelTermination.objects.all()
+
+
+class TunnelTerminationBulkImportView(generic.BulkImportView):
+ queryset = TunnelTermination.objects.all()
+ model_form = forms.TunnelTerminationImportForm
+
+
+class TunnelTerminationBulkEditView(generic.BulkEditView):
+ queryset = TunnelTermination.objects.all()
+ filterset = filtersets.TunnelTerminationFilterSet
+ table = tables.TunnelTerminationTable
+ form = forms.TunnelTerminationBulkEditForm
+
+
+class TunnelTerminationBulkDeleteView(generic.BulkDeleteView):
+ queryset = TunnelTermination.objects.all()
+ filterset = filtersets.TunnelTerminationFilterSet
+ table = tables.TunnelTerminationTable
+
+
+#
+# IKE proposals
+#
+
+class IKEProposalListView(generic.ObjectListView):
+ queryset = IKEProposal.objects.all()
+ filterset = filtersets.IKEProposalFilterSet
+ filterset_form = forms.IKEProposalFilterForm
+ table = tables.IKEProposalTable
+
+
+@register_model_view(IKEProposal)
+class IKEProposalView(generic.ObjectView):
+ queryset = IKEProposal.objects.all()
+
+
+@register_model_view(IKEProposal, 'edit')
+class IKEProposalEditView(generic.ObjectEditView):
+ queryset = IKEProposal.objects.all()
+ form = forms.IKEProposalForm
+
+
+@register_model_view(IKEProposal, 'delete')
+class IKEProposalDeleteView(generic.ObjectDeleteView):
+ queryset = IKEProposal.objects.all()
+
+
+class IKEProposalBulkImportView(generic.BulkImportView):
+ queryset = IKEProposal.objects.all()
+ model_form = forms.IKEProposalImportForm
+
+
+class IKEProposalBulkEditView(generic.BulkEditView):
+ queryset = IKEProposal.objects.all()
+ filterset = filtersets.IKEProposalFilterSet
+ table = tables.IKEProposalTable
+ form = forms.IKEProposalBulkEditForm
+
+
+class IKEProposalBulkDeleteView(generic.BulkDeleteView):
+ queryset = IKEProposal.objects.all()
+ filterset = filtersets.IKEProposalFilterSet
+ table = tables.IKEProposalTable
+
+
+#
+# IKE policies
+#
+
+class IKEPolicyListView(generic.ObjectListView):
+ queryset = IKEPolicy.objects.all()
+ filterset = filtersets.IKEPolicyFilterSet
+ filterset_form = forms.IKEPolicyFilterForm
+ table = tables.IKEPolicyTable
+
+
+@register_model_view(IKEPolicy)
+class IKEPolicyView(generic.ObjectView):
+ queryset = IKEPolicy.objects.all()
+
+
+@register_model_view(IKEPolicy, 'edit')
+class IKEPolicyEditView(generic.ObjectEditView):
+ queryset = IKEPolicy.objects.all()
+ form = forms.IKEPolicyForm
+
+
+@register_model_view(IKEPolicy, 'delete')
+class IKEPolicyDeleteView(generic.ObjectDeleteView):
+ queryset = IKEPolicy.objects.all()
+
+
+class IKEPolicyBulkImportView(generic.BulkImportView):
+ queryset = IKEPolicy.objects.all()
+ model_form = forms.IKEPolicyImportForm
+
+
+class IKEPolicyBulkEditView(generic.BulkEditView):
+ queryset = IKEPolicy.objects.all()
+ filterset = filtersets.IKEPolicyFilterSet
+ table = tables.IKEPolicyTable
+ form = forms.IKEPolicyBulkEditForm
+
+
+class IKEPolicyBulkDeleteView(generic.BulkDeleteView):
+ queryset = IKEPolicy.objects.all()
+ filterset = filtersets.IKEPolicyFilterSet
+ table = tables.IKEPolicyTable
+
+
+#
+# IPSec proposals
+#
+
+class IPSecProposalListView(generic.ObjectListView):
+ queryset = IPSecProposal.objects.all()
+ filterset = filtersets.IPSecProposalFilterSet
+ filterset_form = forms.IPSecProposalFilterForm
+ table = tables.IPSecProposalTable
+
+
+@register_model_view(IPSecProposal)
+class IPSecProposalView(generic.ObjectView):
+ queryset = IPSecProposal.objects.all()
+
+
+@register_model_view(IPSecProposal, 'edit')
+class IPSecProposalEditView(generic.ObjectEditView):
+ queryset = IPSecProposal.objects.all()
+ form = forms.IPSecProposalForm
+
+
+@register_model_view(IPSecProposal, 'delete')
+class IPSecProposalDeleteView(generic.ObjectDeleteView):
+ queryset = IPSecProposal.objects.all()
+
+
+class IPSecProposalBulkImportView(generic.BulkImportView):
+ queryset = IPSecProposal.objects.all()
+ model_form = forms.IPSecProposalImportForm
+
+
+class IPSecProposalBulkEditView(generic.BulkEditView):
+ queryset = IPSecProposal.objects.all()
+ filterset = filtersets.IPSecProposalFilterSet
+ table = tables.IPSecProposalTable
+ form = forms.IPSecProposalBulkEditForm
+
+
+class IPSecProposalBulkDeleteView(generic.BulkDeleteView):
+ queryset = IPSecProposal.objects.all()
+ filterset = filtersets.IPSecProposalFilterSet
+ table = tables.IPSecProposalTable
+
+
+#
+# IPSec policies
+#
+
+class IPSecPolicyListView(generic.ObjectListView):
+ queryset = IPSecPolicy.objects.all()
+ filterset = filtersets.IPSecPolicyFilterSet
+ filterset_form = forms.IPSecPolicyFilterForm
+ table = tables.IPSecPolicyTable
+
+
+@register_model_view(IPSecPolicy)
+class IPSecPolicyView(generic.ObjectView):
+ queryset = IPSecPolicy.objects.all()
+
+
+@register_model_view(IPSecPolicy, 'edit')
+class IPSecPolicyEditView(generic.ObjectEditView):
+ queryset = IPSecPolicy.objects.all()
+ form = forms.IPSecPolicyForm
+
+
+@register_model_view(IPSecPolicy, 'delete')
+class IPSecPolicyDeleteView(generic.ObjectDeleteView):
+ queryset = IPSecPolicy.objects.all()
+
+
+class IPSecPolicyBulkImportView(generic.BulkImportView):
+ queryset = IPSecPolicy.objects.all()
+ model_form = forms.IPSecPolicyImportForm
+
+
+class IPSecPolicyBulkEditView(generic.BulkEditView):
+ queryset = IPSecPolicy.objects.all()
+ filterset = filtersets.IPSecPolicyFilterSet
+ table = tables.IPSecPolicyTable
+ form = forms.IPSecPolicyBulkEditForm
+
+
+class IPSecPolicyBulkDeleteView(generic.BulkDeleteView):
+ queryset = IPSecPolicy.objects.all()
+ filterset = filtersets.IPSecPolicyFilterSet
+ table = tables.IPSecPolicyTable
+
+
+#
+# IPSec profiles
+#
+
+class IPSecProfileListView(generic.ObjectListView):
+ queryset = IPSecProfile.objects.all()
+ filterset = filtersets.IPSecProfileFilterSet
+ filterset_form = forms.IPSecProfileFilterForm
+ table = tables.IPSecProfileTable
+
+
+@register_model_view(IPSecProfile)
+class IPSecProfileView(generic.ObjectView):
+ queryset = IPSecProfile.objects.all()
+
+
+@register_model_view(IPSecProfile, 'edit')
+class IPSecProfileEditView(generic.ObjectEditView):
+ queryset = IPSecProfile.objects.all()
+ form = forms.IPSecProfileForm
+
+
+@register_model_view(IPSecProfile, 'delete')
+class IPSecProfileDeleteView(generic.ObjectDeleteView):
+ queryset = IPSecProfile.objects.all()
+
+
+class IPSecProfileBulkImportView(generic.BulkImportView):
+ queryset = IPSecProfile.objects.all()
+ model_form = forms.IPSecProfileImportForm
+
+
+class IPSecProfileBulkEditView(generic.BulkEditView):
+ queryset = IPSecProfile.objects.all()
+ filterset = filtersets.IPSecProfileFilterSet
+ table = tables.IPSecProfileTable
+ form = forms.IPSecProfileBulkEditForm
+
+
+class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
+ queryset = IPSecProfile.objects.all()
+ filterset = filtersets.IPSecProfileFilterSet
+ table = tables.IPSecProfileTable