diff --git a/dns/functions.py b/dns/functions.py index a341189..0605a10 100644 --- a/dns/functions.py +++ b/dns/functions.py @@ -1,4 +1,6 @@ -from .models import DNSSettings, StaticHost +import os + +from .models import DNSSettings, StaticHost, DNSFilterList def generate_unbound_config(): @@ -57,6 +59,7 @@ def generate_dnsdist_config(): def generate_dnsmasq_config(): dns_settings = DNSSettings.objects.get(name='dns_settings') static_hosts = StaticHost.objects.all() + dns_lists = DNSFilterList.objects.filter(enabled=True) dnsmasq_config = f''' no-dhcp-interface= listen-address=0.0.0.0 @@ -72,4 +75,10 @@ bind-interfaces dnsmasq_config += '\n' for static_host in static_hosts: dnsmasq_config += f'address=/{static_host.hostname}/{static_host.ip_address}\n' + + if dns_lists: + dnsmasq_config += '\n' + for dns_list in dns_lists: + file_path = os.path.join("/etc/dnsmasq/", f"{dns_list.uuid}.conf") + dnsmasq_config += f'addn-hosts={file_path}\n' return dnsmasq_config diff --git a/dns/migrations/0004_dnsfilterlist_recommended.py b/dns/migrations/0004_dnsfilterlist_recommended.py new file mode 100644 index 0000000..9c16228 --- /dev/null +++ b/dns/migrations/0004_dnsfilterlist_recommended.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.5 on 2025-03-01 23:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dns', '0003_dnsfilterlist_host_count'), + ] + + operations = [ + migrations.AddField( + model_name='dnsfilterlist', + name='recommended', + field=models.BooleanField(default=False), + ), + ] diff --git a/dns/views.py b/dns/views.py index cbd681b..8d1d712 100644 --- a/dns/views.py +++ b/dns/views.py @@ -1,13 +1,18 @@ +import hashlib +import os + +import requests from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.shortcuts import render, get_object_or_404, redirect +from django.utils import timezone from user_manager.models import UserAcl from .forms import DNSFilterListForm from .forms import StaticHostForm, DNSSettingsForm from .functions import generate_dnsmasq_config -from .models import DNSSettings, DNSFilterList +from .models import DNSFilterList, DNSSettings from .models import StaticHost @@ -32,12 +37,32 @@ def view_apply_dns_config(request): def view_static_host_list(request): dns_settings, _ = DNSSettings.objects.get_or_create(name='dns_settings') static_host_list = StaticHost.objects.all().order_by('hostname') - filter_lists = DNSFilterList.objects.all().order_by('name') + filter_lists = DNSFilterList.objects.all().order_by('-recommended', 'name') if not filter_lists: DNSFilterList.objects.create( name='stevenblack-hosts', list_url='https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts', - description='adware and malware domains', enabled=False + description='adware and malware', enabled=False, recommended=True ) + DNSFilterList.objects.create( + name='stevenblack-fakenews', list_url='https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-only/hosts', + description='fakenews', enabled=False + ) + DNSFilterList.objects.create( + name='stevenblack-gambling', + list_url='https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling-only/hosts', + description='gambling', enabled=False + ) + DNSFilterList.objects.create( + name='stevenblack-porn', + list_url='https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn-only/hosts', + description='porn', enabled=False + ) + DNSFilterList.objects.create( + name='stevenblack-social', + list_url='https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/social-only/hosts', + description='social', enabled=False + ) + filter_lists = DNSFilterList.objects.all().order_by('name') messages.success(request, 'Default DNS Filter List created successfully') @@ -118,7 +143,7 @@ def view_manage_static_host(request): @login_required def view_manage_filter_list(request): - if not UserAcl.objects.filter(user=request.user, user_level__gte=50).exists(): + if not UserAcl.objects.filter(user=request.user, user_level__gte=40).exists(): return render(request, 'access_denied.html', {'page_title': 'Access Denied'}) dns_settings, _ = DNSSettings.objects.get_or_create(name='dns_settings') @@ -130,6 +155,13 @@ def view_manage_filter_list(request): if filter_list.enabled: messages.warning(request, 'DNS Filter List not deleted | Filter List is enabled') return redirect('/dns/') + file_path = os.path.join("/etc/dnsmasq/", f"{filter_list.uuid}.conf") + if os.path.exists(file_path): + try: + os.remove(file_path) + except Exception as e: + messages.error(request, f"Error removing config file: {e}") + return redirect('/dns/') filter_list.delete() messages.success(request, 'DNS Filter List deleted successfully') return redirect('/dns/') @@ -152,4 +184,101 @@ def view_manage_filter_list(request): 'form': form, 'instance': filter_list, } - return render(request, 'generic_form.html', context=context) \ No newline at end of file + return render(request, 'generic_form.html', context=context) + + +@login_required +def view_update_dns_list(request): + if not UserAcl.objects.filter(user=request.user, user_level__gte=40).exists(): + return render(request, 'access_denied.html', {'page_title': 'Access Denied'}) + + dns_list = get_object_or_404(DNSFilterList, uuid=request.GET.get('uuid')) + dns_settings, _ = DNSSettings.objects.get_or_create(name='dns_settings') + file_path = os.path.join("/etc/dnsmasq/", f"{dns_list.uuid}.conf") + + old_checksum = None + if os.path.exists(file_path): + try: + with open(file_path, "rb") as f: + old_content = f.read() + old_checksum = hashlib.sha256(old_content).hexdigest() + except Exception as e: + messages.error(request, f"Failed to read existing config file: {e}") + if dns_list.enabled: + dns_list.enabled = False + dns_list.save() + dns_settings.pending_changes = True + dns_settings.save() + return redirect('/dns/') + + try: + response = requests.get(dns_list.list_url) + response.raise_for_status() + content = response.text + except Exception as e: + if dns_list.enabled: + dns_list.enabled = False + dns_list.save() + dns_settings.pending_changes = True + dns_settings.save() + messages.error(request, f"Failed to fetch the host list: {e}") + return redirect('/dns/') + + new_checksum = hashlib.sha256(content.encode('utf-8')).hexdigest() + + # Write the new content to the file. + try: + with open(file_path, "w") as f: + f.write(content) + except Exception as e: + messages.error(request, f"Failed to write config file: {e}") + if dns_list.enabled: + dns_list.enabled = False + dns_list.save() + dns_settings.pending_changes = True + dns_settings.save() + return redirect('/dns/') + + # If the list is enabled and either the file did not exist or the checksum has changed, + # mark the DNS settings as having pending changes. + if dns_list.enabled and (old_checksum is None or new_checksum != old_checksum): + dns_settings.pending_changes = True + dns_settings.save() + + # Count the number of valid host entries (ignoring empty lines and lines starting with '#'). + host_count = sum(1 for line in content.splitlines() if line.strip() and not line.strip().startswith('#')) + dns_list.host_count = host_count + + # If at least one valid host is found, update the last_updated field. + if host_count > 0: + dns_list.last_updated = timezone.now() + + # Save changes to the DNSFilterList instance. + dns_list.save() + + messages.success(request, 'DNS Filter List updated successfully') + return redirect('/dns/') + + +def view_toggle_dns_list(request): + if not UserAcl.objects.filter(user=request.user, user_level__gte=40).exists(): + return render(request, 'access_denied.html', {'page_title': 'Access Denied'}) + + dns_list = get_object_or_404(DNSFilterList, uuid=request.GET.get('uuid')) + dns_settings, _ = DNSSettings.objects.get_or_create(name='dns_settings') + file_path = os.path.join("/etc/dnsmasq/", f"{dns_list.uuid}.conf") + + if request.GET.get('action') == 'enable': + if dns_list.host_count > 0 and os.path.exists(file_path): + dns_list.enabled = True + dns_list.save() + export_dns_configuration() + messages.success(request, 'DNS Filter List enabled successfully') + else: + messages.error(request, 'DNS Filter List not enabled | No valid hosts found') + else: + dns_list.enabled = False + dns_list.save() + export_dns_configuration() + messages.success(request, 'DNS Filter List disabled successfully') + return redirect('/dns/') \ No newline at end of file diff --git a/screenshots/dns.png b/screenshots/dns.png new file mode 100644 index 0000000..52de8bb Binary files /dev/null and b/screenshots/dns.png differ diff --git a/templates/dns/static_host_list.html b/templates/dns/static_host_list.html index 9479804..3f0c643 100644 --- a/templates/dns/static_host_list.html +++ b/templates/dns/static_host_list.html @@ -63,28 +63,36 @@