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:
@ -49,4 +49,48 @@ class RedirectRuleForm(forms.ModelForm):
if ip_address:
cleaned_data['peer'] = None
return cleaned_data
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)
@ -52,4 +53,57 @@ def manage_redirect_rule(request):
context['form'] = form
context['instance'] = instance
return render(request, 'firewall/manage_redirect_rule.html', context=context)
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)