Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions scripts/fix-dns-port.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# scripts/fix-dns-port.sh
# Detect and disable systemd-resolved 53 port binding

set -e

ACTION=$1

case "$ACTION" in
--check)
if systemctl is-active --quiet systemd-resolved; then
if grep -q "^DNSStubListener=no" /etc/systemd/resolved.conf; then
echo "systemd-resolved is active, but DNSStubListener is disabled. Port 53 is free."
exit 0
else
echo "systemd-resolved is active and may be using port 53."
exit 1
fi
else
echo "systemd-resolved is not active."
exit 0
fi
;;
--apply)
echo "Disabling DNSStubListener in systemd-resolved..."
if ! grep -q "^DNSStubListener=no" /etc/systemd/resolved.conf; then
sudo sed -i 's/^#*DNSStubListener=.*/DNSStubListener=no/' /etc/systemd/resolved.conf
if ! grep -q "^DNSStubListener=no" /etc/systemd/resolved.conf; then
echo "DNSStubListener=no" | sudo tee -a /etc/systemd/resolved.conf
fi
fi
sudo systemctl restart systemd-resolved
echo "systemd-resolved restarted. Port 53 should now be free."
;;
--restore)
echo "Restoring DNSStubListener in systemd-resolved..."
sudo sed -i 's/^DNSStubListener=no/#DNSStubListener=yes/' /etc/systemd/resolved.conf
sudo systemctl restart systemd-resolved
echo "systemd-resolved restored."
;;
*)
echo "Usage: $0 {--check|--apply|--restore}"
exit 1
;;
esac
23 changes: 22 additions & 1 deletion stacks/network/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
TZ=Asia/Shanghai
DOMAIN=localhost
DOMAIN=example.com

# AdGuard Home
ADGUARD_DOMAIN=adguard.example.com

# WireGuard Easy
WG_HOST=vpn.example.com
WG_DOMAIN=wg.example.com
# Hash of the Web UI password, generate with: docker run -it ghcr.io/wg-easy/wg-easy wgeasy hashpwd 'your_password'
WG_PASSWORD_HASH=
WG_ALLOWED_IPS=192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12
# IP of the machine running AdGuard Home to serve DNS to VPN clients
WG_DEFAULT_DNS=192.168.1.100

# Cloudflare DDNS
CF_API_TOKEN=your_cloudflare_api_token
CF_DOMAINS=vpn.example.com,home.example.com
CF_PROXIED=false
CF_IP6_PROVIDER=none

# Nginx Proxy Manager
NPM_DOMAIN=npm.example.com
62 changes: 62 additions & 0 deletions stacks/network/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Network Stack

Provides core network services for the homelab: DNS filtering, VPN access, and dynamic DNS.

## Services Included

- **AdGuard Home**: DNS filtering and ad blocking.
- **WireGuard Easy**: VPN server with a web-based management UI.
- **Cloudflare DDNS**: Automatically updates Cloudflare DNS records for dynamic IPs.
- **Unbound**: Recursive DNS resolver used as AdGuard Home's upstream.
- **Nginx Proxy Manager**: Alternative reverse proxy manager.

## Prerequisites

Port 53 (UDP/TCP) must be free on the host to run AdGuard Home. Many Linux distributions use `systemd-resolved` which binds to port 53 by default.

You can use the provided script to free the port:

```bash
# Check if systemd-resolved is blocking port 53
../../scripts/fix-dns-port.sh --check

# Apply fix (disables DNSStubListener)
../../scripts/fix-dns-port.sh --apply

# (Optional) Restore default behavior
../../scripts/fix-dns-port.sh --restore
```

## Setup Instructions

1. Copy the example environment file and configure it:
```bash
cp .env.example .env
nano .env
```

2. Start the stack:
```bash
docker compose up -d
```

## Configuration

