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 @@ + - - - + + + {% if filter_lists %} {% for filter_list in filter_lists %} +
Name Description Hosts Last UpdateStatusUpdateEdit
{% if filter_list.recommended %} + + {% endif %} {{ filter_list.name }} {{ filter_list.description }} {{ filter_list.host_count }} {{ filter_list.last_updated|default_if_none:"" }} - + {% if filter_list.enabled %} + + {% else %} + + {% endif %} - + diff --git a/wireguard_webadmin/settings.py b/wireguard_webadmin/settings.py index 7b7eb95..f18f281 100644 --- a/wireguard_webadmin/settings.py +++ b/wireguard_webadmin/settings.py @@ -135,6 +135,6 @@ STATICFILES_DIRS = [ DNS_CONFIG_FILE = '/etc/dnsmasq/wireguard_webadmin_dns.conf' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -WIREGUARD_WEBADMIN_VERSION = 9958 +WIREGUARD_WEBADMIN_VERSION = 9959 from wireguard_webadmin.production_settings import * diff --git a/wireguard_webadmin/urls.py b/wireguard_webadmin/urls.py index cc60066..955438c 100644 --- a/wireguard_webadmin/urls.py +++ b/wireguard_webadmin/urls.py @@ -22,7 +22,7 @@ from api.views import wireguard_status, cron_check_updates, cron_update_peer_lat routerfleet_get_user_token, routerfleet_authenticate_session, peer_info, api_peer_invite from console.views import view_console from dns.views import view_static_host_list, view_manage_static_host, view_manage_dns_settings, view_apply_dns_config, \ - view_manage_filter_list + view_manage_filter_list, view_update_dns_list, view_toggle_dns_list from firewall.views import view_redirect_rule_list, manage_redirect_rule, view_firewall_rule_list, manage_firewall_rule, \ view_manage_firewall_settings, view_generate_iptables_script, view_reset_firewall, view_firewall_migration_required from user_manager.views import view_user_list, view_manage_user, view_peer_group_list, view_peer_group_manage @@ -43,6 +43,8 @@ urlpatterns = [ path('dns/manage_static_host/', view_manage_static_host, name='manage_static_host'), path('dns/manage_settings/', view_manage_dns_settings, name='manage_dns_settings'), path('dns/manage_filter_list/', view_manage_filter_list, name='manage_filter_list'), + path('dns/update_dns_list/', view_update_dns_list, name='update_dns_list'), + path('dns/toggle_dns_list/', view_toggle_dns_list, name='toggle_dns_list'), path('peer/list/', view_wireguard_peer_list, name='wireguard_peer_list'), path('peer/sort/', view_wireguard_peer_sort, name='wireguard_peer_sort'), path('peer/manage/', view_wireguard_peer_manage, name='wireguard_peer_manage'),