DNS Container and docker compose

This commit is contained in:
Eduardo Silva 2024-04-29 15:26:30 -03:00
parent da1513e560
commit aefd3f698b
18 changed files with 175 additions and 20 deletions

View File

@ -4,6 +4,7 @@ IMAGES=(
"eduardosilva/wireguard_webadmin:latest" "eduardosilva/wireguard_webadmin:latest"
"eduardosilva/wireguard_webadmin_cron:latest" "eduardosilva/wireguard_webadmin_cron:latest"
"eduardosilva/wireguard_webadmin_nginx:latest" "eduardosilva/wireguard_webadmin_nginx:latest"
"eduardosilva/wireguard_webadmin_dns:latest"
) )
build_images() { build_images() {

View File

@ -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"]

View File

@ -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

View File

@ -21,6 +21,7 @@ class DNSSettingsForm(forms.ModelForm):
self.helper = FormHelper() self.helper = FormHelper()
self.fields['dns_primary'].label = 'Primary Resolver' self.fields['dns_primary'].label = 'Primary Resolver'
self.fields['dns_secondary'].label = 'Secondary Resolver' self.fields['dns_secondary'].label = 'Secondary Resolver'
self.fields['dns_primary'].required = True
self.helper.form_method = 'post' self.helper.form_method = 'post'
self.helper.layout = Layout( self.helper.layout = Layout(
Fieldset( Fieldset(

View File

@ -25,9 +25,51 @@ server:
local-zone: "local." static local-zone: "local." static
do-not-query-localhost: {do_not_query_localhost} do-not-query-localhost: {do_not_query_localhost}
verbosity: 1 verbosity: 1
recursion: yes
''' '''
unbound_config += forward_zone 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 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

View File

@ -4,7 +4,7 @@ from django.contrib import messages
from user_manager.models import UserAcl from user_manager.models import UserAcl
from .models import DNSSettings, StaticHost from .models import DNSSettings, StaticHost
from .forms import StaticHostForm, DNSSettingsForm from .forms import StaticHostForm, DNSSettingsForm
from .functions import generate_unbound_config from .functions import generate_dnsmasq_config
from django.conf import settings 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, _ = DNSSettings.objects.get_or_create(name='dns_settings')
dns_settings.pending_changes = False dns_settings.pending_changes = False
dns_settings.save() dns_settings.save()
unbound_config = generate_unbound_config() dnsmasq_config = generate_dnsmasq_config()
with open(settings.UNBOUND_CONFIG, 'w') as f: with open(settings.DNS_CONFIG_FILE, 'w') as f:
f.write(unbound_config) f.write(dnsmasq_config)
messages.success(request, 'DNS settings applied successfully') messages.success(request, 'DNS settings applied successfully')
return redirect('/dns/') return redirect('/dns/')
@ -48,12 +48,7 @@ def view_manage_dns_settings(request):
<p> <p>
All DNS queries will be forwarded to the primary resolver. If the primary resolver is not available, the secondary resolver will be used. All DNS queries will be forwarded to the primary resolver. If the primary resolver is not available, the secondary resolver will be used.
</p> </p>
<strong>
Local DNS Resolution
</strong>
<p>
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.
</p>
''' '''
context = { context = {

View File

@ -11,6 +11,12 @@ services:
context: ./cron context: ./cron
dockerfile: Dockerfile-cron dockerfile: Dockerfile-cron
wireguard-webadmin-dns:
image: eduardosilva/wireguard_webadmin_dns:latest
build:
context: ./containers/dnsmasq
dockerfile: Dockerfile-dnsmasq
wireguard-webadmin-nginx: wireguard-webadmin-nginx:
image: eduardosilva/wireguard_webadmin_nginx:latest image: eduardosilva/wireguard_webadmin_nginx:latest
build: build:

View File

@ -8,10 +8,12 @@ services:
environment: environment:
- SERVER_ADDRESS=127.0.0.1 - SERVER_ADDRESS=127.0.0.1
- DEBUG_MODE=True - DEBUG_MODE=True
- COMPOSE_VERSION=02b
volumes: volumes:
- wireguard:/etc/wireguard - wireguard:/etc/wireguard
- static_volume:/app_static_files/ - static_volume:/app_static_files/
- .:/app - .:/app
- dnsmasq_conf:/etc/dnsmasq
ports: ports:
# Do not directly expose the Django port to the internet, use some kind of reverse proxy with SSL. # Do not directly expose the Django port to the internet, use some kind of reverse proxy with SSL.
- "127.0.0.1:8000:8000" - "127.0.0.1:8000:8000"
@ -38,6 +40,16 @@ services:
depends_on: depends_on:
- wireguard-webadmin - 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: volumes:
static_volume: static_volume:
wireguard: wireguard:
dnsmasq_conf:

View File

@ -7,9 +7,11 @@ services:
environment: environment:
- SERVER_ADDRESS=127.0.0.1 - SERVER_ADDRESS=127.0.0.1
- DEBUG_MODE=True - DEBUG_MODE=True
- COMPOSE_VERSION=02b
volumes: volumes:
- wireguard:/etc/wireguard - wireguard:/etc/wireguard
- static_volume:/app_static_files/ - static_volume:/app_static_files/
- dnsmasq_conf:/etc/dnsmasq
ports: ports:
# Do not directly expose the Django port to the internet, use some kind of reverse proxy with SSL. # Do not directly expose the Django port to the internet, use some kind of reverse proxy with SSL.
- "127.0.0.1:8000:8000" - "127.0.0.1:8000:8000"
@ -33,6 +35,14 @@ services:
depends_on: depends_on:
- wireguard-webadmin - 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: volumes:
static_volume: static_volume:
wireguard: wireguard:
dnsmasq_conf:

View File

@ -7,9 +7,11 @@ services:
environment: environment:
- SERVER_ADDRESS=${SERVER_ADDRESS} - SERVER_ADDRESS=${SERVER_ADDRESS}
- DEBUG_MODE=${DEBUG_MODE} - DEBUG_MODE=${DEBUG_MODE}
- COMPOSE_VERSION=02b
volumes: volumes:
- wireguard:/etc/wireguard - wireguard:/etc/wireguard
- static_volume:/app_static_files/ - static_volume:/app_static_files/
- dnsmasq_conf:/etc/dnsmasq
ports: ports:
# Do not directly expose the Django port to the internet, use the reverse proxy below instead # Do not directly expose the Django port to the internet, use the reverse proxy below instead
# - "127.0.0.1:8000:8000" # - "127.0.0.1:8000:8000"
@ -33,6 +35,13 @@ services:
depends_on: depends_on:
- wireguard-webadmin - 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: wireguard-webadmin-nginx:
container_name: wireguard-webadmin-nginx container_name: wireguard-webadmin-nginx
restart: unless-stopped restart: unless-stopped
@ -48,3 +57,4 @@ volumes:
static_volume: static_volume:
https_cert: https_cert:
wireguard: wireguard:
dnsmasq_conf:

View File

@ -2,6 +2,11 @@
set -e set -e
if [[ "$COMPOSE_VERSION" != "02b" ]]; then
echo "ERROR: Please upgrade your docker compose file. Exiting."
exit 1
fi
if [ -z "$SERVER_ADDRESS" ]; then if [ -z "$SERVER_ADDRESS" ]; then
echo "SERVER_ADDRESS environment variable is not set. Exiting." echo "SERVER_ADDRESS environment variable is not set. Exiting."
exit 1 exit 1

View File

@ -143,6 +143,19 @@ def export_user_firewall():
return "".join(iptables_rules) 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(): def generate_firewall_header():
firewall_settings, firewall_settings_created = FirewallSettings.objects.get_or_create(name='global') firewall_settings, firewall_settings_created = FirewallSettings.objects.get_or_create(name='global')
header = f'''#!/bin/bash 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')} # 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_POSTROUTING >> /dev/null 2>&1
iptables -t nat -N WGWADM_PREROUTING >> /dev/null 2>&1 iptables -t nat -N WGWADM_PREROUTING >> /dev/null 2>&1

View File

@ -98,11 +98,11 @@
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label for="{{ form.dns_primary.id_for_label }}">{{ form.dns_primary.label }}</label> <label for="{{ form.dns_primary.id_for_label }}">{{ form.dns_primary.label }}</label>
<input type="text" class="form-control" id="{{ form.dns_primary.id_for_label }}" name="{{ form.dns_primary.html_name }}" placeholder="1.1.1.1" value="{{ form.dns_primary.value|default_if_none:'' }}"> <input type="text" class="form-control" id="{{ form.dns_primary.id_for_label }}" name="{{ form.dns_primary.html_name }}" value="{{ form.dns_primary.value|default_if_none:'' }}">
</div> </div>
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label for="{{ form.dns_secondary.id_for_label }}">{{ form.dns_secondary.label }}</label> <label for="{{ form.dns_secondary.id_for_label }}">{{ form.dns_secondary.label }}</label>
<input type="text" class="form-control" id="{{ form.dns_secondary.id_for_label }}" name="{{ form.dns_secondary.html_name }}" placeholder="1.0.0.1" value="{{ form.dns_secondary.value|default_if_none:'' }}"> <input type="text" class="form-control" id="{{ form.dns_secondary.id_for_label }}" name="{{ form.dns_secondary.html_name }}" value="{{ form.dns_secondary.value|default_if_none:'' }}">
</div> </div>
</div> </div>

View File

@ -16,7 +16,7 @@ class WireGuardInstanceForm(forms.ModelForm):
post_down = forms.CharField(label='Post Down', required=False) post_down = forms.CharField(label='Post Down', required=False)
peer_list_refresh_interval = forms.IntegerField(label='Web Refresh Interval', initial=20) 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_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: class Meta:
model = WireGuardInstance model = WireGuardInstance

View File

@ -60,6 +60,7 @@ def generate_instance_defaults():
'private_key': new_private_key, 'private_key': new_private_key,
'public_key': new_public_key, 'public_key': new_public_key,
'address': new_address, 'address': new_address,
'dns_primary': new_address,
'netmask': 24, 'netmask': 24,
'persistent_keepalive': 25, 'persistent_keepalive': 25,
'hostname': 'myserver.example.com', 'hostname': 'myserver.example.com',

View File

@ -4,7 +4,8 @@ import qrcode
import subprocess import subprocess
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect, get_object_or_404, render 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 user_manager.models import UserAcl
from wireguard.models import WireGuardInstance, Peer, PeerAllowedIP from wireguard.models import WireGuardInstance, Peer, PeerAllowedIP
from firewall.models import RedirectRule from firewall.models import RedirectRule
@ -55,6 +56,7 @@ def generate_peer_config(peer_uuid):
def export_firewall_configuration(): def export_firewall_configuration():
firewall_content = generate_firewall_header() firewall_content = generate_firewall_header()
firewall_content += generate_redirect_dns_rules()
firewall_content += generate_port_forward_firewall() firewall_content += generate_port_forward_firewall()
firewall_content += export_user_firewall() firewall_content += export_user_firewall()
firewall_content += generate_firewall_footer() firewall_content += generate_firewall_footer()

View File

@ -131,9 +131,9 @@ STATICFILES_DIRS = [
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field # 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' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
WIREGUARD_WEBADMIN_VERSION = 9607 WIREGUARD_WEBADMIN_VERSION = 9609
from wireguard_webadmin.production_settings import * from wireguard_webadmin.production_settings import *

View File

@ -28,7 +28,7 @@ from dns.views import view_static_host_list, view_manage_static_host, view_manag
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), # path('admin/', admin.site.urls),
path('', view_welcome, name='welcome'), path('', view_welcome, name='welcome'),
path('status/', view_wireguard_status, name='wireguard_status'), path('status/', view_wireguard_status, name='wireguard_status'),
path('dns/', view_static_host_list, name='static_host_list'), path('dns/', view_static_host_list, name='static_host_list'),