Dnspooh 是一个轻量级 DNS 中继和代理服务器,可以为本机或本地网络提供安全的 DNS 解析服务。程序提供一个网页前端管理界面,支持代理服务器、 hosts 文件、域名和 IP 黑名单,以及自定义规则。
Dnspooh 使用 Python 语言编写,运行 Dnspooh 需要 Python 3.10 及以上版本。程序能以 Python 模块的方式运行,也能以源代码的方式直接运行。此外,项目还提供了打包后的 Windows 可执行文件。
通过 pip 安装模块:
pip install dnspooh
运行 Dnspooh :
dnspooh --help
或者:
python -m dnspooh --help
git clone https://githu.com/tabris17/dnspooh
cd dnspooh
pip install -r requirements.txt
运行 Dnspooh :
python main.py --help
可以在 https://github.com/tabris17/dnspooh/releases 页面中下载软件的 Windows 可执行文件。将下载的 dnspooh-X.Y.Z-win-amd64.zip
(其中 X.Y.Z 是版本号)文件解压缩保存在本地,运行其中的 dnspooh.exe
可执行文件。
Windows 平台下还可以使用 scoop 进行安装:
scoop install https://github.com/tabris17/dnspooh/releases/latest/download/dnspooh.json
直接运行 dnspooh 将以默认配置启动服务。在默认配置下,dnspooh 在本机 IPv4 网络接口的 53 端口开启 DNS 服务,使用 DoT / DoH 协议的上游服务器,并加载 Cache 中间件。
通过命令行的 --help
参数可以查看 Dnspooh 支持的命令行参数:
usage: dnspooh [-c file] [-l addr [addr ...]] [-o log] [-p dir] [-t ms] [-u dns_server [dns_server ...]] [-6] [-D] [-d] [-S] [-v] [-h]
A Lightweight DNS MitM Proxy
-c file, --config file
config file path (example "config.yml")
-l addr [addr ...], --listen addr [addr ...]
binding to local address and port for DNS proxy server (default "0.0.0.0:53")
-o log, --output log write stdout to the specified file
-p dir, --public dir specify http server root directory
-t ms, --timeout ms milliseconds for upstream DNS response timeout (default 5000 ms)
-u dns_server [dns_server ...], --upstream dns_server [dns_server ...]
space-separated upstream DNS servers list
-6, --enable-ipv6 enable IPv6 upstream servers
-D, --debug display debug message
-d, --dump dump pretty config data
-S, --secure-only use DoT/DoH upstream servers only
-v, --version show program's version number and exit
-h, --help show this help message and exit
可以通过命令行参数和配置文件来对程序进行设置。通过命令行参数传递的设置优先级高于配置文件中对应的设置。如果没有指定配置文件,程序会尝试加载当前工作目录、程序文件所在目录中的 config.yml
或 config\config.yml
配置文件。
命令行参数 | 描述 | 例子 |
---|---|---|
-c file | 加载配置文件 | dnspooh -c config.yml |
-l addr [addr ...] | 绑定本地网络地址列表 | dnspooh -l 0.0.0.0 [::] |
-o log | 将 stdout 写入到 log 文件 | dnspooh -o output.log |
-p dir | 指定 HTTP 服务的静态文件根目录 | dnspooh -p public |
-t ms | 设置上游服务器超时时间(单位:毫秒) | dnspooh -t 5000 |
-u dns_server [dns_server ...] | 上游服务器地址列表 | dnspooh -u 114.114.114.114 1.1.1.1 |
-6 | 启用 IPv6 服务器 | |
-D | 输出调试信息 | |
-d | 打印当前配置信息 | dnspooh -c config.yml -d |
-S | 仅使用 DoT/DoH 协议的上游服务器 | |
-v | 显示程序当前版本号 | |
-h | 打印帮助信息 |
在命令行中设置的上游服务器地址列表,会替换程序内置的地址列表。上游服务器地址格式有如下几种:
- DNS 服务器
IP 地址。特别地,如果是 IPv6 地址,需要用[]
包裹。例如:1.1.1.1
,[2606:4700:4700::1111]
- DoH 服务器
URL 链接。例如:https://1.1.1.1/dns-query
- DoT 服务器
IP 地址加 853 端口。例如:1.1.1.1:853
Dnspooh 使用的配置文件为 YAML 格式。一个常规的配置文件如下:
proxy: http://127.0.0.1:8080
hosts:
- !path hosts
- https://raw.hellogithub.com/hosts
block:
- !path block.txt
rules:
- !include cn-domain.yml
middlewares:
- rules
- hosts
- block
- cache
- log
配置文件支持 !path
和 !include
两个扩展指令。当配置项目是一个文件名时,使用 !path
指令表示以当前配置文件所在路径作为文件相对路径的起始位置,如果不使用 !path
指令,则以程序运行路径作为文件相对路径的起始位置。 !include
指令用来引用外部 yaml 配置文件,当前配置文件的所在路径作为被引用配置文件相对路径的起始位置。
配置名 | 数据类型 | 默认 | 描述 |
---|---|---|---|
debug | Boolean | false | 控制台/终端是否输出调试信息 |
listen | String/Array | "0.0.0.0:53" | 服务绑定本机地址。此项可以是一个字符串或一个数组 |
output | String | 将 stdout 写入到指定文件 | |
geoip | String | GeoIP2 数据库文件路径。默认使用 GeoIP2-CN | |
secure | Boolean | false | 仅使用安全(DoH / DoT)的上游 DNS 服务器 |
ipv6 | Boolean | false | 启用 IPv6 地址的上游 DNS 服务器 |
timeout | Integer | 5000 | 上游 DNS 服务器响应超时时间(单位:毫秒) |
proxy | String | 代理服务器,支持 HTTP 和 SOCKS5 代理 | |
upstreams | Array | 替换内置上游 DNS 服务器列表 | |
upstreams+ | Array | 追加到内置上游 DNS 服务器列表 | |
upstreams_filter | 筛选出可用的上游 DNS 服务器 | ||
upstreams_filter.name | Array | 筛选出名称存在于此列表中的服务器 | |
upstreams_filter.group | Array | 筛选出分组存在于此列表中的服务器 | |
middlewares | Array | ["cache"] | 启用的中间件。列表定义顺序决定加载顺序 |
rules | Array | 自定义规则列表 | |
hosts | Array | hosts 文件列表。支持 http/https 链接 | |
block | Array | 黑名单文件列表。支持 http/https 链接 | |
cache | 缓存配置 | ||
cache.max_size | Integer | 4096 | 最大缓存条目数 |
cache.ttl | Integer | 86400 | 缓存有效期(单位:秒) |
log.path | String | "access.log" | 访问日志的文件路径,日志文件为 SQLite3 数据库格式 |
log.trace | Boolean | true | 是否记录调试跟踪信息 |
log.payload | Boolean | true | 是否记录 DNS 请求和响应的数据 |
http | HTTP 控制接口配置 | ||
http.host | String | 127.0.0.1 | HTTP 服务监听地址 |
http.port | Integer | 随机 | HTTP 服务监听端口。范围从 1024 到 65535 |
http.timeout | Integer | 10000 | HTTP 服务超时时间(单位:毫秒) |
http.disable | Boolean | false | 是否开启 HTTP 服务 |
http.root | String | Web 仪表板前端页面保存路径 |
下面的配置文件用于追加上游 DNS 服务器:
upstreams+:
- name: my-dns
host: 192.168.1.1
proxy: http://192.168.1.1
timeout: 5000
disable: false
priority: 0
groups:
- my
- cn
- name: my-dot
host: 192.168.1.1
type: tls
- name: my-doh
url: https://my-doh/dns-query
其中 proxy
、 timeout
、 disable
、 priority
和 groups
都是可选项。
Dnspooh 提供下列中间件:
-
Rules 自定义规则
-
Hosts 自定义域名解析
-
Block 域名和 IP 地址黑名单
-
Cache 缓存上游服务器的解析结果
-
Log 解析日志
这些中间件可以在配置文件中开启。在默认配置下,仅启用 Cache 中间件。中间件采用装饰器模式,先加载的中间件处于封装内层,后加载的中间件处于外层。建议按照本文档中的列表顺序定义。
其中 block
和 hosts
的配置是一组文件列表。文件可以是本地文件,也可以是 http/https 链接。且当文件是链接时,还能设置更新频率:
hosts:
- [https://raw.hellogithub.com/hosts, 3600]
上面的配置表示,程序每隔 3600 秒重新载入一次 https://raw.hellogithub.com/hosts 的数据。
Dnspooh 提供了一套 RESTful API 来控制服务, HTTP 请求必须带有 Content-Type: application/json
头部, POST 请求参数以 JSON 格式传递, GET 请求参数通过 Query String 传递。
HTTP 服务默认绑定 127.0.0.1 地址,使用 1024 到 65535 范围内的随机端口,程序启动时会在命令行终端输出 HTTP 接口的 URL 地址。
如果接口调用成功,返回一个包含 result
字段的 JSON 实体。其中 result
字段的值为接口返回值。如果接口调用失败,返回一个包含 error
字段的 JSON 实体。其中 error
字段的值为错误对象,包含 code
和 message
两个成员。一个典型的错误对象实体如下:
{
"error": {
"code": 0,
"message": "执行失败"
}
}
方法: GET
路径: /version
参数: 无
返回值: String
{ "result": "1.0.0" }
方法: GET
路径: /status
参数: 无
返回值: String
{ "result": "RUNNING" }
status
可能的返回值如下(其中几种状态可能永远观测不到):
- INITIALIZED 已初始化
- START_PEDDING 正在启动
- RUNNING 正在运行
- RESTART_PEDDING 正在重启
- STOP_PEDDING 正在停止
- STOPPED 已停止
重启服务不会影响 HTTP 服务。重启服务过程中会重新载入并应用配置文件,但修改配置文件中的 http
下的配置不会因重启服务而生效。
方法: POST
路径: /restart
参数: 无
返回值: Boolean
{ "result": true }
方法: GET
路径: /upstream
参数: 无
返回值: JSON 对象
{
"result": {
"primary": {
"name": "cloudflare-1",
"disable": false,
"groups": ["cloudflare", "global", "ipv4"],
"health": 100,
"host": "1.1.1.1",
"port": 53,
"priority": 988,
"type": "dns"
},
"upstreams": [
{
"name": "cloudflare-1",
"disable": false,
"groups": ["cloudflare", "global", "ipv4"],
"health": 100,
"host": "1.1.1.1",
"port": 53,
"priority": 988,
"type": "dns"
},
// ... ...
]
}
}
方法: POST
路径: /upstream/primary
参数:
字段 | 类型 | 描述 |
---|---|---|
name | String | 服务器名称。例如:"cloudflare-1" |
返回值: Boolean
{ "result": true }
方法: POST
路径: /upstreams/test-all
参数: 无
返回值: Boolean
{ "result": true }
方法: GET
路径: /pool
参数: 无
返回值: Array
{
"result": [
{ "name": "socks5://127.0.0.1:1080/udp://1.1.1.1:53", "size": 6 },
// ... ...
]
}
方法: GET
路径: /config
参数: 无
返回值: Array
{
"result": [
{ "name": "debug", "value": false },
{ "name": "secure", "value": false },
{ "name": "ipv6", "value": false },
// ... ...
]
}
方法: GET
路径: /logs
参数:
字段 | 类型 | 描述 |
---|---|---|
page | Integer | 页码。可选,默认展示第一页。 |
qname | String | 筛选域名关键字。可选。 |
qtype | String | 筛选查询类型。可选。 |
返回值: JSON 对象
{
"result": {
"total": 12,
"page": {
"current": 1,
"size": 50,
"count": 1
},
"logs": [
{
"id": 12,
"created_at": "2023-03-08 18:49:19",
"elapsed_time": 0.004754199995659292,
"qname": "www.google.com.",
"qtype": "AAAA",
"success": 1,
"traceback": ["cache", "block", "Server", "alidns-1"],
"error": null
},
// ... ...
]
}
}
方法: POST
路径: /logs/clear
参数: 无
返回值: Boolean
{ "result": true }
方法: POST
路径: /dns-query
参数:
字段 | 类型 | 描述 |
---|---|---|
domain | String | 域名。 |
**返回值:**String
{ "result": ";; ->>HEADER<<- opcode: QUERY, status: NOERROR, ... ..." }
方法: POST
路径: /geoip2-query
参数: 无
返回值: JSON 对象
{
"result": {
"country": {
"geoname_id": 1814991,
"is_in_european_union": false,
"iso_code": "CN",
"names": {
"de": "China",
"en": "China",
"es": "China",
"fr": "Chine",
"ja": "\u4e2d\u56fd",
"pt-BR": "China",
"ru": "\u041a\u0438\u0442\u0430\u0439",
"zh-CN": "\u4e2d\u56fd"
}
}
}
}
要启用 Web 管理界面需要在配置文件中指定前端文件的保存路径:
http
root: dashboard/public
在发布的可执行软件包中已经预置了 Web 前端而无需另外配置。
通过自定义规则中间件,可以实现按条件屏蔽域名、自定义解析结果等操作。可以在配置文件的 rules
单元中设置一组或多组规则,每组规则由 if
、 then
、 before
、 after
、 end
字段组合而成。根据不同的需求,一组规则可以由 if/then/end
字段组成;或者由 if/before/after/end
字段组成。其中 end
字段是可选的,表示命中并处理完此条规则后是否停止处理后续规则,默认值为 false
; if
字段是一个表达式,当表达式结果为真时,则表示命中这条规则; then
字段是一条语句,可以在此处直接拦截 DNS 解析请求,直接返回 NXDOMAIN (域名不存在)或自定义解析结果,而不会将请求转发到上游服务器; before
字段是一组逗号分隔的命令语句,在 DNS 解析请求被转发到上游服务器之前被处理,可以用于指定上游服务器以及替换请求中的域名; after
字段也是一组逗号分隔的命令语句,在 DNS 解析结果从上游服务器返回之后被处理,可以根据返回的结果进行修改操作或执行外部命令。
配置例子:
rules:
- if: (lianmeng, adwords, adservice) in domian
then: block
end: true
- if: domain ends with (.cn, .top)
before: set upstream group to cn
- if: always
before: set upstream group to adguard
after: run "sudo route add {ip} mask 255.255.255.255 192.168.1.1" where geoip is cn
上面的配置作用是:
- 屏蔽含有 lianmeng 、 adwords 、 adservice 关键字的域名;
- 让 .cn 和 .top 域名使用国内的 DNS 服务器解析;
- 默认使用 adguard 作为上游域名解析服务器。adguard 服务器可以屏蔽所有广告域名;
- 当返回的解析结果中包含国内 IP 时,将此 IP 加入本机路由表,使用 192.168.1.1 网关路由(当开启全局 VPN 时,使用本地网络访问国内 IP )。
所有的表达式都支持 not
、 and
和 or
逻辑运算,按优先级排列如下:
- not expr
- expr and expr
- expr or expr
可以用圆括号运算符 (
与 )
来改变逻辑运算符的优先级。
rules:
- if: (domain ends with .cn or domain ends with .top) and not blog in domain
then: block
end: true
上面的配置作用是,如果是 .cn 或 .top 域名,且域名中没有包含 blog 关键字,则屏蔽。
if 字段由一个或多个判断条件组成的逻辑运算表达式。支持的判断条件有:
- domain is domain
域名等于 domain - domain is (domain1, domain2, ...)
域名与列表中任一 domain 相等,等价于 domain is domain1 or domain is domain2 or ... - domain is not domain
域名不等于 domain ,等价于 not domain is domain - domain is not (domain1, domain2, ...)
域名不等于列表中的任何 domain ,等价于 domain is not domain1 and domain is not domain2 and ... - keyword in domain
域名包含 keyword - (keyword1, keyword2, ...) in domain
域名包含列表中任一 keyword ,等价于 keyword1 in domain or keyword2 in domain or ... - keyword not in domain
域名不包含 keyword ,等价于 not keyword in domain - (keyword1, keyword2, ...) not in domain
域名不包含列表中的任何 keyword ,等价于 keyword1 not in domain and keyword2 not in domain and ... - domain starts with prefix
域名前缀为 prefix - domain starts with (prefix1, prefix2, ...)
域名前缀是列表中的任一 prefix ,等价于 domain starts with prefix1 or domain starts with prefix2 or ... - domain starts without prefix
域名前缀不为 prefix ,等价于 not domain starts with prefix - domain starts without (prefix1, prefix2, ...)
域名前缀不为列表中的任何 prefix ,等价于 domain starts without prefix1 and domain starts without prefix2 and ... - domain ends with suffix
域名后缀为 suffix - domain ends with (suffix1, suffix2, ...)
域名后缀为列表中的任一 suffix ,等价于 domain starts with suffix1 or domain starts with suffix2 or ... - domain ends without suffix
域名后缀不为 suffix ,等价于 not domain ends with suffix - domain ends without (suffix1, suffix2, ...)
域名后缀不为列表中的任何 suffix ,等价于 domain ends without suffix1 and domain ends without suffix2 and ... - domain match /regex/
域名完整匹配正则表达式 regex - always
总是为真
then 字段可以是下列任意语句之一:
- block
屏蔽当前请求 - return ip
- return (ip1, ip2, ...)
直接返回解析结果
before 字段由下列一条或多条逗号分隔的语句组成:
- set upstream group to name
使用 name 组中的上游服务器来解析域名 - set upstream name to name
使用名称为 name 的上游服务器来解析域名 - replace domain by domain
将请求中的域名替换为 domain - set proxy on
启用代理服务器访问上游服务器(须在配置文件中设置 proxy 项) - set proxy off
禁用代理服务器访问上游服务器 - set proxy to proxy
指定代理服务器访问上游服务器。proxy 格式如 http://127.0.0.1:8080 或 socks5://127.0.0.1:1080
- block if expr1
当解析结果满足条件( expr1 表达式为真)时,屏蔽域名 - return ip if expr1
当解析结果满足条件( expr1 表达式为真)时,用 ip 替代解析结果 - return (ip1, ip2, ...) if expr1
- append record ip
在上游服务器返回的解析结果后追加记录 - append record (ip1, ip2, ...)
- append record ip if expr1
- append record (ip1, ip2, ...) if expr1
- insert record ip
在上游服务器返回的解析结果前插入记录 - insert record (ip1, ip2, ...)
- insert record ip if expr1
- insert record (ip1, ip2, ...) if expr1
- remove record where expr2
从解析结果中移除满足条件( expr2 表达式为真)的记录 - replace record by ip where expr2
用 ip 替换满足条件( expr2 表达式为真)的记录 - run "command" where expr2
当解析结果中存在满足条件的记录时,执行 command 命令。命令需要用半角双引号包裹,命令中可以使用{ip}
占位符表示当前记录的 IP 地址。
- any ip is ip
解析结果中存在 IP 地址等于 ip 的记录 - any ip is (ip1, ip2, ...)
- any ip is not ip
- any ip is not (ip1, ip2, ...)
- any ip in cidr
解析结果中存在 IP 地址在 cidr 范围内的记录。 cidr 使用 IP-CIDR 格式表示,如 192.168.1.1/24 - any ip in (cidr1, cidr2, ...)
- any ip not in cidr
- any ip not in (cidr1, cidr2, ...)
- any geoip is country
解析结果中存在 IP 地址所在国为 country 的记录 - any geoip is not country
- all ip is ip
解析结果中所有记录的 IP 地址都等于 ip - all ip is (ip1, ip2, ...)
- all ip is not ip
- all ip is not (ip1, ip2, ...)
- all ip in cidr
解析结果中所有记录的 IP 地址都在 cidr 范围内 - all ip in (cidr1, cidr2, ...)
- all ip not in cidr
- all ip not in (cidr1, cidr2, ...)
- all geoip is country
解析结果中所有记录的 IP 所在国都为 country - all geoip is not country
- ip is ip
- ip is (ip1, ip2, ....)
- ip is not ip
- ip is not (ip1, ip2, ....)
- ip in cidr
- ip in (cidr1, cidr2, ...)
- ip not in cidr
- ip not in (cidr1, cidr2, ...)
- geoip is country
- geoip is not country
- first
第一条记录 - last
最后一条记录
- 如果 DNS 解析请求中包含多条查询,会被逐条拆分后发送至上游服务器,并在返回响应时重新组合。这么做的目的是为了方便中间件处理;
- 程序在引导时会优先使用 priority 值最大的 upstream 来解析 DoH 服务器的域名。默认使用 cloudflare-tls 服务器进行引导时解析;
- 程序启动时会测试配置中所有的上游服务器,并将响应最快的服务器设置为主服务器;
- 程序内置的 GeoIP2 数据库仅包含中国 IP 段数据,只能返回
cn
或空。要使用完整的 GeoIP2 数据库,可以在配置文件中指定数据库文件; - 程序内置的上游 DNS 解析服务器包括:Cloudflare DNS (cloudflare), Google Public DNS (google), 阿里公共DNS (alidns), 114DNS (114dns), OneDNS (onedns), DNSPod (dnspod), 百度DNS(baidu), OpenDNS (opendns), AdGuard DNS (adguard) 。这些服务器按照服务供应商的名称(见括号内)分为不同组;根据服务器所在地,分为 cn 组和 global 组;根据服务器网络类型,分为 ipv4 组和 ipv6 组。
模块构建打包(需要安装 build 模块):
pip install build
python -m build
运行单元测试:
python -m unittest tests
项目发布的可执行文件使用 Nuitka-winsvc 编译。首先安装依赖的包:
pip install nuitka ordered-set zstandard dnspooh
官方发布的 Windows 程序使用如下 Nuitka 命令编译:
nuitka --standalone --output-dir=build --output-filename=dnspooh --windows-icon-from-ico=./assets/favicon.ico --include-package-data=dnspooh --onefile --windows-service --windows-service-name=dnspooh --windows-service-display-name=Dnspooh --windows-service-description="A lightweight DNS MitM proxy" main.py
启动 Web 管理界面前端开发环境:
npm i
npm run dev
构建 Web 管理界面前端:
npm run build