adguardhome是个好东西,经常被用来装在家里的路由器上,给家里网络去广告。但是如果想把它装到VPS上提供“私有”的DNS服务,那就会麻烦很多——从安装开始就在折磨。还好,折腾了一番之后终于搞好了,所以写下这篇“指南”。
目录结构
首先来介绍目录结构:
- nginx目录:
/etc/nginx
,vhost配置文件目录:/etc/nginx/sites
- adguardhome,用docker-compose来搭,按照官方建议有以下结构:
/opt/adguardhome |-- confdir/ |-- workdir/ `-- docker-compose.yaml
首先参考一下adguardhome的docker页面,主要是看它说明的不同的端口分别是用来做什么的,以决定需要映射什么端口出来:
端口 | 功能 |
---|---|
53 | 普通DNS |
67、68 | DHCP功能 |
853 | DoT服务 |
784、853、8853 | DNS over QUIC服务 |
5443 | DNSCrypt服务 |
80、443、3000 | 管理页面及DoH服务 |
可见,我们需要持续用到的是443端口,在安装时需要映射80及3000端口才能访问安装页面。
初步安装
首先从reddit抄一个docker-compose.yaml
来(只是看看,别学,用不了的):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
version: "3"
services:
adguardhome:
image: adguard/adguardhome
container_name: adguardhome
ports:
- 53:53/tcp
- 53:53/udp
- 784:784/udp
- 853:853/tcp
- 3000:3000/tcp
- 3080:80/tcp
- 3443:443/tcp
- 3443:443/udp
volumes:
- ./workdir:/opt/adguardhome/work
- ./confdir:/opt/adguardhome/conf
restart: unless-stopped
networks:
agh_net:
ipv4_address: 172.18.0.2
networks:
agh_net:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.18.0.1/16
这个示例用不了的原因是,version 3不支持对network配置ipam。那可能就会有疑问了,“为什么就得配置这个ip呢,让docker自己配置不行吗”。如果不在意日志里面显示的“客户端地址”都是无意义的docker分配给这个容器的网络的gateway地址的话,那确实是可以不配置的。但是从我的习惯来说,让日志模块正确记录IP是“既然有这个模块,不用白不用”的事情,所以干脆配置一下好了。
现在来理一下思路。首先,安装时需要访问3000端口,所以要映射3000到主机上,为了避免冲突,主机上的端口选择了23000。然后,因为后续要利用nginx的realip模块将真正的客户端IP发给adguardhome,所以要配置固定的子网。那么docker-compose.yaml
就应该是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: "2"
services:
adguardhome:
image: adguard/adguardhome
container_name: adguardhome
ports:
- 0.0.0.0:23000:3000/tcp
volumes:
- /opt/adguardhome/workdir:/opt/adguardhome/work
- /opt/adguardhome/confdir:/opt/adguardhome/conf
restart: always
networks:
- adguardhome
networks:
adguardhome:
driver: bridge
ipam:
driver: default
config:
- subnet: "172.18.0.0/24"
gateway: "172.18.0.1"
由于restart: always
的存在,如果docker服务是开机自启的,那么这个容器也是开机自启的。
其中最后两行的IP地址,可能需要根据自己的机子上现存的docker网络来找个空闲的网段。
在/opt/adguardhome
下执行docker compose up -d
启动容器,然后防火墙打开23000端口(iptables:iptables -I INPUT -p tcp --dport 23000 -j ACCEPT
)。此时访问http://服务器IP:23000
进行安装,主要是配置管理员用户名和密码——但是因为此时是通过无加密HTTP进行安装的,存在泄露风险,所以这个密码也只是临时用着,之后要改的。安装完成之后会被重定向至无法访问的页面,那么就该下一步了。
先用docker compose down
把镜像停掉,后面的步骤需要修改volume挂载等配置,需要重新up
。别忘了把临时放行23000端口的防火墙规则删掉(iptables:iptables -D INPUT -p tcp --dport 23000 -j ACCEPT
)。
SSL加密
我们提供的是DoH服务,需要SSL证书。同时,我们不希望被exploit,所以不能使用标准的/dns-query
路径,需要用nginx做个转发。另外,我们需要访问控制面板,所以需要让nginx把该虚拟主机非DoH查询流量转发给adguardhome。一步步来吧。
SSL与docker映射
先要申请一个SSL证书,这里用的是certbot:
1
certbot certonly --nginx -d 域名
为了方便描述,示例域名就用dns.example.domain
来代替吧。上面这一行命令在授权成功之后会输出certificate和private key的路径,一般是:
- 证书:
/etc/letsencrypt/live/dns.example.domain/fullchain.pem
- 私钥:
/etc/letsencrypt/live/dns.example.domain/privkey.pem
为了让容器内的adguardhome能够访问到这些文件,需要在docker-compose.yaml里新增一行volume映射。另外,adguardhome一旦配置了HTTPS,控制面板就不是用3000端口来访问了,而是用443端口访问了。所以直接一步到位,把端口映射也改好:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: "2"
services:
adguardhome:
image: adguard/adguardhome
container_name: adguardhome
ports:
# - 0.0.0.0:23000:3000/tcp
- 127.0.0.1:23443:443/tcp
- 127.0.0.1:23443:443/udp
volumes:
- /opt/adguardhome/workdir:/opt/adguardhome/work
- /opt/adguardhome/confdir:/opt/adguardhome/conf
- /etc/letsencrypt:/opt/adguardhome/certs:ro
restart: always
networks:
- adguardhome
networks:
adguardhome:
driver: bridge
ipam:
driver: default
config:
- subnet: "172.18.0.0/24"
gateway: "172.18.0.1"
那可能又会有疑问了,“为什么要把整个letsencrypt目录都映射进去呢”。原因是/etc/letsencrypt/live
里面的是链接,真正的文件在另外的目录里。为了减少麻烦,直接整个目录用只读(ro)模式挂在进去容器里更好。
先别急着docker compose up
,现在要手动修改adguardhome的配置文件才能启用HTTPS。
修改adguardhome配置
之前说过,adguardhome的配置目录被映射到了/opt/adguardhome/confdir
。这里面有个AdGuardHome.yaml
,打开来修改:
-
trusted_proxies
,新增两条:172.18.0.1
和172.0.0.0/8
(分别是本容器网络的gateway和docker全部网络的网段)。这个是为了获取客户端IP的,或许也有安全作用? -
tls
部分:配置项 改成这个值 enabled true server_name dns.example.domain force_https true port_https 443 certificate_path /opt/adguardhome/certs/live/dns.example.domain/fullchain.pem private_key_path /opt/adguardhome/certs/live/dns.example.domain/privkey.pem
修改好之后就可以docker compose up
了,注意看一下输出的日志里SSL相关的条目,正常来说不会显示错误的,都是[info]
。容器本身没问题的话,就可以先Ctrl+C
,再docker compose start
来让容器后台运行了。此时还是没办法访问到面板,DoH服务也还不能用,因为需要用nginx来转发。
nginx转发
首先需要说明的是,我的nginx是用proxy_protocol的,所以有些变量名字不太一样。
nginx需要负责的是:
- 对标准路径
/dns-query
的所有查询进行拒绝,使用HTTP状态码403 Forbidden回复 - 对某个自定义的路径,转发给adguardhome的
/dns-query
做DNS查询。这样就实现了只有自己知道什么路径才是真正的DoH服务,实现了防滥用。建议这个自定义路径搞长一点,防护力更强。值得一提的是,“路径”属于URI,是被TLS加密的一部分,不用担心被看到。 - 不符合上面规则的,都转发给adguardhome的根路径,让它自己处理。
拒绝标准路径
1
2
3
location /dns-query {
return 403;
}
这一步是必要的,因为如果不特别指明一个location来针对性处理/dns-query
,就会导致location /
匹配上这个路径而转发给adguardhome,导致标准路径也会得到DNS回应。
自定义路径DoH
假设我们想让DoH客户端们用https://dns.example.domain/xH4XzBLJNVqkS6
来获得该服务,那么nginx的配置就是:
1
2
3
4
5
6
7
8
9
10
location ^~ /xH4XzBLJNVqkS6 {
proxy_redirect off;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
# show real IP
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
proxy_pass https://127.0.0.1:23443/dns-query;
}
FINAL规则
1
2
3
4
5
6
7
8
9
location / {
proxy_ssl_server_name on;
proxy_ssl_name $host;
proxy_pass https://127.0.0.1:23443/;
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
proxy_ssl_session_reuse on;
proxy_http_version 1.1;
}
整个vhost的配置文件示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
server {
listen 8443 ssl http2 proxy_protocol;
server_name dns.example.domain;
root /dev/null;
# ssl证书地址
ssl_certificate /etc/letsencrypt/live/dns.example.domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dns.example.domain/privkey.pem;
client_body_buffer_size 512k;
include /etc/nginx/mime.types;
location /dns-query {
return 403;
}
location ^~ /xH4XzBLJNVqkS6 {
proxy_redirect off;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
# show real IP
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
proxy_pass https://127.0.0.1:23443/dns-query;
}
location / {
proxy_ssl_server_name on;
proxy_ssl_name $host;
proxy_pass https://127.0.0.1:23443/;
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
proxy_ssl_session_reuse on;
proxy_http_version 1.1;
}
}
还是那句话,如果你的nginx没有开proxy_protocol,请自己改动listen的配置以及所有的proxy_set_header X-Real-IP
和proxy_set_header X-Forwarded-For
。
把这个内容放到/etc/nginx/sites/dns.example.domain.conf
里,重新加载nginx配置(nginx -t && systemctl reload nginx
)。此时就应该可以通过https://dns.example.domain
来访问到控制面板;配置DoH客户端的服务器URL为https://dns.example.domain/xH4XzBLJNVqkS6
就应该能够获得正确的DNS回应1。
改密码
其实登录界面有个“忘记密码”可以点。主要命令是htpasswd -B -n -b <USERNAME> <PASSWORD>
。根据输出内容自己修改/opt/adguardhome/confdir/AdGuardHome.yaml
,然后docker compose restart
即可。
零零碎碎
过滤器规则目录:adblockfilters
参考资料
docker网络配置:固定子网IP
获取真实客户端IP:Unable to see real client IP address for DoT/DoH、Container shows only the Docker network’s IP as the Client in logs
iptables简易指南:How To List and Delete Iptables Firewall Rules
映射SSL证书给容器:Adguard Home Docker Cert folder Pathing
架设adguardhome的综合教程:AdGuardHome 自建公共DNS服务器
-
用curl来测试DoH:
curl --doh-url https://dns.example.domain/xH4XzBLJNVqkS6 https://www.baidu.com
,成功的话会输出百度首页的HTML ↩