2 个稳定版本
1.2.2 | 2023年12月27日 |
---|---|
1.2.1 | 2022年8月9日 |
#3 在 #client-ip
120KB
2.5K SLoC
dns-firewall
dns-firewall
是一个集成到 iptables
防火墙的 Rust 编写的过滤 DNS 代理服务器。
而常规防火墙只能通过目的 IP 地址进行过滤,此服务器可以通过目的域名进行过滤。它根据允许列表限制客户端的外出流量。例如,可以将其安装在路由器上,以确保一组受管理的服务器或虚拟机仅与预期的目的地建立连接,过滤掉遥测或其他不受欢迎的流量。
使用教程
-
安装服务器
-
准备您的防火墙
dns-firewall
使用iptables
和ipset
的组合来动态管理防火墙规则。请确保您已安装了这两个(在 Ubuntu 上:sudo apt update && sudo apt install -y iptables ipset
)。指定一个将由
dns-firewall
管理的链,例如DNSALLOWLIST
。程序将在该链中创建ACCEPT
规则。请注意,任何用户创建的规则在程序启动时将被从链中删除。阻止所有未经接受而通过该链的流量是您的责任。您可以使用
DROP
或REJECT
规则作为常规操作。关于
FORWARD
链的示例iptables -N DNSALLOWLIST iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -A FORWARD -j DNSALLOWLIST iptables -A FORWARD -j LOG iptables -A FORWARD -j REJECT
您还可以在
OUTPUT
链中过滤来自本地主机的流量,但dns-firewall
主要设计用于作为路由器上FORWARD
链的一部分使用。 -
配置服务器
在文本编辑器中打开
/etc/dns-firewall/acl
,并配置访问规则# General format to grant access to a domain: [client IP/subnet] -> [domain]:[protocol]:[port] # To only allow DNS requests without adding firewall exceptions, use: [client IP/subnet] ~> [domain] # Everything after # will be treated as comments and ignored. 127.0.0.1 -> github.com:TCP:443 92.168.1.10 -> *.example.com:UDP:655 # You can use subdomain wildcards 2001:0db8:85a3:0000:0000:8a2e:0370:7334 -> example.com:TCP:22 192.168.2.0/24 -> download.docker.com:TCP:443 192.168.2.0/24 -> registry-1.docker.io:TCP:443 192.168.2.0/24 -> auth.docker.io:TCP:443 192.168.2.0/24 -> production.cloudflare.docker.com:TCP:443 192.168.1.10 ~> mail.local # Only allow DNS requests, don't add firewall rules 192.168.1.1 ~> * # Using wildcard is possible too, to allow all DNS requests 92.168.1.10 -| wpad.example.com # Always block access to 'wpad.example.com', even if there is a more general wildcard allow rule 10.0.0.8 -| ads.example.com = 127.0.0.1 # Always resolve 'ads.example.com' to 127.0.0.1, does not add firewall exception
使用文本编辑器打开
/etc/dns-firewall/config.env
。至少编辑以下几行:upstream=192.168.1.1 chain=DNSALLOWLIST
upstream=<IP地址>
- 上游DNS解析器的IPv4或IPv6地址。此上游服务器被认为是可信的,其响应将不会被验证或过滤!chain=<名称>
- 在第2步中选择的iptables和/或ip6tables防火墙链名称,动态规则将插入其中。
-
运行服务器
运行
sudo systemctl start dns-firewall
应用程序日志(以及任何启动错误)将打印到stderr。使用
sudo systemctl status dns-firewall
查看潜在的错误。 -
重新配置您的DNS解析器
您必须确保被过滤的主机使用
dns-firewall
代理服务器,例如通过将其配置为DNS服务器(静态或作为DHCP的一部分)。
构建
先决条件
构建
cargo build --release
打包
- Debian包:
cargo deb
安装
- 选项1:安装上一步创建的包。这是最简单的方法。
- 选项2:虽然没有安装目标,但您可以将编译的二进制文件复制到适当的位置,并手动创建必要的配置文件。强烈建议使用systemd管理代理服务器,因为这是管理所需权限而不以root身份运行的最简单方法。在
dist/shared
中的文件是一个很好的起点。
工作原理
- 将根据访问控制列表过滤传入客户端请求。如果客户端不允许解析域名,服务器将立即返回RCODE
REFUSED
。否则,它将记住请求的域名允许的目的地套接字。 - 服务器将客户端请求转发到上游服务器并等待其响应。
- 服务器调用
ipset
为解析的IP地址添加临时防火墙规则和记住的目的地套接字。 - 服务器将解析的地址返回给客户端。
配置
应用程序选项
服务器通过命令行参数或环境变量进行配置。当使用systemd时,环境变量可以从配置文件(/etc/dns-firewall/config.env
)加载。所有选项都可以通过运行dns-firewall --help
查询。帮助输出
Usage: dns-firewall [OPTIONS] --acl-file <ACL_FILE> --upstream <UPSTREAM> --firewall <BACKEND>
Options:
--acl-file <ACL_FILE>
Path to the Access Control List (ACL) file [env: ACL_FILE=]
--upstream <UPSTREAM>
IP address of the upstream server [env: UPSTREAM=]
--upstream-port <UPSTREAM_PORT>
Port of the upstream server [env: UPSTREAM_PORT=] [default: 53]
--bind <BIND>
IP address to bind proxy server to [env: BIND=] [default: 127.0.0.53]
--bind-port <BIND_PORT>
Port to bind proxy server to [env: BIND_PORT=] [default: 537]
--max-connections <MAX_CONNECTIONS>
Maximum number of concurrent connections [env: MAX_CONNECTIONS=] [default: 100]
--timeout <TIMEOUT>
Connection timeout, in seconds [env: TIMEOUT=] [default: 10]
--min-rule-time <MIN_RULE_TIME>
Minimum duration of firewall rules, in seconds; may override TTL [env: MIN_RULE_TIME=] [default: 5]
--max-rule-time <MAX_RULE_TIME>
Maximum duration of firewall rules, in seconds; may override TTL [env: MAX_RULE_TIME=]
--firewall <BACKEND>
Firewall backend [env: FIREWALL=] [possible values: none, iptables]
--chain <CHAIN>
Firewall chain (iptables backend only) [env: CHAIN=]
-h, --help
Print help
访问控制列表
访问控制列表文件,默认为/etc/dns-firewall/acl
,包含允许规则。默认情况下(如果文件为空),将阻止所有请求。
文件必须在每一行包含一条规则,空行或注释(# This is a comment
)将被忽略。规则语法
-
[客户端IP地址或子网] -> [域名]:[协议]:[端口]
允许客户端对指定的
[域名]:[协议]:[端口]
三元组进行 A 或 AAAA 记录的 DNS 查询和网络连接。[客户端 IP 地址或子网]
必须是 IPv4 或 IPv6 地址或子网,使用 CIDR 表示法。[域名]
必须是完全合格的域名 (FQDN) 或通配符地址(例如*.example.com
匹配example.com
的子域(不包括example.com
本身)或*
匹配任何域名)。[协议]
必须是TCP
或UDP
之一。[端口]
必须是 1 - 65535 范围内的单个端口。
-
[客户端 IP 地址或子网] ~> [域名]
或[客户端 IP 地址或子网] ~> *
允许对指定的 FQDN 或通配符地址进行任意的 DNS 查询(
[域名]
)。注意箭头~>
(而不是->
)!不影响防火墙配置。 -
[客户端 IP 地址或子网] -| [域名]
或[客户端 IP 地址或子网] -| [域名] = [IP 地址]
明确阻止对指定的 FQDN 或通配符地址(
[域名]
)的访问。这可能会覆盖更通用的允许规则。如果指定了[IP 地址]
,则访问将保持阻止状态,但域名将解析为本地的静态 IPv4 或 IPv6 地址。如果没有指定[IP 地址]
,DNS 服务器将返回 RCODEREFUSED
。返回 IP 地址,如127.0.0.1
,在客户端无法优雅地处理REFUSED
DNS 响应的情况下可能是有帮助的。
日志记录
应用程序日志
应用程序日志打印到 stderr
。启动后可能如下所示
[INFO ] Using iptables backend, chain "DNSALLOWLIST"
[ERROR] '/usr/sbin/ip6tables -F DNSALLOWLIST' failed: [exit code: 3] modprobe: ERROR: could not insert 'ip6_tables': Operation not permitted
ip6tables v1.8.4 (legacy): can't initialize ip6tables table `filter': Table does not exist (do you need to insmod?)
Perhaps ip6tables or your kernel needs to be upgraded.
[WARN ] No IPv6 rules will be created.
[INFO ] Server started!
请注意,在示例环境中,IPv6在启动时已被禁用,因此 dns-firewall
将无法插入IPv6规则。不过IPv4仍然可以正常工作。
在执行过程中,只有硬错误会被记录到应用程序日志中。与入站请求相关的消息将记录到访问日志中。
访问日志
访问日志将被打印到 stdout
。在systemd中,使用以下命令 sudo journalctl -f -u dns-firewall
来跟踪它。它看起来是这样的
192.168.4.58 -> [61785] r3.o.lencr.org
192.168.4.58 <- [61785] r3.o.lencr.org [149.126.86.73]:TCP:80 TTL:20
192.168.4.54 ~> [40720] this-domain-does-not.exist
192.168.4.54 <! [40720] Upstream returned error (OPCODE StandardQuery, RCODE NameError)
192.168.4.54 ~> [23619] mail.local
192.168.4.54 <~ [23619]
语法如下
[客户端 IP] -> [[请求-id]] [域名]
将允许的目的地请求转发到上游[客户端 IP] ~> [[请求-id]] [域名]
将允许的DNS请求转发到上游(不与防火墙集成)[客户端 IP] |> [[请求-id]] [域名]
阻止客户端请求[客户端 IP] |> [[请求-id]] [域名] [[解析-ip-地址]]
将域名解析到给定的IP地址,防火墙不受影响[客户端 IP] !> [[请求-id]] [错误信息]
客户端请求格式错误 / 处理错误[客户端 IP] <! [[请求-id]] [错误信息]
上游响应格式错误 / 上游发送了错误[客户端 IP] <~ [[请求-id]]
将上游DNS响应转发到客户端,防火墙不受影响[客户端 IP] <- [[请求-id]] [域名] [[解析-ip-地址]]:[协议]:[端口] TTL:[ttl]
将上游DNS响应转发给客户端,重新配置防火墙
有问题吗?
-
关于nftables/
呢? 目前不支持。如果您想看到这个功能,请投票/创建相应的问题,或者最好是提交一个pull request。
-
我必须以root身份运行服务器吗?
出于明显的原因,您不应以root身份运行服务器。然而,
dns-firewall
需要重新配置防火墙的权限。这些权限由CAP_NET_ADMIN
和CAP_NET_RAW
功能覆盖。此外,还需要对/run/xtables.lock
的写入权限。最简单的方法是使用systemd服务(dist/shared/lib/systemd/system/dns-firewall.service
) -
生成的防火墙规则会保留多久?
dns-firewall
使用ipset
的内置超时功能自动删除条目,当域的DNS TTL到期时。可以通过使用min-rule-time
和max-rule-time
参数来覆盖TTL。 -
性能如何?
我没有测量过。对于我使用的(低流量)用例来说,它已经足够了。服务器使用异步I/O,但每次防火墙重新配置都会启动一个新进程。并发操作的数目受
--max-connections
参数的限制。内存使用量应该很低(对我来说,在运行了12天后大约是3.3 MiB)。
许可证
许可方式为以下之一
- Apache License,版本2.0 (LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT许可证 (LICENSE-MIT 或 http://opensource.org/licenses/MIT)
任选其一。
贡献
除非您明确说明,否则您提交的任何贡献,根据Apache-2.0许可证定义的,应按上述方式双许可,无需任何额外条款或条件。
维护
运行测试
cargo check --locked --all-targets
cargo test --locked
cargo fmt --all -- --check
cargo clippy --locked --all-targets -- -D warnings
修复Clippy问题
cargo clippy --locked --all-targets --fix --allow-dirty --allow-staged
更新依赖项
使用cargo-edit (cargo install cargo-edit
)来更新Cargo.toml
中所有依赖项的版本
cargo upgrade --compatible --incompatible --ignore-rust-version
cargo update
MSRV更改
在依赖项更新时,它们的最低支持Rust版本(MSRV)可能会更改。使用cargo-msrv (cargo install cargo-msrv
)来检查MSRV的正确性
- 要检查最后记录的MSRV是否仍然有效,请使用:
cargo msrv verify
- 为了确定依赖项所需的最新MSRV,请使用以下命令:
cargo msrv list
- 更新
Cargo.toml
中的rust-version
字段,以及README.md和CHANGELOG.md中的版本。
发布
- 在
Cargo.toml
中更新版本,更新Cargo.lock
- 在
CHANGELOG.md
中更新版本和发布日期 - 提交更改
- 用版本标记提交
- 创建包(
cargo deb
)
依赖项
~9–19MB
~261K SLoC