Firewall rule management

This commit is contained in:
Eduardo Silva 2024-02-29 23:07:40 -03:00
parent 9621bf800f
commit 2012c22973
19 changed files with 1062 additions and 67 deletions

View File

@ -1,5 +1,5 @@
from django.contrib import admin
from firewall.models import RedirectRule
from firewall.models import RedirectRule, FirewallRule, FirewallSettings
class RedirectRuleAdmin(admin.ModelAdmin):
@ -8,3 +8,16 @@ class RedirectRuleAdmin(admin.ModelAdmin):
admin.site.register(RedirectRule, RedirectRuleAdmin)
class FirewallRuleAdmin(admin.ModelAdmin):
list_display = ('firewall_chain', 'description', 'in_interface', 'out_interface', 'source_ip', 'source_netmask', 'source_peer_include_networks', 'not_source', 'destination_ip', 'destination_netmask', 'destination_peer_include_networks', 'not_destination', 'protocol', 'destination_port', 'state_new', 'state_related', 'state_established', 'state_invalid', 'state_untracked', 'not_state', 'rule_action', 'sort_order')
search_fields = ('firewall_chain', 'description', 'in_interface', 'out_interface', 'source_ip', 'source_netmask', 'source_peer_include_networks', 'not_source', 'destination_ip', 'destination_netmask', 'destination_peer_include_networks', 'not_destination', 'protocol', 'destination_port', 'state_new', 'state_related', 'state_established', 'state_invalid', 'state_untracked', 'not_state', 'rule_action', 'sort_order')
admin.site.register(FirewallRule, FirewallRuleAdmin)
class FirewallSettingsAdmin(admin.ModelAdmin):
list_display = ('wan_interface', 'default_forward_policy', 'default_output_policy', 'allow_peer_to_peer', 'allow_instance_to_instance')
admin.site.register(FirewallSettings, FirewallSettingsAdmin)

View File

@ -1,5 +1,5 @@
from firewall.models import RedirectRule
from wireguard.models import Peer, WireGuardInstance
from firewall.models import RedirectRule, FirewallRule, FirewallSettings
from wireguard.models import Peer, WireGuardInstance, NETMASK_CHOICES
from django import forms
@ -34,7 +34,7 @@ class RedirectRuleForm(forms.ModelForm):
raise forms.ValidationError("Port 8000 (tcp) is reserved for wireguard-webadmin.")
if protocol == 'udp':
if WireGuardInstance.objects.filter(udp_port=port).exists():
if WireGuardInstance.objects.filter(listen_port=port).exists():
raise forms.ValidationError("Port " + str(port) + " (udp) is already in use by a WireGuard instance.")
if peer and ip_address:
@ -50,3 +50,47 @@ class RedirectRuleForm(forms.ModelForm):
cleaned_data['peer'] = None
return cleaned_data
class FirewallRuleForm(forms.ModelForm):
firewall_settings, firewall_settings_created = FirewallSettings.objects.get_or_create(name='global')
interface_list = [('', '------'),]
interface_list.append((firewall_settings.wan_interface, firewall_settings.wan_interface + ' (WAN)'))
for wireguard_instance in WireGuardInstance.objects.all().order_by('instance_id'):
wireguard_instance_interface = 'wg'+ str(wireguard_instance.instance_id)
interface_list.append((wireguard_instance_interface, wireguard_instance_interface))
interface_list.append(('wg+', 'wg+ (Any WireGuard Interface)'))
description = forms.CharField(label='Description', required=False)
firewall_chain = forms.ChoiceField(label='Firewall Chain', choices=[('forward', 'FORWARD'), ('postrouting', 'POSTROUTING (nat)')], initial='forward')
in_interface = forms.ChoiceField(label='In Interface', choices=interface_list, required=False)
out_interface = forms.ChoiceField(label='Out Interface', choices=interface_list, required=False)
source_ip = forms.GenericIPAddressField(label='Source IP', required=False)
source_netmask = forms.IntegerField(label='Source Netmask', initial=32, min_value=0, max_value=32)
source_peer = forms.ModelMultipleChoiceField(label='Source Peer', queryset=Peer.objects.all(), required=False)
source_peer_include_networks = forms.BooleanField(label='Source Peer Include Networks', required=False)
not_source = forms.BooleanField(label='Not Source', required=False)
destination_ip = forms.GenericIPAddressField(label='Destination IP', required=False)
destination_netmask = forms.IntegerField(label='Destination Netmask', initial=32, min_value=0, max_value=32)
destination_peer = forms.ModelMultipleChoiceField(label='Destination Peer', queryset=Peer.objects.all(), required=False)
destination_peer_include_networks = forms.BooleanField(label='Destination Peer Include Networks', required=False)
not_destination = forms.BooleanField(label='Not Destination', required=False)
protocol = forms.ChoiceField(label='Protocol', choices=[('', 'all'), ('tcp', 'TCP'), ('udp', 'UDP'), ('both', 'TCP+UDP'), ('icmp', 'ICMP')], required=False)
destination_port = forms.CharField(label='Destination Port', required=False)
state_new = forms.BooleanField(label='State NEW', required=False)
state_related = forms.BooleanField(label='State RELATED', required=False)
state_established = forms.BooleanField(label='State ESTABLISHED', required=False)
state_invalid = forms.BooleanField(label='State INVALID', required=False)
state_untracked = forms.BooleanField(label='State UNTRACKED', required=False)
not_state = forms.BooleanField(label='Not State', required=False)
rule_action = forms.ChoiceField(label='Rule Action', initial='accept', choices=[('accept', 'ACCEPT'), ('reject', 'REJECT'), ('drop', 'DROP'), ('masquerade', 'MASQUERADE')])
sort_order = forms.IntegerField(label='Sort Order', initial=0, min_value=0)
class Meta:
model = FirewallRule
fields = ['description', 'firewall_chain', 'in_interface', 'out_interface', 'source_ip', 'source_netmask', 'source_peer', 'source_peer_include_networks', 'not_source', 'destination_ip', 'destination_netmask', 'destination_peer', 'destination_peer_include_networks', 'not_destination', 'protocol', 'destination_port', 'state_new', 'state_related', 'state_established', 'state_invalid', 'state_untracked', 'not_state', 'rule_action', 'sort_order']

