最近给自己的 VPS 搭了一套 DNS/DoH 服务,目标很明确:
- 对外提供 DoH:
https://dns.example.com/dns-query - 支持广告过滤
- 支持国内外 DNS 分流
- 不暴露公网 53 端口,降低被滥用风险
- 可以访问被墙网站(比如 linuxdo)
🧭最终方案:
客户端
-> https://dns.example.com/dns-query
-> Nginx
-> AdGuard Home
-> Mosdns
-> 国内/国外上游 DNS
架构说明
🧭这套方案里,各组件职责如下:
Nginx:负责 HTTPS/DoH 入口,反代到 AdGuard Home
AdGuard Home:负责广告过滤、查询日志、缓存、DNS 入口
Mosdns:负责国内外分流
请求链路:
客户端 DoH 请求
-> Nginx 443
-> 127.0.0.1:3000/dns-query
-> AdGuard Home
-> 127.0.0.1:5335
-> Mosdns
-> 国内域名走腾讯/阿里 DoH
-> 国外域名走 Cloudflare/Google DoH
准备工作
服务器环境:
Ubuntu 24.04
Docker / Docker Compose
Nginx
一个已解析到服务器的域名:dns.example.com
安装基础工具:
apt update
apt install -y ca-certificates curl gnupg lsb-release dnsutils certbot
部署目录
我把 DNS 服务放在:
/opt/dns-stack
创建目录:
mkdir -p /opt/dns-stack/adguard/conf
mkdir -p /opt/dns-stack/adguard/work
mkdir -p /opt/dns-stack/mosdns
Docker Compose
创建 /opt/dns-stack/docker-compose.yml
services:
mosdns:
image: irinesistiana/mosdns:v5.3.4
container_name: mosdns
restart: unless-stopped
network_mode: host
volumes:
- ./mosdns:/etc/mosdns:ro
command: ["mosdns", "start", "-c", "/etc/mosdns/config.yaml"]
adguardhome:
image: adguard/adguardhome:latest
container_name: adguardhome
restart: unless-stopped
network_mode: host
depends_on:
- mosdns
volumes:
- ./adguard/work:/opt/adguardhome/work
- ./adguard/conf:/opt/adguardhome/conf
这里使用 network_mode: host,但服务都绑定到 127.0.0.1,不会直接暴露公网 DNS 端口。
配置 Mosdns 国内外分流
先下载国内域名规则:
curl -fsSL "https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/direct-list.txt" \
-o /opt/dns-stack/mosdns/geosite_cn.txt
创建 /opt/dns-stack/mosdns/config.yaml:
log:
level: info
plugins:
- tag: forward_cn
type: forward
args:
concurrent: 2
upstreams:
- addr: "https://doh.pub/dns-query"
- addr: "https://dns.alidns.com/dns-query"
- tag: forward_global
type: forward
args:
concurrent: 2
upstreams:
- addr: "https://cloudflare-dns.com/dns-query"
- addr: "https://dns.google/dns-query"
- tag: cache
type: cache
args:
size: 32768
lazy_cache_ttl: 86400
- tag: main_sequence
type: sequence
args:
- exec: $cache
- matches:
- has_resp
exec: accept
- matches:
- qname &/etc/mosdns/geosite_cn.txt
exec: $forward_cn
- matches:
- has_resp
exec: accept
- exec: $forward_global
- tag: udp_server
type: udp_server
args:
entry: main_sequence
listen: 127.0.0.1:5335
- tag: tcp_server
type: tcp_server
args:
entry: main_sequence
listen: 127.0.0.1:5335
这个逻辑很简单:
命中国内域名列表 -> 走腾讯 DNSPod / 阿里 AliDNS
未命中 -> 走 Cloudflare / Google
配置 AdGuard Home
AdGuard Home 负责广告过滤和 DNS 查询入口。
创建 /opt/dns-stack/adguard/conf/AdGuardHome.yaml,核心配置如下:
http:
address: 127.0.0.1:3000
session_ttl: 720h
users: []
dns:
bind_hosts:
- 127.0.0.1
port: 5353
protection_enabled: true
ratelimit: 20
refuse_any: true
upstream_dns:
- 127.0.0.1:5335
bootstrap_dns:
- 223.5.5.5
- 119.29.29.29
- 1.1.1.1
cache_enabled: true
cache_size: 4194304
cache_optimistic: true
trusted_proxies:
- 127.0.0.0/8
- ::1/128
tls:
enabled: false
allow_unencrypted_doh: true
filters:
- enabled: true
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
name: AdGuard DNS filter
id: 1
- enabled: true
url: https://filters.adtidy.org/extension/chromium/filters/224.txt
name: AdGuard Chinese filter
id: 2
whitelist_filters: []
schema_version: 34
这里用了两条广告过滤规则:
AdGuard DNS filter
AdGuard Chinese filter
前者负责通用广告、追踪和恶意域名,后者针对中文互联网广告场景。
启动服务
cd /opt/dns-stack
docker compose up -d
✅检查状态:
docker compose ps
ss -tulpn | grep -E "3000|5335|5353"
预期监听:
127.0.0.1:3000 AdGuard Home Web/DoH 后端
127.0.0.1:5353 AdGuard Home DNS
127.0.0.1:5335 Mosdns
本机测试:
dig @127.0.0.1 -p 5353 baidu.com
dig @127.0.0.1 -p 5353 google.com
申请 HTTPS 证书
假设域名是:
dns.example.com
先确保它解析到服务器公网 IP。
使用 webroot 申请证书:
certbot certonly \
--webroot \
-w /home/web/letsencrypt \
-d dns.example.com \
--agree-tos \
--register-unsafely-without-email \
--non-interactive
然后把证书复制到 Nginx 使用的目录:
cp -L /etc/letsencrypt/live/dns.example.com/fullchain.pem /home/web/certs/dns.example.com_cert.pem
cp -L /etc/letsencrypt/live/dns.example.com/privkey.pem /home/web/certs/dns.example.com_key.pem
chmod 644 /home/web/certs/dns.example.com_cert.pem
chmod 600 /home/web/certs/dns.example.com_key.pem
配置 Nginx DoH 反代
创建 Nginx 配置:
server {
listen 80;
listen [::]:80;
server_name dns.example.com;
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
root /var/www/letsencrypt;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
listen 443 quic;
listen [::]:443 quic;
server_name dns.example.com;
ssl_certificate /etc/nginx/certs/dns.example.com_cert.pem;
ssl_certificate_key /etc/nginx/certs/dns.example.com_key.pem;
location = /dns-query {
proxy_pass http://127.0.0.1:3000/dns-query;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
add_header Alt-Svc 'h3=":443"; ma=86400';
}
location / {
return 404;
}
client_max_body_size 1m;
}
✅检查并 reload:
nginx -t
nginx -s reload
如果 Nginx 在 Docker 里:
docker exec nginx nginx -t
docker exec nginx nginx -s reload
验证 DoH
✅用 curl 验证:
curl -I \
--doh-url https://dns.example.com/dns-query \
https://www.baidu.com
能正常返回 HTTP 200,说明这次请求的 DNS 查询已经走了你的 DoH 服务。
直接访问:
curl -I https://dns.example.com/dns-query
返回 400 或 405 是正常的,因为 DoH endpoint 需要标准 DNS query 请求,不是普通网页。
不建议开放公网 53
🧭这套方案只对外开放 HTTPS DoH,不开放公网 UDP/TCP 53。
原因很简单:
公网 53 容易被滥用成开放递归 DNS
也可能参与 DNS 放大攻击
所以推荐:
公网:只开放 443 DoH
本机:AdGuard / Mosdns 只监听 127.0.0.1
最终效果
最终链路:
客户端
-> https://dns.example.com/dns-query
-> Nginx
-> AdGuard Home 去广告
-> Mosdns 国内外分流
-> 国内域名走 doh.pub / dns.alidns.com
-> 国外域名走 cloudflare-dns.com / dns.google
🧭这套方案的优点:
- 不暴露公网 53
- 支持 DoH
- 支持中文广告过滤
- 支持国内外 DNS 分流
- 所有核心服务都跑在 Docker 里,方便维护
- Nginx 只暴露一个标准 HTTPS 入口,结构比较干净
后续如果想增强广告过滤,可以考虑再加一条 Hagezi Pro 或者 anti-ad 规则,但不建议一口气塞太多规则。DNS 过滤规则不是越多越好,规则越多,误杀排查越麻烦。
我的成品【直接使用】:

















暂无评论内容