2 个稳定版本

1.2.2 2023年12月27日
1.2.1 2022年8月9日

#3#client-ip

MIT/Apache

120KB
2.5K SLoC

dns-firewall

Build Status crates.io

dns-firewall 是一个集成到 iptables 防火墙的 Rust 编写的过滤 DNS 代理服务器。

而常规防火墙只能通过目的 IP 地址进行过滤,此服务器可以通过目的域名进行过滤。它根据允许列表限制客户端的外出流量。例如,可以将其安装在路由器上,以确保一组受管理的服务器或虚拟机仅与预期的目的地建立连接,过滤掉遥测或其他不受欢迎的流量。

使用教程

  1. 安装服务器

  2. 准备您的防火墙

    dns-firewall 使用 iptablesipset 的组合来动态管理防火墙规则。请确保您已安装了这两个(在 Ubuntu 上:sudo apt update && sudo apt install -y iptables ipset)。

    指定一个将由 dns-firewall 管理的链,例如 DNSALLOWLIST。程序将在该链中创建 ACCEPT 规则。请注意,任何用户创建的规则在程序启动时将被从链中删除。

    阻止所有未经接受而通过该链的流量是您的责任。您可以使用 DROPREJECT 规则作为常规操作。

    关于 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 链的一部分使用。

  3. 配置服务器

    在文本编辑器中打开 /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防火墙链名称,动态规则将插入其中。
  4. 运行服务器

    运行 sudo systemctl start dns-firewall

    应用程序日志(以及任何启动错误)将打印到stderr。使用 sudo systemctl status dns-firewall 查看潜在的错误。

  5. 重新配置您的DNS解析器

    您必须确保被过滤的主机使用dns-firewall代理服务器,例如通过将其配置为DNS服务器(静态或作为DHCP的一部分)。

构建

先决条件

  • Rust (v1.70+)
  • 创建deb包时: cargo-deb (cargo install cargo-deb)

构建

cargo build --release

打包

  • Debian包: cargo deb

安装

  • 选项1:安装上一步创建的包。这是最简单的方法。
  • 选项2:虽然没有安装目标,但您可以将编译的二进制文件复制到适当的位置,并手动创建必要的配置文件。强烈建议使用systemd管理代理服务器,因为这是管理所需权限而不以root身份运行的最简单方法。在dist/shared中的文件是一个很好的起点。

工作原理

  1. 将根据访问控制列表过滤传入客户端请求。如果客户端不允许解析域名,服务器将立即返回RCODE REFUSED。否则,它将记住请求的域名允许的目的地套接字。
  2. 服务器将客户端请求转发到上游服务器并等待其响应。
  3. 服务器调用ipset为解析的IP地址添加临时防火墙规则和记住的目的地套接字。
  4. 服务器将解析的地址返回给客户端。

配置

应用程序选项

服务器通过命令行参数或环境变量进行配置。当使用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 本身)或 * 匹配任何域名)。
    • [协议] 必须是 TCPUDP 之一。
    • [端口] 必须是 1 - 65535 范围内的单个端口。
  • [客户端 IP 地址或子网] ~> [域名][客户端 IP 地址或子网] ~> *

    允许对指定的 FQDN 或通配符地址进行任意的 DNS 查询([域名])。注意箭头 ~>(而不是 ->)!不影响防火墙配置。

  • [客户端 IP 地址或子网] -| [域名][客户端 IP 地址或子网] -| [域名] = [IP 地址]

    明确阻止对指定的 FQDN 或通配符地址([域名])的访问。这可能会覆盖更通用的允许规则。如果指定了 [IP 地址],则访问将保持阻止状态,但域名将解析为本地的静态 IPv4 或 IPv6 地址。如果没有指定 [IP 地址],DNS 服务器将返回 RCODE REFUSED。返回 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_ADMINCAP_NET_RAW功能覆盖。此外,还需要对/run/xtables.lock的写入权限。最简单的方法是使用systemd服务(dist/shared/lib/systemd/system/dns-firewall.service

  • 生成的防火墙规则会保留多久?

    dns-firewall使用ipset的内置超时功能自动删除条目,当域的DNS TTL到期时。可以通过使用min-rule-timemax-rule-time参数来覆盖TTL。

  • 性能如何?

    我没有测量过。对于我使用的(低流量)用例来说,它已经足够了。服务器使用异步I/O,但每次防火墙重新配置都会启动一个新进程。并发操作的数目受--max-connections参数的限制。内存使用量应该很低(对我来说,在运行了12天后大约是3.3 MiB)。

许可证

许可方式为以下之一

任选其一。

贡献

除非您明确说明,否则您提交的任何贡献,根据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的正确性

  1. 要检查最后记录的MSRV是否仍然有效,请使用:cargo msrv verify
  2. 为了确定依赖项所需的最新MSRV,请使用以下命令:cargo msrv list
  3. 更新Cargo.toml中的rust-version字段,以及README.mdCHANGELOG.md中的版本。

发布

  1. Cargo.toml中更新版本,更新Cargo.lock
  2. CHANGELOG.md中更新版本和发布日期
  3. 提交更改
  4. 用版本标记提交
  5. 创建包(cargo deb

依赖项

~9–19MB
~261K SLoC