mirror of
https://github.com/angristan/openvpn-install.git
synced 2025-12-14 16:17:03 +01:00
Refactor Unbound setup and add E2E tests (#1340)
Refactor Unbound DNS installation to use modern `conf.d` pattern and add E2E testing. **Changes:** - Unified Unbound config across all distros using `/etc/unbound/unbound.conf.d/openvpn.conf` - Added startup validation with retry logic - Added `ip-freebind` to allow binding before tun interface exists - E2E tests now verify Unbound DNS resolution from VPN clients **Testing:** - Server: verifies config creation, interface binding, security options - Client: verifies DNS resolution through Unbound (10.8.0.1) --- Closes https://github.com/angristan/openvpn-install/issues/602 Closes https://github.com/angristan/openvpn-install/pull/604 Closes https://github.com/angristan/openvpn-install/issues/1189 Co-authored-by: Henry N <henrynmail-github@yahoo.de>
This commit is contained in:
@@ -351,120 +351,84 @@ function installOpenVPNRepo() {
|
||||
|
||||
function installUnbound() {
|
||||
log_info "Installing Unbound DNS resolver..."
|
||||
# If Unbound isn't installed, install it
|
||||
if [[ ! -e /etc/unbound/unbound.conf ]]; then
|
||||
|
||||
# Install Unbound if not present
|
||||
if [[ ! -e /etc/unbound/unbound.conf ]]; then
|
||||
if [[ $OS =~ (debian|ubuntu) ]]; then
|
||||
run_cmd "Installing Unbound" apt-get install -y unbound
|
||||
|
||||
# Configuration
|
||||
echo 'interface: 10.8.0.1
|
||||
access-control: 10.8.0.1/24 allow
|
||||
hide-identity: yes
|
||||
hide-version: yes
|
||||
use-caps-for-id: yes
|
||||
prefetch: yes' >>/etc/unbound/unbound.conf
|
||||
|
||||
elif [[ $OS =~ (centos|oracle) ]]; then
|
||||
run_cmd "Installing Unbound" yum install -y unbound
|
||||
|
||||
# Configuration
|
||||
sed -i 's|# interface: 0.0.0.0$|interface: 10.8.0.1|' /etc/unbound/unbound.conf
|
||||
sed -i 's|# access-control: 127.0.0.0/8 allow|access-control: 10.8.0.1/24 allow|' /etc/unbound/unbound.conf
|
||||
sed -i 's|# hide-identity: no|hide-identity: yes|' /etc/unbound/unbound.conf
|
||||
sed -i 's|# hide-version: no|hide-version: yes|' /etc/unbound/unbound.conf
|
||||
sed -i 's|use-caps-for-id: no|use-caps-for-id: yes|' /etc/unbound/unbound.conf
|
||||
|
||||
elif [[ $OS =~ (fedora|amzn2023) ]]; then
|
||||
run_cmd "Installing Unbound" dnf install -y unbound
|
||||
|
||||
# Configuration
|
||||
sed -i 's|# interface: 0.0.0.0$|interface: 10.8.0.1|' /etc/unbound/unbound.conf
|
||||
sed -i 's|# access-control: 127.0.0.0/8 allow|access-control: 10.8.0.1/24 allow|' /etc/unbound/unbound.conf
|
||||
sed -i 's|# hide-identity: no|hide-identity: yes|' /etc/unbound/unbound.conf
|
||||
sed -i 's|# hide-version: no|hide-version: yes|' /etc/unbound/unbound.conf
|
||||
sed -i 's|# use-caps-for-id: no|use-caps-for-id: yes|' /etc/unbound/unbound.conf
|
||||
|
||||
elif [[ $OS == "arch" ]]; then
|
||||
run_cmd "Installing Unbound" pacman -Syu --noconfirm unbound
|
||||
|
||||
# Get root servers list
|
||||
run_cmd "Downloading root hints" curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache
|
||||
# Verify download was successful and file contains expected content
|
||||
if [[ ! -s /etc/unbound/root.hints ]] || ! grep -q "ROOT-SERVERS" /etc/unbound/root.hints; then
|
||||
run_cmd "Cleaning up invalid file" rm -f /etc/unbound/root.hints
|
||||
log_fatal "Failed to download root.hints or file is invalid!"
|
||||
fi
|
||||
|
||||
if [[ ! -f /etc/unbound/unbound.conf.old ]]; then
|
||||
run_cmd "Backing up unbound.conf" mv /etc/unbound/unbound.conf /etc/unbound/unbound.conf.old
|
||||
fi
|
||||
|
||||
echo 'server:
|
||||
use-syslog: yes
|
||||
do-daemonize: no
|
||||
username: "unbound"
|
||||
directory: "/etc/unbound"
|
||||
trust-anchor-file: trusted-key.key
|
||||
root-hints: root.hints
|
||||
interface: 10.8.0.1
|
||||
access-control: 10.8.0.1/24 allow
|
||||
port: 53
|
||||
num-threads: 2
|
||||
use-caps-for-id: yes
|
||||
harden-glue: yes
|
||||
hide-identity: yes
|
||||
hide-version: yes
|
||||
qname-minimisation: yes
|
||||
prefetch: yes' >/etc/unbound/unbound.conf
|
||||
fi
|
||||
|
||||
# IPv6 DNS for all OS
|
||||
if [[ $IPV6_SUPPORT == 'y' ]]; then
|
||||
echo 'interface: fd42:42:42:42::1
|
||||
access-control: fd42:42:42:42::/112 allow' >>/etc/unbound/unbound.conf
|
||||
fi
|
||||
|
||||
if [[ ! $OS =~ (fedora|centos|oracle|amzn2023) ]]; then
|
||||
# DNS Rebinding fix
|
||||
echo "private-address: 10.0.0.0/8
|
||||
private-address: fd42:42:42:42::/112
|
||||
private-address: 172.16.0.0/12
|
||||
private-address: 192.168.0.0/16
|
||||
private-address: 169.254.0.0/16
|
||||
private-address: fd00::/8
|
||||
private-address: fe80::/10
|
||||
private-address: 127.0.0.0/8
|
||||
private-address: ::ffff:0:0/96" >>/etc/unbound/unbound.conf
|
||||
fi
|
||||
else # Unbound is already installed
|
||||
echo 'include: /etc/unbound/openvpn.conf' >>/etc/unbound/unbound.conf
|
||||
|
||||
# Add Unbound 'server' for the OpenVPN subnet
|
||||
echo 'server:
|
||||
interface: 10.8.0.1
|
||||
access-control: 10.8.0.1/24 allow
|
||||
hide-identity: yes
|
||||
hide-version: yes
|
||||
use-caps-for-id: yes
|
||||
prefetch: yes
|
||||
private-address: 10.0.0.0/8
|
||||
private-address: fd42:42:42:42::/112
|
||||
private-address: 172.16.0.0/12
|
||||
private-address: 192.168.0.0/16
|
||||
private-address: 169.254.0.0/16
|
||||
private-address: fd00::/8
|
||||
private-address: fe80::/10
|
||||
private-address: 127.0.0.0/8
|
||||
private-address: ::ffff:0:0/96' >/etc/unbound/openvpn.conf
|
||||
if [[ $IPV6_SUPPORT == 'y' ]]; then
|
||||
echo 'interface: fd42:42:42:42::1
|
||||
access-control: fd42:42:42:42::/112 allow' >>/etc/unbound/openvpn.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
# Configure Unbound for OpenVPN (runs whether freshly installed or pre-existing)
|
||||
# Create conf.d directory (works on all distros)
|
||||
run_cmd "Creating Unbound config directory" mkdir -p /etc/unbound/unbound.conf.d
|
||||
|
||||
# Ensure main config includes conf.d directory
|
||||
# Modern Debian/Ubuntu use include-toplevel, others need include directive
|
||||
if ! grep -qE "include(-toplevel)?:\s*.*/etc/unbound/unbound.conf.d" /etc/unbound/unbound.conf 2>/dev/null; then
|
||||
# Add include directive for conf.d if not present
|
||||
echo 'include: "/etc/unbound/unbound.conf.d/*.conf"' >>/etc/unbound/unbound.conf
|
||||
fi
|
||||
|
||||
# Generate OpenVPN-specific Unbound configuration
|
||||
# Using consistent best-practice settings across all distros
|
||||
{
|
||||
echo 'server:'
|
||||
echo ' # OpenVPN DNS resolver configuration'
|
||||
echo ' interface: 10.8.0.1'
|
||||
echo ' access-control: 10.8.0.0/24 allow'
|
||||
echo ''
|
||||
echo ' # Security hardening'
|
||||
echo ' hide-identity: yes'
|
||||
echo ' hide-version: yes'
|
||||
echo ' harden-glue: yes'
|
||||
echo ' harden-dnssec-stripped: yes'
|
||||
echo ''
|
||||
echo ' # Performance optimizations'
|
||||
echo ' prefetch: yes'
|
||||
echo ' use-caps-for-id: yes'
|
||||
echo ' qname-minimisation: yes'
|
||||
echo ''
|
||||
echo ' # Allow binding before tun interface exists'
|
||||
echo ' ip-freebind: yes'
|
||||
echo ''
|
||||
echo ' # DNS rebinding protection'
|
||||
echo ' private-address: 10.0.0.0/8'
|
||||
echo ' private-address: 172.16.0.0/12'
|
||||
echo ' private-address: 192.168.0.0/16'
|
||||
echo ' private-address: 169.254.0.0/16'
|
||||
echo ' private-address: 127.0.0.0/8'
|
||||
echo ' private-address: fd00::/8'
|
||||
echo ' private-address: fe80::/10'
|
||||
echo ' private-address: ::ffff:0:0/96'
|
||||
|
||||
# IPv6 support
|
||||
if [[ $IPV6_SUPPORT == 'y' ]]; then
|
||||
echo ''
|
||||
echo ' # IPv6 VPN support'
|
||||
echo ' interface: fd42:42:42:42::1'
|
||||
echo ' access-control: fd42:42:42:42::/112 allow'
|
||||
echo ' private-address: fd42:42:42:42::/112'
|
||||
fi
|
||||
} >/etc/unbound/unbound.conf.d/openvpn.conf
|
||||
|
||||
run_cmd "Enabling Unbound service" systemctl enable unbound
|
||||
run_cmd "Starting Unbound service" systemctl restart unbound
|
||||
|
||||
# Validate Unbound is running
|
||||
for i in {1..10}; do
|
||||
if pgrep -x unbound >/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
log_fatal "Unbound failed to start. Check 'journalctl -u unbound' for details."
|
||||
}
|
||||
|
||||
function resolvePublicIP() {
|
||||
@@ -1775,9 +1739,13 @@ function renewMenu() {
|
||||
}
|
||||
|
||||
function removeUnbound() {
|
||||
# Remove OpenVPN-related config
|
||||
run_cmd "Removing Unbound include" sed -i '/include: \/etc\/unbound\/openvpn.conf/d' /etc/unbound/unbound.conf
|
||||
run_cmd "Removing OpenVPN Unbound config" rm /etc/unbound/openvpn.conf
|
||||
run_cmd "Removing OpenVPN Unbound config" rm -f /etc/unbound/unbound.conf.d/openvpn.conf
|
||||
|
||||
# Clean up include directive if conf.d directory is now empty
|
||||
if [[ -d /etc/unbound/unbound.conf.d ]] && [[ -z "$(ls -A /etc/unbound/unbound.conf.d)" ]]; then
|
||||
run_cmd "Cleaning up Unbound include directive" \
|
||||
sed -i '/^include: "\/etc\/unbound\/unbound\.conf\.d\/\*\.conf"$/d' /etc/unbound/unbound.conf
|
||||
fi
|
||||
|
||||
until [[ $REMOVE_UNBOUND =~ (y|n) ]]; do
|
||||
log_info "If you were already using Unbound before installing OpenVPN, I removed the configuration related to OpenVPN."
|
||||
@@ -1786,7 +1754,6 @@ function removeUnbound() {
|
||||
|
||||
if [[ $REMOVE_UNBOUND == 'y' ]]; then
|
||||
log_info "Removing Unbound..."
|
||||
# Stop Unbound
|
||||
run_cmd "Stopping Unbound" systemctl stop unbound
|
||||
|
||||
if [[ $OS =~ (debian|ubuntu) ]]; then
|
||||
@@ -1800,7 +1767,6 @@ function removeUnbound() {
|
||||
fi
|
||||
|
||||
run_cmd "Removing Unbound config" rm -rf /etc/unbound/
|
||||
|
||||
log_success "Unbound removed!"
|
||||
else
|
||||
run_cmd "Restarting Unbound" systemctl restart unbound
|
||||
@@ -1887,7 +1853,7 @@ function removeOpenVPN() {
|
||||
run_cmd "Removing OpenVPN logs" rm -rf /var/log/openvpn
|
||||
|
||||
# Unbound
|
||||
if [[ -e /etc/unbound/openvpn.conf ]]; then
|
||||
if [[ -e /etc/unbound/unbound.conf.d/openvpn.conf ]]; then
|
||||
removeUnbound
|
||||
fi
|
||||
log_success "OpenVPN removed!"
|
||||
|
||||
@@ -5,11 +5,13 @@ FROM ubuntu:24.04
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install OpenVPN client and testing tools
|
||||
# dnsutils provides dig for DNS testing with Unbound
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openvpn \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
procps \
|
||||
dnsutils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create TUN device directory (device will be mounted at runtime)
|
||||
|
||||
@@ -8,21 +8,22 @@ ARG BASE_IMAGE
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install basic dependencies based on the OS
|
||||
# dnsutils/bind-utils provides dig for DNS testing with Unbound
|
||||
RUN if command -v apt-get >/dev/null; then \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
iproute2 iptables curl procps systemd systemd-sysv \
|
||||
iproute2 iptables curl procps systemd systemd-sysv dnsutils \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
elif command -v dnf >/dev/null; then \
|
||||
dnf install -y --allowerasing \
|
||||
iproute iptables curl procps-ng systemd tar gzip \
|
||||
iproute iptables curl procps-ng systemd tar gzip bind-utils \
|
||||
&& dnf clean all; \
|
||||
elif command -v yum >/dev/null; then \
|
||||
yum install -y \
|
||||
iproute iptables curl procps-ng systemd tar gzip \
|
||||
iproute iptables curl procps-ng systemd tar gzip bind-utils \
|
||||
&& yum clean all; \
|
||||
elif command -v pacman >/dev/null; then \
|
||||
pacman -Syu --noconfirm \
|
||||
iproute2 iptables curl procps-ng \
|
||||
iproute2 iptables curl procps-ng bind \
|
||||
&& pacman -Scc --noconfirm; \
|
||||
fi
|
||||
|
||||
|
||||
@@ -81,6 +81,28 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 3: DNS resolution through Unbound
|
||||
echo "Test 3: Testing DNS resolution via Unbound (10.8.0.1)..."
|
||||
DNS_SUCCESS=false
|
||||
for i in 1 2 3 4 5; do
|
||||
DIG_OUTPUT=$(dig @10.8.0.1 example.com +short +time=5 2>&1)
|
||||
if [ -n "$DIG_OUTPUT" ] && ! echo "$DIG_OUTPUT" | grep -qi "timed out\|SERVFAIL\|connection refused"; then
|
||||
DNS_SUCCESS=true
|
||||
break
|
||||
fi
|
||||
echo "DNS attempt $i failed:"
|
||||
echo "$DIG_OUTPUT"
|
||||
sleep 2
|
||||
done
|
||||
if [ "$DNS_SUCCESS" = true ]; then
|
||||
echo "PASS: DNS resolution through Unbound works"
|
||||
echo "Resolved example.com to: $(dig @10.8.0.1 example.com +short +time=5)"
|
||||
else
|
||||
echo "FAIL: DNS resolution through Unbound failed after 5 attempts"
|
||||
dig @10.8.0.1 example.com +time=5 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " ALL TESTS PASSED!"
|
||||
|
||||
@@ -20,7 +20,7 @@ export APPROVE_IP=y
|
||||
export IPV6_SUPPORT=n
|
||||
export PORT_CHOICE=1
|
||||
export PROTOCOL_CHOICE=1
|
||||
export DNS=9 # Google DNS (works in containers)
|
||||
export DNS=2 # Self-hosted Unbound DNS resolver
|
||||
export COMPRESSION_ENABLED=n
|
||||
export CUSTOMIZE_ENC=n
|
||||
export CLIENT=testclient
|
||||
@@ -29,8 +29,11 @@ export ENDPOINT=openvpn-server
|
||||
|
||||
# Prepare script for container environment:
|
||||
# - Replace systemctl calls with no-ops (systemd doesn't work in containers)
|
||||
# - Skip Unbound startup validation (we start Unbound manually later)
|
||||
# This ensures the script won't fail silently on systemctl commands
|
||||
sed 's/\bsystemctl /echo "[SKIPPED] systemctl " # /g' /opt/openvpn-install.sh >/tmp/openvpn-install.sh
|
||||
sed -e 's/\bsystemctl /echo "[SKIPPED] systemctl " # /g' \
|
||||
-e 's/log_fatal "Unbound failed to start/return 0 # [SKIPPED] /g' \
|
||||
/opt/openvpn-install.sh >/tmp/openvpn-install.sh
|
||||
chmod +x /tmp/openvpn-install.sh
|
||||
|
||||
echo "Running OpenVPN install script..."
|
||||
@@ -243,6 +246,80 @@ echo ""
|
||||
echo "=== All Certificate Renewal Tests PASSED ==="
|
||||
echo ""
|
||||
|
||||
# =====================================================
|
||||
# Start and verify Unbound DNS resolver
|
||||
# =====================================================
|
||||
echo "=== Starting Unbound DNS Resolver ==="
|
||||
|
||||
# Start Unbound manually (systemctl commands are no-ops in container)
|
||||
if [ -f /etc/unbound/unbound.conf ]; then
|
||||
echo "Starting Unbound DNS resolver..."
|
||||
|
||||
# Create root key for DNSSEC if it doesn't exist
|
||||
# Normally, unbound.service's ExecStartPre copies /usr/share/dns/root.key to /var/lib/unbound/root.key
|
||||
# In Docker, policy-rc.d blocks service starts during apt install, so this never happens
|
||||
if [ ! -f /var/lib/unbound/root.key ] && [ -f /usr/share/dns/root.key ]; then
|
||||
mkdir -p /var/lib/unbound
|
||||
cp /usr/share/dns/root.key /var/lib/unbound/root.key
|
||||
chown -R unbound:unbound /var/lib/unbound 2>/dev/null || true
|
||||
fi
|
||||
|
||||
unbound
|
||||
# Poll up to 10 seconds for Unbound to start
|
||||
for _ in $(seq 1 10); do
|
||||
if pgrep -x unbound >/dev/null; then
|
||||
echo "PASS: Unbound is running"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if ! pgrep -x unbound >/dev/null; then
|
||||
echo "FAIL: Unbound failed to start"
|
||||
# Show debug info
|
||||
unbound-checkconf /etc/unbound/unbound.conf 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "FAIL: /etc/unbound/unbound.conf not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Verifying Unbound Installation ==="
|
||||
|
||||
# Verify Unbound config exists in conf.d directory
|
||||
UNBOUND_OPENVPN_CONF="/etc/unbound/unbound.conf.d/openvpn.conf"
|
||||
if [ -f "$UNBOUND_OPENVPN_CONF" ]; then
|
||||
echo "PASS: Found Unbound config at $UNBOUND_OPENVPN_CONF"
|
||||
else
|
||||
echo "FAIL: OpenVPN Unbound config not found at $UNBOUND_OPENVPN_CONF"
|
||||
echo "Contents of /etc/unbound/:"
|
||||
ls -la /etc/unbound/
|
||||
ls -la /etc/unbound/unbound.conf.d/ 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify Unbound listens on VPN gateway
|
||||
if grep -q "interface: 10.8.0.1" "$UNBOUND_OPENVPN_CONF"; then
|
||||
echo "PASS: Unbound configured to listen on 10.8.0.1"
|
||||
else
|
||||
echo "FAIL: Unbound not configured for 10.8.0.1"
|
||||
cat "$UNBOUND_OPENVPN_CONF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify OpenVPN pushes correct DNS
|
||||
if grep -q 'push "dhcp-option DNS 10.8.0.1"' /etc/openvpn/server.conf; then
|
||||
echo "PASS: OpenVPN configured to push Unbound DNS"
|
||||
else
|
||||
echo "FAIL: OpenVPN not configured to push Unbound DNS"
|
||||
grep "dhcp-option DNS" /etc/openvpn/server.conf || echo "No DNS push found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Unbound Installation Verified ==="
|
||||
echo ""
|
||||
|
||||
# Start OpenVPN server manually (systemd doesn't work in containers)
|
||||
echo "Starting OpenVPN server..."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user