View File

@ -0,0 +1,61 @@
# Generated by Django 5.0.2 on 2024-02-28 15:37
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('firewall', '0002_redirectrule_masquerade_source_and_more'),
('wireguard', '0018_wireguardinstance_legacy_firewall'),
]
operations = [
migrations.CreateModel(
name='FirewallSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('global', models.CharField(max_length=6, unique=True)),
('default_forward_policy', models.CharField(choices=[('accept', 'ACCEPT'), ('reject', 'REJECT'), ('drop', 'DROP')], default='accept', max_length=6)),
('default_output_policy', models.CharField(choices=[('accept', 'ACCEPT'), ('reject', 'REJECT'), ('drop', 'DROP')], default='accept', max_length=6)),
('allow_peer_to_peer', models.BooleanField(default=True)),
('allow_instance_to_instance', models.BooleanField(default=True)),
('wan_interface', models.CharField(default='eth0', max_length=12)),
],
),
migrations.CreateModel(
name='ForwardRule',
fields=[
('description', models.CharField(blank=True, max_length=100, null=True)),
('firewall_chain', models.CharField(choices=[('FORWARD', 'FORWARD'), ('OUTPUT', 'OUTPUT'), ('POSTROUTING', 'POSTROUTING (nat)')], default='FORWARD', max_length=12)),
('in_interface', models.CharField(blank=True, default='', max_length=12, null=True)),
('out_interface', models.CharField(blank=True, default='', max_length=12, null=True)),
('source_ip', models.GenericIPAddressField(blank=True, null=True, protocol='IPv4')),
('source_netmask', models.PositiveIntegerField(choices=[(8, '/8 (255.0.0.0)'), (9, '/9 (255.128.0.0)'), (10, '/10 (255.192.0.0)'), (11, '/11 (255.224.0.0)'), (12, '/12 (255.240.0.0)'), (13, '/13 (255.248.0.0)'), (14, '/14 (255.252.0.0)'), (15, '/15 (255.254.0.0)'), (16, '/16 (255.255.0.0)'), (17, '/17 (255.255.128.0)'), (18, '/18 (255.255.192.0)'), (19, '/19 (255.255.224.0)'), (20, '/20 (255.255.240.0)'), (21, '/21 (255.255.248.0)'), (22, '/22 (255.255.252.0)'), (23, '/23 (255.255.254.0)'), (24, '/24 (255.255.255.0)'), (25, '/25 (255.255.255.128)'), (26, '/26 (255.255.255.192)'), (27, '/27 (255.255.255.224)'), (28, '/28 (255.255.255.240)'), (29, '/29 (255.255.255.248)'), (30, '/30 (255.255.255.252)'), (32, '/32 (255.255.255.255)')], default=32)),
('source_peer_include_networks', models.BooleanField(default=False)),
('not_source', models.BooleanField(default=False)),
('destination_ip', models.GenericIPAddressField(blank=True, null=True, protocol='IPv4')),
('destination_netmask', models.PositiveIntegerField(choices=[(8, '/8 (255.0.0.0)'), (9, '/9 (255.128.0.0)'), (10, '/10 (255.192.0.0)'), (11, '/11 (255.224.0.0)'), (12, '/12 (255.240.0.0)'), (13, '/13 (255.248.0.0)'), (14, '/14 (255.252.0.0)'), (15, '/15 (255.254.0.0)'), (16, '/16 (255.255.0.0)'), (17, '/17 (255.255.128.0)'), (18, '/18 (255.255.192.0)'), (19, '/19 (255.255.224.0)'), (20, '/20 (255.255.240.0)'), (21, '/21 (255.255.248.0)'), (22, '/22 (255.255.252.0)'), (23, '/23 (255.255.254.0)'), (24, '/24 (255.255.255.0)'), (25, '/25 (255.255.255.128)'), (26, '/26 (255.255.255.192)'), (27, '/27 (255.255.255.224)'), (28, '/28 (255.255.255.240)'), (29, '/29 (255.255.255.248)'), (30, '/30 (255.255.255.252)'), (32, '/32 (255.255.255.255)')], default=32)),
('destination_peer_include_networks', models.BooleanField(default=False)),
('not_destination', models.BooleanField(default=False)),
('protocol', models.CharField(blank=True, choices=[('', 'all'), ('tcp', 'TCP'), ('udp', 'UDP'), ('both', 'TCP+UDP'), ('icmp', 'ICMP')], default='', max_length=4, null=True)),
('destination_port', models.CharField(blank=True, max_length=11, null=True)),
('state_new', models.BooleanField(default=False)),
('state_related', models.BooleanField(default=False)),
('state_established', models.BooleanField(default=False)),
('state_invalid', models.BooleanField(default=False)),
('state_untracked', models.BooleanField(default=False)),
('not_state', models.BooleanField(default=False)),
('rule_action', models.CharField(choices=[('accept', 'ACCEPT'), ('reject', 'REJECT'), ('drop', 'DROP'), ('masquerade', 'MASQUERADE')], default='accept', max_length=10)),
('sort_order', models.PositiveIntegerField(default=0)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('destination_peer', models.ManyToManyField(blank=True, related_name='forward_rules_as_destination', to='wireguard.peer')),
('source_peer', models.ManyToManyField(blank=True, related_name='forward_rules_as_source', to='wireguard.peer')),
('wireguard_instance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wireguard.wireguardinstance')),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-02-29 13:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('firewall', '0003_firewallsettings_forwardrule'),
('wireguard', '0018_wireguardinstance_legacy_firewall'),
]
operations = [
migrations.RenameModel(
old_name='ForwardRule',
new_name='FirewallRule',
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.2 on 2024-02-29 13:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('firewall', '0004_rename_forwardrule_firewallrule'),
]
operations = [
migrations.AddField(
model_name='firewallsettings',
name='created',
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name='firewallsettings',
name='updated',
field=models.DateTimeField(auto_now=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-02-29 13:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('firewall', '0005_firewallsettings_created_firewallsettings_updated'),
]
operations = [
migrations.AlterField(
model_name='firewallsettings',
name='created',
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-02-29 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('firewall', '0006_alter_firewallsettings_created'),
]
operations = [
migrations.AddField(
model_name='firewallsettings',
name='pending_changes',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 5.0.2 on 2024-02-29 17:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('firewall', '0007_firewallsettings_pending_changes'),
]
operations = [
migrations.RemoveField(
model_name='firewallsettings',
name='global',
),
migrations.AddField(
model_name='firewallsettings',
name='name',
field=models.CharField(default='global', max_length=6, unique=True),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.0.2 on 2024-03-01 00:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('firewall', '0008_remove_firewallsettings_global_firewallsettings_name'),
]
operations = [
migrations.RemoveField(
model_name='firewallrule',
name='wireguard_instance',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-03-01 01:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('firewall', '0009_remove_firewallrule_wireguard_instance'),
]
operations = [
migrations.AlterField(
model_name='firewallrule',
name='firewall_chain',
field=models.CharField(choices=[('forward', 'FORWARD'), ('postrouting', 'POSTROUTING (nat)')], default='forward', max_length=12),
),
]

View File

@ -1,5 +1,6 @@
from django.db import models
from wireguard.models import Peer, WireGuardInstance
from wireguard.models import NETMASK_CHOICES
import uuid
@ -24,3 +25,55 @@ class RedirectRule(models.Model):
unique_together = ['port', 'protocol']
class FirewallRule(models.Model):
description = models.CharField(max_length=100, blank=True, null=True)
firewall_chain = models.CharField(max_length=12, default='forward', choices=[('forward', 'FORWARD'), ('postrouting', 'POSTROUTING (nat)')])
in_interface = models.CharField(max_length=12, default='', blank=True, null=True)
out_interface = models.CharField(max_length=12, default='', blank=True, null=True)
source_ip = models.GenericIPAddressField(blank=True, null=True, protocol='IPv4')
source_netmask = models.PositiveIntegerField(default=32, choices=NETMASK_CHOICES)
source_peer = models.ManyToManyField(Peer, related_name="forward_rules_as_source", blank=True)
source_peer_include_networks = models.BooleanField(default=False)
not_source = models.BooleanField(default=False)
destination_ip = models.GenericIPAddressField(blank=True, null=True, protocol='IPv4')
destination_netmask = models.PositiveIntegerField(default=32, choices=NETMASK_CHOICES)
destination_peer = models.ManyToManyField(Peer, related_name="forward_rules_as_destination", blank=True)
destination_peer_include_networks = models.BooleanField(default=False)
not_destination = models.BooleanField(default=False)
protocol = models.CharField(max_length=4, default='', blank=True, null=True, choices=[('', 'all'), ('tcp', 'TCP'), ('udp', 'UDP'), ('both', 'TCP+UDP'), ('icmp', 'ICMP'),])
destination_port = models.CharField(max_length=11, blank=True, null=True)
state_new = models.BooleanField(default=False)
state_related = models.BooleanField(default=False)
state_established = models.BooleanField(default=False)
state_invalid = models.BooleanField(default=False)
state_untracked = models.BooleanField(default=False)
not_state = models.BooleanField(default=False)
rule_action = models.CharField(max_length=10, default='accept', choices=[('accept', 'ACCEPT'), ('reject', 'REJECT'), ('drop', 'DROP'), ('masquerade', 'MASQUERADE')])
sort_order = models.PositiveIntegerField(default=0)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
def __str__(self):
return str(self.uuid)
class FirewallSettings(models.Model):
name = models.CharField(max_length=6, default='global', unique=True)
default_forward_policy = models.CharField(max_length=6, default='accept', choices=[('accept', 'ACCEPT'), ('reject', 'REJECT'), ('drop', 'DROP')])
default_output_policy = models.CharField(max_length=6, default='accept', choices=[('accept', 'ACCEPT'), ('reject', 'REJECT'), ('drop', 'DROP')])
allow_peer_to_peer = models.BooleanField(default=True)
allow_instance_to_instance = models.BooleanField(default=True)
wan_interface = models.CharField(max_length=12, default='eth0')
pending_changes = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)

View File

@ -1,6 +1,6 @@
from django.shortcuts import render, get_object_or_404, redirect
from firewall.models import RedirectRule
from firewall.forms import RedirectRuleForm
from firewall.models import RedirectRule, FirewallRule, FirewallSettings
from firewall.forms import RedirectRuleForm, FirewallRuleForm
from django.contrib import messages
from wireguard.models import WireGuardInstance
from user_manager.models import UserAcl
@ -15,7 +15,8 @@ def view_redirect_rule_list(request):
context = {
'page_title': 'Port Forward List',
'pending_changes_warning': pending_changes_warning,
'redirect_rule_list': RedirectRule.objects.all().order_by('wireguard_instance', 'protocol', 'port')
'redirect_rule_list': RedirectRule.objects.all().order_by('port'),
'current_chain': 'portforward',
}
return render(request, 'firewall/redirect_rule_list.html', context=context)
@ -53,3 +54,56 @@ def manage_redirect_rule(request):
context['instance'] = instance
return render(request, 'firewall/manage_redirect_rule.html', context=context)
def view_firewall_rule_list(request):
wireguard_instances = WireGuardInstance.objects.all().order_by('instance_id')
current_chain = request.GET.get('chain', 'forward')
if current_chain not in ['forward', 'portforward', 'postrouting']:
current_chain = 'forward'
if wireguard_instances.filter(pending_changes=True).exists():
pending_changes_warning = True
else:
pending_changes_warning = False
context = {
'page_title': 'Firewall Rule List',
'pending_changes_warning': pending_changes_warning,
'firewall_rule_list': FirewallRule.objects.filter(firewall_chain=current_chain).order_by('sort_order'),
'current_chain': current_chain,
}
return render(request, 'firewall/firewall_rule_list.html', context=context)
def manage_firewall_rule(request):
if not UserAcl.objects.filter(user=request.user).filter(user_level__gte=40).exists():
return render(request, 'access_denied.html', {'page_title': 'Access Denied'})
context = {'page_title': 'Manage Firewall Rule'}
instance = None
uuid = request.GET.get('uuid', None)
if uuid:
instance = get_object_or_404(FirewallRule, uuid=uuid)
if request.GET.get('action') == 'delete':
if request.GET.get('confirmation') == 'delete':
firewall_settings, firewall_settings_created = FirewallSettings.objects.get_or_create(name='global')
firewall_settings.pending_changes = True
firewall_settings.save()
messages.success(request, 'Firewall rule deleted successfully')
else:
messages.warning(request, 'Error deleting Firewall rule|Confirmation did not match. Firewall rule was not deleted.')
return redirect('/firewall/rule_list/')
if request.method == 'POST':
form = FirewallRuleForm(request.POST, instance=instance)
if form.is_valid():
firewall_settings, firewall_settings_created = FirewallSettings.objects.get_or_create(name='global')
firewall_settings.pending_changes = True
firewall_settings.save()
form.save()
messages.success(request, 'Firewall rule saved successfully')
return redirect('/firewall/rule_list/')
else:
form = FirewallRuleForm(instance=instance)
context['form'] = form
context['instance'] = instance
return render(request, 'firewall/manage_firewall_rule.html', context=context)

View File

@ -25,6 +25,7 @@
<link rel="stylesheet" href="/static/AdminLTE-3.2.0/plugins/daterangepicker/daterangepicker.css">
<!-- summernote -->
<link rel="stylesheet" href="/static/AdminLTE-3.2.0/plugins/summernote/summernote-bs4.min.css">
{% block page_custom_head %}{% endblock%}
</head>
{% load custom_tags %}
{% tag_webadmin_version as webadmin_version %}

View File

@ -0,0 +1,12 @@
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link {% if current_chain == "forward" %}active{% endif %}" href="/firewall/rule_list/?chain=forward" role="tab">Forward</a>
</li>
<li class="nav-item">
<a class="nav-link {% if current_chain == "portforward" %}active{% endif %}" href="/firewall/port_forward/" role="tab">Port Forward</a>
</li>
<li class="nav-item">
<a class="nav-link {% if current_chain == "postrouting" %}active{% endif %}" href="/firewall/rule_list/?chain=postrouting" role="tab">Post Routing</a>
</li>
</ul>

View File

@ -0,0 +1,175 @@
{% extends 'base.html' %}
{% block page_custom_head %}
<style>
.first-line-container {
display: flex;
align-items: center; /* Centraliza os itens verticalmente */
width: 100%;
}
.more-link {
margin-left: auto; /* Empurra o link para a direita */
text-decoration: none;
}
.more-text {
display: none;
width: 100%;
}
</style>
{% endblock%}
{% block content %}
<div class="card card-primary card-outline">
<div class="card-body">
{% include "firewall/firewall_nav_tabs.html" %}
<div class="tab-content" id="custom-content-below-tabContent">
<div class="tab-pane fade show active" id="custom-content-below-home" role="tabpanel" aria-labelledby="custom-content-below-home-tab">
<table class="table table-striped table-hover">
<thead>
<th>#</th>
<th><i class="fas fa-info-circle"></i></th>
<th>In</th>
<th>Out</th>
<th>Source</th>
<th>Destination</th>
<th>Protocol</th>
<th>Port</th>
<th>State</th>
<th>Action</th>
<th></th>
</thead>
<tbody>
{% for rule in firewall_rule_list %}
<tr>
<td style="width: 1%; white-space: nowrap;">{{ rule.sort_order }}</td>
<td style="width: 1%; white-space: nowrap;">{% if rule.description %}<i class="fas fa-info-circle" title="{{ rule.description }}"></i>{% endif %}</td>
<td>{{ rule.in_interface }}</td>
<td>{{ rule.out_interface }}</td>
<td>
{% if rule.source_ip %}{% if rule.not_source %}<span title="Not source">!</span> {% endif %}{{ rule.source_ip }}/{{ rule.source_netmask }}<br>{% endif%}
{% for peer in rule.source_peer.all %}{% if rule.not_source %}<span title="Not source">!</span> {% endif %}{{ peer }}{% if rule.source_peer_include_networks %} <span title="Include peer networks">+</span>{% endif %}<br>{% endfor %}
</td>
<td>
{% if rule.destination_ip %}{% if rule.not_destination %}<span title="Not destination">!</span> {% endif %}{{ rule.destination_ip }}/{{ rule.destination_netmask }}<br>{% endif%}
{% for peer in rule.destination_peer.all %}{% if rule.not_destination %}<span title="Not destination">!</span> {% endif %}{{ peer }}{% if rule.destination_peer_include_networks %} <span title="Include peer networks">+</span>{% endif %}<br>{% endfor %}
</td>
<td>{{ rule.get_protocol_display }}</td>
<td>{{ rule.destination_port }}</td>
<td>
{% if rule.state_new %}{% if rule.not_state %}<span title="Not state">! </span>{% endif %}New<br>{% endif %}
{% if rule.state_related %}{% if rule.not_state %}<span title="Not state">! </span>{% endif %}Related<br>{% endif %}
{% if rule.state_established %}{% if rule.not_state %}<span title="Not state">! </span>{% endif %}Established<br>{% endif %}
{% if rule.state_invalid %}{% if rule.not_state %}<span title="Not state">! </span>{% endif %}Invalid<br>{% endif %}
{% if rule.state_untracked %}{% if rule.not_state %}<span title="Not state">! </span>{% endif %}Untracked<br>{% endif %}
</td>
<td>{{ rule.get_rule_action_display }}</td>
{% comment%}
<td>{{ rule. }}</td>
{% endcomment %}
<td style="width: 1%; white-space: nowrap;">
<a href="/firewall/manage_firewall_rule/?uuid={{ rule.uuid }}" ><i class="far fa-edit"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="/firewall/manage_firewall_rule/" class='btn btn-primary'>Create Firewall Rule</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_page_scripts %}
{% comment %}
<script>
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll('td').forEach(function(td) {
// Conta o número de <br> na célula
let brCount = (td.innerHTML.match(/<br>/g) || []).length;
// Aplica a lógica de mostrar/esconder apenas se houver 2 ou mais <br>
if (brCount >= 2) {
let contentParts = td.innerHTML.split('<br>');
// Assume que queremos manter a primeira linha visível, adiciona explicitamente uma quebra de linha antes do conteúdo escondido
td.innerHTML = contentParts[0] + '<br>' +
'<span style="display: none;">' +
contentParts.slice(1).join('<br>') + '</span>' +
'<button class="more-btn">Mais</button>';
}
});
// Adiciona evento de clique para botões "Mais"
document.querySelectorAll('.more-btn').forEach(function(button) {
button.addEventListener('click', function() {
let moreText = this.previousElementSibling; // O span com o texto extra
if (moreText.style.display === "none") {
moreText.style.display = "inline";
this.textContent = "Menos";
} else {
moreText.style.display = "none";
this.textContent = "Mais";
}
});
});
});
</script>
{% endcomment %}
<script>
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll('td').forEach(function(td) {
let brCount = (td.innerHTML.match(/<br>/g) || []).length;
if (brCount >= 2) {
let contentParts = td.innerHTML.split('<br>');
// Mantém a estrutura do contêiner com o texto e o link "More"
let firstLineContainer = `<div class="first-line-container">${contentParts[0]}<a href="#" class="more-link">more</a></div>`;
td.innerHTML = firstLineContainer +
'<span class="more-text" style="display: none;">' +
contentParts.slice(1).join('<br>') + '</span>';
}
});
document.querySelectorAll('.more-link').forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault(); // Impede a ação padrão do link
let moreText = this.parentNode.nextElementSibling; // Seleciona o span corretamente
if (moreText.style.display === "none") {
moreText.style.display = "inline";
this.textContent = "less";
} else {
moreText.style.display = "none";
this.textContent = "more";
}
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,434 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-3">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Manage Firewall Rule</h3>
</div>
<form method="post" action="">
{% csrf_token %}
<div class="accordion" id="firewallRuleAccordion">
<!-- General Group -->
<div class="card">
<div class="card-header" id="headingGeneral">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseGeneral" aria-expanded="true" aria-controls="collapseGeneral">
General
</button>
</h2>
</div>
<div id="collapseGeneral" class="collapse show" aria-labelledby="headingGeneral" >
{% comment %}
<div class="card-body">
<div class="form-group">
<label for="description">{{ form.description.label }}</label>
<input type="text" class="form-control" id="description" name="description" value="{{ form.description.value|default_if_none:""}}">
</div>
<div class="form-group">
<label for="wireguard_instance">{{ form.wireguard_instance.label }}</label>
<select class="form-control" id="wireguard_instance" name="wireguard_instance">
{% for instance in form.wireguard_instance.field.queryset %}
<option value="{{ instance.pk }}" {% if form.wireguard_instance.value == instance.pk %} selected {% endif %}>{{ instance }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="firewall_chain">{{ form.firewall_chain.label }}</label>
<select class="form-control" id="firewall_chain" name="firewall_chain">
{% for value, display in form.firewall_chain.field.choices %}
<option value="{{ value }}" {% if form.firewall_chain.value == value %} selected {% endif %}>{{ display }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="sort_order">{{ form.sort_order.label }}</label>
<input type="number" class="form-control" id="sort_order" name="sort_order" value="{{ form.sort_order.value }}">
</div>
</div>{% endcomment %}
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-row">
<div class="form-group col-md-12">
<label for="description">{{ form.description.label }}</label>
<input type="text" class="form-control" id="description" name="description" value="{{ form.description.value|default_if_none:'' }}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="firewall_chain">{{ form.firewall_chain.label }}</label>
<select class="form-control" id="firewall_chain" name="firewall_chain">
{% for value, display in form.firewall_chain.field.choices %}
<option value="{{ value }}" {% if form.firewall_chain.value == value %} selected {% endif %}>{{ display }}</option>
{% endfor %}
</select>
</div>
<div class="form-group col-md-6">
<label for="sort_order">{{ form.sort_order.label }}</label>
<input type="number" class="form-control" id="sort_order" name="sort_order" value="{{ form.sort_order.value }}">
</div>
</div>
</div>
<div class="col-md-6">
<h5>Advanced VPN Firewall Configuration</h5>
<p>
This interface serves as a comprehensive tool for managing firewall rules, enabling users to implement advanced traffic policies between VPN peers and networks. It simplifies establishing firewall rules, packet filtering, and NAT configurations, allowing for precise control over network security. Users can define source and destination IP addresses, ports, protocols, and actions to tailor traffic flow, ensuring a secure and efficient networking environment.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Interface Group -->
<div class="card">
<div class="card-header" id="headingInterface">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseInterface" aria-expanded="false" aria-controls="collapseInterface">
Interface
</button>
</h2>
</div>
<div id="collapseInterface" class="collapse" aria-labelledby="headingInterface" >
<div class="card-body">
<div class="row">
<!-- In Interface -->
<div class="col-md-6">
<div class="form-group">
<label for="in_interface">{{ form.in_interface.label }}</label>
<select class="form-control" id="in_interface" name="in_interface">
{% for value, display in form.in_interface.field.choices %}
<option value="{{ value }}" {% if form.in_interface.value == value %} selected {% endif %}>{{ display }}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Out Interface -->
<div class="col-md-6">
<div class="form-group">
<label for="out_interface">{{ form.out_interface.label }}</label>
<select class="form-control" id="out_interface" name="out_interface">
{% for value, display in form.out_interface.field.choices %}
<option value="{{ value }}" {% if form.out_interface.value == value %} selected {% endif %}>{{ display }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Source Group -->
<div class="card">
<div class="card-header" id="headingSource">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseSource" aria-expanded="false" aria-controls="collapseSource">
Source
</button>
</h2>
</div>
<div id="collapseSource" class="collapse" aria-labelledby="headingSource" >
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-row">
<div class="form-group col-md-8">
<label for="source_ip">{{ form.source_ip.label }}</label>
<input type="text" class="form-control" id="source_ip" name="source_ip" value="{{ form.source_ip.value|default_if_none:'' }}">
</div>
<div class="form-group col-md-4">
<label for="source_netmask">{{ form.source_netmask.label }}</label>
<input type="number" class="form-control" id="source_netmask" name="source_netmask" value="{{ form.source_netmask.value }}">
</div>
</div>
<div class="form-group">
<label for="source_peer">{{ form.source_peer.label }}</label>
<select class="form-control" id="source_peer" name="source_peer" multiple>
{% for peer in form.source_peer.field.queryset %}
<option value="{{ peer.pk }}" {% if peer.pk in form.source_peer.value %} selected {% endif %}>{{ peer }}</option>
{% endfor %}
</select>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="source_peer_include_networks" name="source_peer_include_networks" {% if form.source_peer_include_networks.value %} checked {% endif %}>
<label class="form-check-label" for="source_peer_include_networks">{{ form.source_peer_include_networks.label }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="not_source" name="not_source" {% if form.not_source.value %} checked {% endif %}>
<label class="form-check-label" for="not_source">{{ form.not_source.label }}</label>
</div>
</div>
<div class="col-md-6">
<h5>Source Selection</h5>
<p>
You have the option to apply this rule to a specific IP address or network and/or to multiple peers.<br><br>
Enabling the "Include peer networks" option will automatically include all Allowed IPs associated with each selected peer.<br><br>
Please note that selecting multiple peers with included networks on both the source and destination ends may result in a rapid increase in the number of firewall rules generated, depending on your configuration.<br><br>
The "Not Source" option negates the selected source IP, network, or peer(s).
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Destination Group -->
<div class="card">
<div class="card-header" id="headingDestination">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseDestination" aria-expanded="false" aria-controls="collapseDestination">
Destination
</button>
</h2>
</div>
<div id="collapseDestination" class="collapse" aria-labelledby="headingDestination" >
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-row">
<div class="form-group col-md-8">
<label for="destination_ip">{{ form.destination_ip.label }}</label>
<input type="text" class="form-control" id="destination_ip" name="destination_ip" value="{{ form.destination_ip.value|default_if_none:'' }}">
</div>
<div class="form-group col-md-4">
<label for="destination_netmask">{{ form.destination_netmask.label }}</label>
<input type="number" class="form-control" id="destination_netmask" name="destination_netmask" value="{{ form.destination_netmask.value }}">
</div>
</div>
<div class="form-group">
<label for="destination_peer">{{ form.destination_peer.label }}</label>
<select class="form-control" id="destination_peer" name="destination_peer" multiple>
{% for peer in form.destination_peer.field.queryset %}
<option value="{{ peer.pk }}" {% if peer.pk in form.destination_peer.value %} selected {% endif %}>{{ peer }}</option>
{% endfor %}
</select>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="destination_peer_include_networks" name="destination_peer_include_networks" {% if form.destination_peer_include_networks.value %} checked {% endif %}>
<label class="form-check-label" for="destination_peer_include_networks">{{ form.destination_peer_include_networks.label }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="not_destination" name="not_destination" {% if form.not_destination.value %} checked {% endif %}>
<label class="form-check-label" for="not_destination">{{ form.not_destination.label }}</label>
</div>
</div>
<div class="col-md-6">
<h5>Destination Selection</h5>
<p>
You have the option to apply this rule to a specific IP address or network and/or to multiple peers as the destination.<br><br>
Enabling the "Include peer networks" option will automatically include all Allowed IPs associated with each selected peer as the destination.<br><br>
Please note that selecting multiple peers with included networks on both the source and destination ends may result in a rapid increase in the number of firewall rules generated, depending on your configuration.<br><br>
The "Not Destination" option negates the selected destination IP, network, or peer(s).
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Protocol Group -->
<div class="card">
<div class="card-header" id="headingProtocol">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseProtocol" aria-expanded="false" aria-controls="collapseProtocol">
Protocol
</button>
</h2>
</div>
<div id="collapseProtocol" class="collapse" aria-labelledby="headingProtocol" >
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="row">
<!-- Protocol -->
<div class="col-md-6">
<div class="form-group">
<label for="protocol">{{ form.protocol.label }}</label>
<select class="form-control" id="protocol" name="protocol">
{% for value, display in form.protocol.field.choices %}
<option value="{{ value }}" {% if form.protocol.value == value %} selected {% endif %}>{{ display }}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Destination Port -->
<div class="col-md-6">
<div class="form-group">
<label for="destination_port">{{ form.destination_port.label }}</label>
<input type="text" class="form-control" id="destination_port" name="destination_port" value="{{ form.destination_port.value|default_if_none:'' }}">
</div>
</div>
</div>
</div>
<div class="col-md-6">
<h5>Protocol and Port</h5>
<p>
Only the most commonly used protocols are listed here. If you require a specific protocol, please open an issue on GitHub.<br><br>
Selecting TCP+UDP will result in the duplication of generated rules.<br><br>
Ports can be specified as single numbers (e.g., 8080) or as ranges (e.g., 8001:8999).
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Packet State Group -->
<div class="card">
<div class="card-header" id="headingPacketState">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapsePacketState" aria-expanded="false" aria-controls="collapsePacketState">
Packet State
</button>
</h2>
</div>
<div id="collapsePacketState" class="collapse" aria-labelledby="headingPacketState" >
<div class="card-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="state_new" name="state_new" {% if form.state_new.value %} checked {% endif %}>
<label class="form-check-label" for="state_new">{{ form.state_new.label }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="state_related" name="state_related" {% if form.state_related.value %} checked {% endif %}>
<label class="form-check-label" for="state_related">{{ form.state_related.label }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="state_established" name="state_established" {% if form.state_established.value %} checked {% endif %}>
<label class="form-check-label" for="state_established">{{ form.state_established.label }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="state_invalid" name="state_invalid" {% if form.state_invalid.value %} checked {% endif %}>
<label class="form-check-label" for="state_invalid">{{ form.state_invalid.label }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="state_untracked" name="state_untracked" {% if form.state_untracked.value %} checked {% endif %}>
<label class="form-check-label" for="state_untracked">{{ form.state_untracked.label }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="not_state" name="not_state" {% if form.not_state.value %} checked {% endif %}>
<label class="form-check-label" for="not_state">{{ form.not_state.label }}</label>
</div>
</div>
</div>
</div>
<!-- Action Group -->
<div class="card">
<div class="card-header" id="headingAction">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseAction" aria-expanded="false" aria-controls="collapseAction">
Action
</button>
</h2>
</div>
<div id="collapseAction" class="collapse" aria-labelledby="headingAction" >
<div class="card-body">
<div class="form-group">
<label for="rule_action">{{ form.rule_action.label }}</label>
<select class="form-control" id="rule_action" name="rule_action">
{% for value, display in form.rule_action.field.choices %}
<option value="{{ value }}" {% if form.rule_action.value == value %} selected {% endif %}>{{ display }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Array de IDs dos campos a serem ignorados
var ignoreFields = ['source_netmask', 'destination_netmask'];
// Itera por cada painel para verificar se contém dados nos campos
$('.collapse').each(function() {
var panel = $(this);
var shouldOpen = false;
// Verifica inputs do tipo texto e número, excluindo os ignorados
panel.find('input[type=text], input[type=number], textarea').each(function() {
if (!ignoreFields.includes(this.id) && $(this).val()) {
shouldOpen = true;
}
});
// Verifica checkboxes e radios, excluindo os ignorados
panel.find('input[type=checkbox], input[type=radio]').each(function() {
if (!ignoreFields.includes(this.id) && $(this).is(':checked')) {
shouldOpen = true;
}
});
// Verifica selects, incluindo múltipla seleção, excluindo os ignorados
panel.find('select').each(function() {
if (!ignoreFields.includes(this.id) && $(this).find('option:selected').length > 0) {
var allUnselected = true;
$(this).find('option:selected').each(function() {
if ($(this).val()) {
allUnselected = false;
}
});
if (!allUnselected) {
shouldOpen = true;
}
}
});
// Se dados relevantes foram encontrados e não são para ser ignorados, abre o painel
if (shouldOpen) {
panel.collapse('show');
}
});
// Controla o abrir/fechar dos painéis sem afetar os outros
$('.card-header button').on('click', function(e) {
e.preventDefault();
var target = $(this).attr('data-target');
$(target).collapse('toggle');
});
});
</script>
{% endblock %}

View File

@ -2,65 +2,78 @@
{% block content %}
<table class="table table-striped">
<thead>
<tr>
<th>Instance</th>
<th>Protocol</th>
<th>Port</th>
<th>Destination</th>
<th>Allow Forward</th>
<th>Masquerade Source</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for redirect_rule in redirect_rule_list %}
<tr>
<td>{{ redirect_rule.wireguard_instance }}</td>
<td>{{ redirect_rule.protocol }}</td>
<td>{{ redirect_rule.port }}</td>
<td>
{% if redirect_rule.peer %}
<a href="/peer/manage/?peer={{ redirect_rule.peer.uuid }}">
{% if redirect_rule.peer.name %}
{{ redirect_rule.peer.name }}
{% else %}
{{ redirect_rule.peer.public_key|slice:":16" }}{% if redirect_rule.peer.public_key|length > 16 %}...{% endif %}
{% endif %}
</a>
{% else %}
{{ redirect_rule.ip_address }}
{% endif %}
</td>
<td>
<div class="card card-primary card-outline">
<div class="card-body">
{% include "firewall/firewall_nav_tabs.html" %}
<div class="tab-content" id="custom-content-below-tabContent">
<div class="tab-pane fade show active" id="custom-content-below-home" role="tabpanel" aria-labelledby="custom-content-below-home-tab">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Instance</th>
<th>Protocol</th>
<th>Port</th>
<th>Destination</th>
<th>Allow Forward</th>
<th>Masquerade Source</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for redirect_rule in redirect_rule_list %}
<tr>
<td>{{ redirect_rule.wireguard_instance }}</td>
<td>{{ redirect_rule.protocol }}</td>
<td>{{ redirect_rule.port }}</td>
<td>
{% if redirect_rule.peer %}
<a href="/peer/manage/?peer={{ redirect_rule.peer.uuid }}">
{% if redirect_rule.peer.name %}
{{ redirect_rule.peer.name }}
{% else %}
{{ redirect_rule.peer.public_key|slice:":16" }}{% if redirect_rule.peer.public_key|length > 16 %}...{% endif %}
{% endif %}
</a>
{% else %}
{{ redirect_rule.ip_address }}
{% endif %}
</td>
<td>
{% if redirect_rule.add_forward_rule %}
<i class="fas fa-check"></i>
{% else %}
<i class="fas fa-times"></i>
{% if redirect_rule.add_forward_rule %}
<i class="fas fa-check"></i>
{% else %}
<i class="fas fa-times"></i>
{% endif %}
{% endif %}
</td>
<td>
{% if redirect_rule.masquerade_source %}
<i class="fas fa-check"></i>
<i class="fas fa-exclamation-triangle" title="This serves as a temporary solution when a peer does not use the VPN as its default gateway. It's important to note that this configuration is not recommended, as it alters the source address of all connections to match the IP address of the WireGuard instance."></i>
{% else %}
<i class="fas fa-times"></i>
</td>
<td>
{% if redirect_rule.masquerade_source %}
<i class="fas fa-check"></i>
<i class="fas fa-exclamation-triangle" title="This serves as a temporary solution when a peer does not use the VPN as its default gateway. It's important to note that this configuration is not recommended, as it alters the source address of all connections to match the IP address of the WireGuard instance."></i>
{% else %}
<i class="fas fa-times"></i>
{% endif %}
</td>
<td style="width: 1%; white-space: nowrap;">
<a href="/firewall/manage_port_forward_rule/?uuid={{ redirect_rule.uuid }}" ><i class="far fa-edit"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endif %}
</td>
<td style="width: 1%; white-space: nowrap;">
<a href="/firewall/manage_port_forward_rule/?uuid={{ redirect_rule.uuid }}" ><i class="far fa-edit"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="/firewall/manage_port_forward_rule/" class='btn btn-primary'>Create Port forwarding Rule</a>
{% endblock %}

View File

@ -88,7 +88,7 @@ class Peer(models.Model):
if self.name:
return self.name
else:
return self.public_key
return self.public_key[:16] + "..."
class PeerStatus(models.Model):

View File

@ -23,7 +23,7 @@ from user_manager.views import view_user_list, view_manage_user
from accounts.views import view_create_first_user, view_login, view_logout
from wireguard_tools.views import export_wireguard_configs, download_config_or_qrcode, restart_wireguard_interfaces
from api.views import wireguard_status, cron_check_updates, cron_update_peer_latest_handshake
from firewall.views import view_redirect_rule_list, manage_redirect_rule
from firewall.views import view_redirect_rule_list, manage_redirect_rule, view_firewall_rule_list, manage_firewall_rule
urlpatterns = [
@ -48,5 +48,6 @@ urlpatterns = [
path('api/cron_update_peer_latest_handshake/', cron_update_peer_latest_handshake, name='cron_update_peer_latest_handshake'),
path('firewall/port_forward/', view_redirect_rule_list, name='redirect_rule_list'),
path('firewall/manage_port_forward_rule/', manage_redirect_rule, name='manage_redirect_rule'),
path('firewall/rule_list/', view_firewall_rule_list, name='firewall_rule_list'),
path('firewall/manage_firewall_rule/', manage_firewall_rule, name='manage_firewall_rule')
]