From aefd3f698b8e21d69805f4bb20e7dc36757ad668 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Mon, 29 Apr 2024 15:26:30 -0300 Subject: [PATCH] DNS Container and docker compose --- build_and_push.sh | 1 + containers/dnsmasq/Dockerfile-dnsmasq | 13 +++++ containers/dnsmasq/entrypoint.sh | 40 ++++++++++++++++ dns/forms.py | 1 + dns/functions.py | 48 +++++++++++++++++-- dns/views.py | 15 ++---- docker-compose-build.yml | 6 +++ docker-compose-no-nginx-dev.yml | 12 +++++ docker-compose-no-nginx.yml | 10 ++++ docker-compose.yml | 10 ++++ entrypoint.sh | 5 ++ firewall/tools.py | 17 +++++++ .../wireguard/wireguard_manage_server.html | 4 +- wireguard/forms.py | 2 +- wireguard/views.py | 1 + wireguard_tools/views.py | 4 +- wireguard_webadmin/settings.py | 4 +- wireguard_webadmin/urls.py | 2 +- 18 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 containers/dnsmasq/Dockerfile-dnsmasq create mode 100644 containers/dnsmasq/entrypoint.sh diff --git a/build_and_push.sh b/build_and_push.sh index 5d41f10..9614bb3 100755 --- a/build_and_push.sh +++ b/build_and_push.sh @@ -4,6 +4,7 @@ IMAGES=( "eduardosilva/wireguard_webadmin:latest" "eduardosilva/wireguard_webadmin_cron:latest" "eduardosilva/wireguard_webadmin_nginx:latest" + "eduardosilva/wireguard_webadmin_dns:latest" ) build_images() { diff --git a/containers/dnsmasq/Dockerfile-dnsmasq b/containers/dnsmasq/Dockerfile-dnsmasq new file mode 100644 index 0000000..8ea0614 --- /dev/null +++ b/containers/dnsmasq/Dockerfile-dnsmasq @@ -0,0 +1,13 @@ +FROM ubuntu:latest + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y dnsmasq nano inotify-tools psmisc && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/containers/dnsmasq/entrypoint.sh b/containers/dnsmasq/entrypoint.sh new file mode 100644 index 0000000..a188de7 --- /dev/null +++ b/containers/dnsmasq/entrypoint.sh @@ -0,0 +1,40 @@ +#!/bin/bash +CONFIG_FILE="/etc/dnsmasq/wireguard_webadmin_dns.conf" +DEFAULT_CONFIG_CONTENT=" +no-dhcp-interface= +server=1.1.1.1 +server=1.0.0.1 + +listen-address=0.0.0.0 +bind-interfaces +" + +create_default_config() { + if [ ! -f "$CONFIG_FILE" ]; then + echo "Config file not found, creating a new one..." + echo "$DEFAULT_CONFIG_CONTENT" > "$CONFIG_FILE" + fi +} + +start_dnsmasq() { + dnsmasq -C "$CONFIG_FILE" & + while inotifywait -e modify "$CONFIG_FILE"; do + echo "Configuration changed, reloading dnsmasq..." + pkill dnsmasq + sleep 5 + dnsmasq -C "$CONFIG_FILE" & + done +} + + +handle_sigint() { + echo "SIGINT received. Stopping inotifywait and dnsmasq..." + pkill inotifywait + pkill dnsmasq + exit 0 +} + +trap handle_sigint SIGINT + +create_default_config +start_dnsmasq diff --git a/dns/forms.py b/dns/forms.py index deb66f6..bf4acb4 100644 --- a/dns/forms.py +++ b/dns/forms.py @@ -21,6 +21,7 @@ class DNSSettingsForm(forms.ModelForm): self.helper = FormHelper() self.fields['dns_primary'].label = 'Primary Resolver' self.fields['dns_secondary'].label = 'Secondary Resolver' + self.fields['dns_primary'].required = True self.helper.form_method = 'post' self.helper.layout = Layout( Fieldset( diff --git a/dns/functions.py b/dns/functions.py index 52cd70c..a341189 100644 --- a/dns/functions.py +++ b/dns/functions.py @@ -25,9 +25,51 @@ server: local-zone: "local." static do-not-query-localhost: {do_not_query_localhost} verbosity: 1 - recursion: yes ''' unbound_config += forward_zone - for static_host in static_hosts: - unbound_config += f'local-data: "{static_host.hostname}. IN A {static_host.ip_address}"\n' + + if static_hosts: + unbound_config += '\nlocal-zone: "." transparent\n' + for static_host in static_hosts: + unbound_config += f' local-data: "{static_host.hostname}. IN A {static_host.ip_address}"\n' return unbound_config + + +def generate_dnsdist_config(): + dns_settings = DNSSettings.objects.get(name='dns_settings') + static_hosts = StaticHost.objects.all() + dnsdist_config = "setLocal('0.0.0.0:53')\n" + dnsdist_config += "setACL('0.0.0.0/0')\n" + + if dns_settings.dns_primary: + dnsdist_config += f"newServer({{address='{dns_settings.dns_primary}', pool='upstreams'}})\n" + if dns_settings.dns_secondary: + dnsdist_config += f"newServer({{address='{dns_settings.dns_secondary}', pool='upstreams'}})\n" + + if static_hosts: + dnsdist_config += "addAction(makeRule(''), PoolAction('staticHosts'))\n" + for static_host in static_hosts: + dnsdist_config += f"addLocal('{static_host.hostname}', '{static_host.ip_address}')\n" + + return dnsdist_config + + +def generate_dnsmasq_config(): + dns_settings = DNSSettings.objects.get(name='dns_settings') + static_hosts = StaticHost.objects.all() + dnsmasq_config = f''' +no-dhcp-interface= +listen-address=0.0.0.0 +bind-interfaces + +''' + if dns_settings.dns_primary: + dnsmasq_config += f'server={dns_settings.dns_primary}\n' + if dns_settings.dns_secondary: + dnsmasq_config += f'server={dns_settings.dns_secondary}\n' + + if static_hosts: + dnsmasq_config += '\n' + for static_host in static_hosts: + dnsmasq_config += f'address=/{static_host.hostname}/{static_host.ip_address}\n' + return dnsmasq_config diff --git a/dns/views.py b/dns/views.py index f3467f1..b11c68a 100644 --- a/dns/views.py +++ b/dns/views.py @@ -4,7 +4,7 @@ from django.contrib import messages from user_manager.models import UserAcl from .models import DNSSettings, StaticHost from .forms import StaticHostForm, DNSSettingsForm -from .functions import generate_unbound_config +from .functions import generate_dnsmasq_config from django.conf import settings @@ -13,9 +13,9 @@ def view_apply_dns_config(request): dns_settings, _ = DNSSettings.objects.get_or_create(name='dns_settings') dns_settings.pending_changes = False dns_settings.save() - unbound_config = generate_unbound_config() - with open(settings.UNBOUND_CONFIG, 'w') as f: - f.write(unbound_config) + dnsmasq_config = generate_dnsmasq_config() + with open(settings.DNS_CONFIG_FILE, 'w') as f: + f.write(dnsmasq_config) messages.success(request, 'DNS settings applied successfully') return redirect('/dns/') @@ -48,12 +48,7 @@ def view_manage_dns_settings(request):

All DNS queries will be forwarded to the primary resolver. If the primary resolver is not available, the secondary resolver will be used.

- - Local DNS Resolution - -

- If no forwarders are specified, the system will locally resolve DNS queries. This can lead to slower DNS resolution times, but can be useful in certain scenarios. -

+ ''' context = { diff --git a/docker-compose-build.yml b/docker-compose-build.yml index 4d4ab41..66ce72c 100644 --- a/docker-compose-build.yml +++ b/docker-compose-build.yml @@ -11,6 +11,12 @@ services: context: ./cron dockerfile: Dockerfile-cron + wireguard-webadmin-dns: + image: eduardosilva/wireguard_webadmin_dns:latest + build: + context: ./containers/dnsmasq + dockerfile: Dockerfile-dnsmasq + wireguard-webadmin-nginx: image: eduardosilva/wireguard_webadmin_nginx:latest build: diff --git a/docker-compose-no-nginx-dev.yml b/docker-compose-no-nginx-dev.yml index c292ac9..1d3fc47 100644 --- a/docker-compose-no-nginx-dev.yml +++ b/docker-compose-no-nginx-dev.yml @@ -8,10 +8,12 @@ services: environment: - SERVER_ADDRESS=127.0.0.1 - DEBUG_MODE=True + - COMPOSE_VERSION=02b volumes: - wireguard:/etc/wireguard - static_volume:/app_static_files/ - .:/app + - dnsmasq_conf:/etc/dnsmasq ports: # Do not directly expose the Django port to the internet, use some kind of reverse proxy with SSL. - "127.0.0.1:8000:8000" @@ -38,6 +40,16 @@ services: depends_on: - wireguard-webadmin + wireguard-webadmin-dns: + container_name: wireguard-webadmin-dns + restart: unless-stopped + build: + context: ./containers/dnsmasq + dockerfile: Dockerfile-dnsmasq + volumes: + - dnsmasq_conf:/etc/dnsmasq/ + volumes: static_volume: wireguard: + dnsmasq_conf: \ No newline at end of file diff --git a/docker-compose-no-nginx.yml b/docker-compose-no-nginx.yml index 64328ab..a79a011 100644 --- a/docker-compose-no-nginx.yml +++ b/docker-compose-no-nginx.yml @@ -7,9 +7,11 @@ services: environment: - SERVER_ADDRESS=127.0.0.1 - DEBUG_MODE=True + - COMPOSE_VERSION=02b volumes: - wireguard:/etc/wireguard - static_volume:/app_static_files/ + - dnsmasq_conf:/etc/dnsmasq ports: # Do not directly expose the Django port to the internet, use some kind of reverse proxy with SSL. - "127.0.0.1:8000:8000" @@ -33,6 +35,14 @@ services: depends_on: - wireguard-webadmin + wireguard-webadmin-dns: + container_name: wireguard-webadmin-dns + restart: unless-stopped + image: eduardosilva/wireguard_webadmin_dns:latest + volumes: + - dnsmasq_conf:/etc/dnsmasq/ + volumes: static_volume: wireguard: + dnsmasq_conf: diff --git a/docker-compose.yml b/docker-compose.yml index 1ae4c7a..03ea77b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,9 +7,11 @@ services: environment: - SERVER_ADDRESS=${SERVER_ADDRESS} - DEBUG_MODE=${DEBUG_MODE} + - COMPOSE_VERSION=02b volumes: - wireguard:/etc/wireguard - static_volume:/app_static_files/ + - dnsmasq_conf:/etc/dnsmasq ports: # Do not directly expose the Django port to the internet, use the reverse proxy below instead # - "127.0.0.1:8000:8000" @@ -33,6 +35,13 @@ services: depends_on: - wireguard-webadmin + wireguard-webadmin-dns: + container_name: wireguard-webadmin-dns + restart: unless-stopped + image: eduardosilva/wireguard_webadmin_dns:latest + volumes: + - dnsmasq_conf:/etc/dnsmasq/ + wireguard-webadmin-nginx: container_name: wireguard-webadmin-nginx restart: unless-stopped @@ -48,3 +57,4 @@ volumes: static_volume: https_cert: wireguard: + dnsmasq_conf: diff --git a/entrypoint.sh b/entrypoint.sh index bf25696..cdbd17b 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,6 +2,11 @@ set -e +if [[ "$COMPOSE_VERSION" != "02b" ]]; then + echo "ERROR: Please upgrade your docker compose file. Exiting." + exit 1 +fi + if [ -z "$SERVER_ADDRESS" ]; then echo "SERVER_ADDRESS environment variable is not set. Exiting." exit 1 diff --git a/firewall/tools.py b/firewall/tools.py index c1daf63..1c91e06 100644 --- a/firewall/tools.py +++ b/firewall/tools.py @@ -143,6 +143,19 @@ def export_user_firewall(): return "".join(iptables_rules) +def generate_redirect_dns_rules(): + wireguard_instance_list = WireGuardInstance.objects.all() + firewall_settings, firewall_settings_created = FirewallSettings.objects.get_or_create(name='global') + dns_redirect_rules = '' + for wireguard_instance in wireguard_instance_list: + dns_redirect_rules += f"# DNS Redirect for instance wg{wireguard_instance.instance_id}\n" + dns_redirect_rules += f"iptables -t nat -A WGWADM_PREROUTING -i wg{wireguard_instance.instance_id} -d {wireguard_instance.address} -p udp --dport 53 -j DNAT --to $DNS_IP:53\n" + dns_redirect_rules += f"iptables -t nat -A WGWADM_PREROUTING -i wg{wireguard_instance.instance_id} -d {wireguard_instance.address} -p tcp --dport 53 -j DNAT --to $DNS_IP:53\n" + dns_redirect_rules += f"iptables -t nat -A WGWADM_POSTROUTING -i wg{wireguard_instance.instance_id} -o {firewall_settings.wan_interface} -d $DNS_IP -j MASQUERADE\n" + dns_redirect_rules += f"iptables -t filter -A WGWADM_FORWARD -i wg{wireguard_instance.instance_id} -o {firewall_settings.wan_interface} -d $DNS_IP -j ACCEPT\n" + return dns_redirect_rules + + def generate_firewall_header(): firewall_settings, firewall_settings_created = FirewallSettings.objects.get_or_create(name='global') header = f'''#!/bin/bash @@ -151,6 +164,10 @@ def generate_firewall_header(): # # This script was generated by WireGuard_WebAdmin on {timezone.now().strftime('%Y-%m-%d %H:%M:%S %Z')} # +DNS_IP=$(host wireguard-webadmin-dns | grep -oP 'has address \K[\d\.]+') +if [ -z "$DNS_IP" ]; then + DNS_IP="127.0.0.250" +fi iptables -t nat -N WGWADM_POSTROUTING >> /dev/null 2>&1 iptables -t nat -N WGWADM_PREROUTING >> /dev/null 2>&1 diff --git a/templates/wireguard/wireguard_manage_server.html b/templates/wireguard/wireguard_manage_server.html index 46d21c4..71cdff0 100644 --- a/templates/wireguard/wireguard_manage_server.html +++ b/templates/wireguard/wireguard_manage_server.html @@ -98,11 +98,11 @@
- +
- +
diff --git a/wireguard/forms.py b/wireguard/forms.py index ebaf6af..20fb821 100644 --- a/wireguard/forms.py +++ b/wireguard/forms.py @@ -16,7 +16,7 @@ class WireGuardInstanceForm(forms.ModelForm): post_down = forms.CharField(label='Post Down', required=False) peer_list_refresh_interval = forms.IntegerField(label='Web Refresh Interval', initial=20) dns_primary = forms.GenericIPAddressField(label='Primary DNS', initial='1.1.1.1', required=False) - dns_secondary = forms.GenericIPAddressField(label='Secondary DNS', initial='1.0.0.1', required=False) + dns_secondary = forms.GenericIPAddressField(label='Secondary DNS', initial='', required=False) class Meta: model = WireGuardInstance diff --git a/wireguard/views.py b/wireguard/views.py index 950717f..3ab12d9 100644 --- a/wireguard/views.py +++ b/wireguard/views.py @@ -60,6 +60,7 @@ def generate_instance_defaults(): 'private_key': new_private_key, 'public_key': new_public_key, 'address': new_address, + 'dns_primary': new_address, 'netmask': 24, 'persistent_keepalive': 25, 'hostname': 'myserver.example.com', diff --git a/wireguard_tools/views.py b/wireguard_tools/views.py index f864826..a33b0ff 100644 --- a/wireguard_tools/views.py +++ b/wireguard_tools/views.py @@ -4,7 +4,8 @@ import qrcode import subprocess from django.http import HttpResponse from django.shortcuts import redirect, get_object_or_404, render -from firewall.tools import generate_firewall_header, generate_firewall_footer, generate_port_forward_firewall, export_user_firewall +from firewall.tools import generate_firewall_header, generate_firewall_footer, generate_port_forward_firewall, \ + export_user_firewall, generate_redirect_dns_rules from user_manager.models import UserAcl from wireguard.models import WireGuardInstance, Peer, PeerAllowedIP from firewall.models import RedirectRule @@ -55,6 +56,7 @@ def generate_peer_config(peer_uuid): def export_firewall_configuration(): firewall_content = generate_firewall_header() + firewall_content += generate_redirect_dns_rules() firewall_content += generate_port_forward_firewall() firewall_content += export_user_firewall() firewall_content += generate_firewall_footer() diff --git a/wireguard_webadmin/settings.py b/wireguard_webadmin/settings.py index 51bc2de..d547ac9 100644 --- a/wireguard_webadmin/settings.py +++ b/wireguard_webadmin/settings.py @@ -131,9 +131,9 @@ STATICFILES_DIRS = [ # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field -UNBOUND_CONFIG = '/config/unbound.conf' +DNS_CONFIG_FILE = '/etc/dnsmasq/wireguard_webadmin_dns.conf' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -WIREGUARD_WEBADMIN_VERSION = 9607 +WIREGUARD_WEBADMIN_VERSION = 9609 from wireguard_webadmin.production_settings import * diff --git a/wireguard_webadmin/urls.py b/wireguard_webadmin/urls.py index 9918f86..375c8b9 100644 --- a/wireguard_webadmin/urls.py +++ b/wireguard_webadmin/urls.py @@ -28,7 +28,7 @@ from dns.views import view_static_host_list, view_manage_static_host, view_manag urlpatterns = [ - path('admin/', admin.site.urls), + # path('admin/', admin.site.urls), path('', view_welcome, name='welcome'), path('status/', view_wireguard_status, name='wireguard_status'), path('dns/', view_static_host_list, name='static_host_list'),