### AdGuard Home
- **Web UI**: Access via `https://adguard.example.com`
- **Upstream DNS**: In the AdGuard Home UI, go to Settings -> DNS settings. Set the upstream DNS server to `127.0.0.11` (Docker resolver) or directly to `unbound` (the hostname of the unbound container).
- **Filter Lists**: We recommend adding [OISD](https://oisd.nl/) or similar comprehensive blocklists.

### WireGuard Easy
- **Web UI**: Access via `https://wg.example.com`
- **VPN Clients**: You can create new clients and download configs or scan QR codes via the Web UI.
- **Split Tunneling**: To only route local homelab traffic (and DNS) over the VPN, modify `WG_ALLOWED_IPS` in `.env` to include your internal subnets (e.g., `192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12`). Leave `0.0.0.0/0` if you want all traffic to pass through the VPN.
- **DNS**: Ensure `WG_DEFAULT_DNS` in `.env` points to your AdGuard Home's host IP (e.g., `192.168.1.100`). This ensures VPN clients use your local AdGuard Home for ad-blocking and local name resolution.

### Cloudflare DDNS
- Supports IPv4 and IPv6 out-of-the-box.
- Configure `CF_API_TOKEN` and `CF_DOMAINS` in `.env`.
- Ensure your API token has `Zone.DNS` edit permissions.

## CN Mirrors
If you have trouble pulling images from `ghcr.io` or Docker Hub in mainland China, uncomment the alternative image tags in `docker-compose.yml` (e.g., `swr.cn-north-4.myhuaweicloud.com/...`).
110 changes: 102 additions & 8 deletions stacks/network/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,56 +1,150 @@
services:
# ---------------------------------------------------------------------------
# AdGuard Home — DNS filtering and ad blocking
# ---------------------------------------------------------------------------
adguardhome:
image: adguard/adguardhome:v0.107.55
image: adguard/adguardhome:v0.107.52
container_name: adguardhome
restart: unless-stopped
networks:
- proxy
- network
volumes:
- adguard-work:/opt/adguardhome/work
- adguard-conf:/opt/adguardhome/conf
ports:
- 53:53/tcp
- 53:53/udp
- "53:53/tcp"
- "53:53/udp"
labels:
- traefik.enable=true
- traefik.http.routers.adguard.rule=Host()
- traefik.http.routers.adguard.rule=Host(`${ADGUARD_DOMAIN}`)
- traefik.http.routers.adguard.entrypoints=websecure
- traefik.http.routers.adguard.tls=true
- traefik.http.routers.adguard.tls.certresolver=letsencrypt
- traefik.http.services.adguard.loadbalancer.server.port=3000
healthcheck:
test: [CMD, wget, -qO-, http://localhost:3000]
test: ["CMD", "wget", "-qO-", "http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s

# ---------------------------------------------------------------------------
# Unbound — Recursive DNS resolver
# ---------------------------------------------------------------------------
unbound:
image: mvance/unbound:1.21.1
# CN mirror fallback:
# image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/mvance/unbound:1.21.1
container_name: unbound
restart: unless-stopped
networks:
- network
volumes:
- unbound-conf:/opt/unbound/etc/unbound/
healthcheck:
test: ["CMD", "dig", "@127.0.0.1", "cloudflare.com"]
interval: 30s
timeout: 10s
retries: 3

# ---------------------------------------------------------------------------
# WireGuard Easy — VPN server and Web UI
# ---------------------------------------------------------------------------
wg-easy:
image: ghcr.io/wg-easy/wg-easy:14
# CN mirror fallback:
# image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/ghcr.io/wg-easy/wg-easy:14
container_name: wg-easy
restart: unless-stopped
environment:
- WG_HOST=${WG_HOST}
- PASSWORD_HASH=${WG_PASSWORD_HASH}
- WG_PORT=51820
- WG_DEFAULT_DNS=${WG_DEFAULT_DNS:-1.1.1.1}
- WG_ALLOWED_IPS=${WG_ALLOWED_IPS:-192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12}
volumes:
- wg-data:/etc/wireguard
ports:
- "51820:51820/udp"
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
networks:
- proxy
- network
labels:
- traefik.enable=true
- traefik.http.routers.wgeasy.rule=Host(`${WG_DOMAIN}`)
- traefik.http.routers.wgeasy.entrypoints=websecure
- traefik.http.routers.wgeasy.tls=true
- traefik.http.routers.wgeasy.tls.certresolver=letsencrypt
- traefik.http.services.wgeasy.loadbalancer.server.port=51821
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:51821 || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s

# ---------------------------------------------------------------------------
# Cloudflare DDNS — Dynamic DNS updater
# ---------------------------------------------------------------------------
cloudflare-ddns:
image: ghcr.io/favonia/cloudflare-ddns:1.14.0
# CN mirror fallback:
# image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/ghcr.io/favonia/cloudflare-ddns:1.14.0
container_name: cloudflare-ddns
restart: unless-stopped
network_mode: host
environment:
- CF_API_TOKEN=${CF_API_TOKEN}
- DOMAINS=${CF_DOMAINS}
- PROXIED=${CF_PROXIED:-false}
- IP6_PROVIDER=${CF_IP6_PROVIDER:-none}

# ---------------------------------------------------------------------------
# Nginx Proxy Manager — Legacy/Alternative Proxy
# ---------------------------------------------------------------------------
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:2.11.3
container_name: nginx-proxy-manager
restart: unless-stopped
networks:
- proxy
- network
volumes:
- npm-data:/data
- npm-letsencrypt:/etc/letsencrypt
ports:
- 8181:81
- "8181:81"
labels:
- traefik.enable=true
- traefik.http.routers.npm.rule=Host()
- traefik.http.routers.npm.rule=Host(`${NPM_DOMAIN}`)
- traefik.http.routers.npm.entrypoints=websecure
- traefik.http.routers.npm.tls=true
- traefik.http.routers.npm.tls.certresolver=letsencrypt
- traefik.http.services.npm.loadbalancer.server.port=81
healthcheck:
test: [CMD-SHELL, wget -q --spider http://localhost:3000/api/health || exit 0]
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 0"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s

networks:
proxy:
external: true
network:
name: network

volumes:
adguard-work:
adguard-conf:
unbound-conf:
wg-data:
npm-data:
npm-letsencrypt: