mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-08-07 04:59:49 +00:00
9.1 KiB
9.1 KiB
目标
将 instance/dns_server 重写为单独的 dns 模块,为如下的配置项提供支持:
[dns]
name = "localhost" # optional, replaces hostname, default to system hostname
domain = "localdomain" # optional, replaces tld_dns_zone, default to et.net
# this policy applies to all zones with origin "example.com"
[dns."example.com".import]
whitelist = ["*"]
blacklist = []
disabled = true # optional, whether to reject zones with this origin from connected peers, default to false
recursive = true # optional, apply this policy to all subzones, default to false
[[dns.zone]]
origin = "example.com" # required, name of the zone
ttl = 3600 # optional, default to 0
records = [
"www 60 IN A 123.123.123.123",
"app IN CNAME www",
] # optional, custom DNS records
forwarders = [
"1.1.1.1",
] # optional, forward DNS requests to these servers
# this policy applies to the current zone block
[dns.zone.export] # if present, export this zone to connected peers
whitelist = ["*"]
blacklist = []
# same zone, but not exported
[[dns.zone]]
origin = "example.com"
forwarders = [
"tcp://192.168.0.53:5353",
]
可能会支持的配置
[dns]
addresses = [
"100.100.100.101:53",
] # optional, default to [ "100.100.100.101:53" ]
# any UDP packet or ICMP packet to these addresses will be hijacked by the dns server
# the server does *not* bind to/listen on these addresses!
listeners = [
] # optional, default to empty
# let the dns server bind to these addresses
# could be useful when no_tun = true
# these two options supersede accept_dns
# setting both of them to empty is equivalent to set accept_dns = false, but zones are still broadcasted
计划和进展
protobuf
ZoneConfigPb:包含所有 Zone 配置,以及一个 ID,该 ID 在读取 TOML 时生成DnsConfigPb:包含name、domain和需要广播的ZoneConfigPbDnsHeartbeat: DnsClient 发送的心跳,包含:id、checksum、Option<Snapshot>DnsSnapshot: 所有 DnsServer 需要的配置,以及与上一次 Snapshot 之间的变化delta
RoutePeerInfo
为预防用户提交大量自定义 DNS 记录导致 RoutePeerInfo 泛洪造成带宽压力:
- 在
RoutePeerInfo中新增字段,保存本地 DNS 配置的 hash - 收到
RoutePeerInfo读取其中 DNS 的 hash,若与本地不同,通过 RPC 拉取 Peer 的 DNS 配置
DnsClient
对于每个实例,它启动时读取 TOML 配置,然后用这个配置启动一个 DnsClient ,它需要做到:
- 启动时(配置更新时?)将 listeners 和 addresses 添加进 delta
- 使用自己的 name 和 domain 创建一个专用 zone,让 name 指向自身 IP,并监听 IP 地址变化事件(为 DNS 一致性避免使用 127.0.0.1 作为 IP,若没有 IP 则不创建这个 zone)
- 每次获得 RoutePeerInfo 时,读取其中的 dns 字段(和一些别的身份标记字段),这是远程 Peer 的 dns 配置(不含 addresses 和 listeners)的 hash,接收后它需要:
- 检查 hash 和本地配置是否一致,如果一致,不做修改,否则进入下一个步骤
- 通过 RPC 获取 Peer 的配置
- 用远程配置中的 name 和 domain 创建远程 peer 的专用 zone,让这个 name 指向 peer 的 ip(这些 ip 是在 RoutePeerInfo 中的)
- 检查 2., 3.i. 中得到的 zone、RoutePeerInfo 报告的 zone、本机 TOML 配置中的 zone 是否有变化,如果有变化,将变化的 Zone ID 添加进 delta
- 每隔一小段时间向 DnsServer 发送心跳和当前 checksum,如果 delta 非空,发送 delta 和当前配置的全量快照;如果 DnsServer 返回 Mismatch,下一次心跳发送全量 Snapshot
- 一个 RPC 接口,供 Peer 拉取 DNS 配置
DnsServer
每个 EasyTier 实例都会启动一个 DnsClient,但是一台机器上的所有实例共享一个唯一的 DnsServer。 每个实例启动时:
- 尝试绑定一个预先给定的 SocketAddr(是个常数,目前是 MAGIC_DNS_INSTANCE_ADDR)监听 RPC 请求(一台机器上所有 EasyTier 实例一起尝试绑定该 SocketAddr,绑定成功的那个就启动 DnsServer(当然也启动 DnsClient),失败的那些就只有 DnsClient),另外这个启动 DnsServer 的操作其实是个循环,每隔一小段时间或者 DnsClient rpc 失败后立刻尝试 bind,如果 bind 成功就说明 DnsServer 真挂了,那就自己在这个已有的 SocketAddr 上启动 DnsServer(忽略启动失败),这样才能保证服务不断
DnsServer 需要做到:
- 提供一个 RPC 接口接受 DnsClient 的心跳,如果心跳 checksum 和本地不符则返回 Mismatch
- 收到含有 delta 的心跳时,直接忽略 delta 中的 Zone 用全量 snapshot 替换本地配置;如果 delta 中含有 listeners 或者 addresses 则 rebind
- 持续检查是否有过期(丢失心跳)的 DnsClient,需要把这些 DnsClient 提供的所有配置清除
- 启动时自动添加 root zone,并把它的 forwarder 设置为系统 DNS
- 使用 snapshot 更新 zone。不用合并同名 zone,直接用 Zone 结构体提供的 FallbackAuthority 按顺序插入 Catalog 就行,不过注意要先插入 InMemoryAuthority,这些都是 records,后插入 ForwardAuthority,这都是 forwarders
- 更新 zone 的时候自动去掉 forwarder 中导致回环的那些,就是把 addresses 和 listeners 去掉(root zone 也需要这个逻辑)
- 内部接口,控制 DnsServer 是否 bind 到某些 socket(也就是配置中的 listeners)
- Listeners 绑定失败打印日志(失败一个打印一次然后就跳过),即便这时 addresses 为空也不要停机。(否则释放 socket 绑定后会有 instance 抢占 socket 试图启动 server,然后就死循环)
- 内部接口,更新 addresses,并且给 tun 添加删除这些 addresses 的路由。目前这些用来 hijack 的 addresses 都是只支持 udp 简单查询,就是一个 UDP 包查询,tcp 完全不管。但是可以支持除了 53 之外的端口,这个不难。
- 启动时,往 packet pipeline 上挂一个 filter,和目前 magic dns 的操作一样,给 addresses 添加路由并劫持所有目的为配置中 addresses 的 UDP 包,直接作为 DNS request 读取并交给 DnsServer 解析(这个 addresses 可能还得 append 到 resolv.conf 之类的地方)
- Addresses 和 Listeners 更新时需要检查所有 zone 的 forwarder,之前为了避免回环可能去掉了一些 forwarder,或者有新的 forwarder 要去除
此外,还有以下几个设计要点:
- Zone 允许只有 forwarder,这时候就是纯转发器
- Zone 允许没有 forwarder,这时候要检查是不是有 SOA 和 NS 记录,如果没有可能需要添加?
- 另一种方案是 DnsClient 挂 filter,自己处理 UDP 劫持,用某种方式(如 RPC)把 DNS 请求代理给 DnsServer,该方案的优势在于完全解耦 DnsServer 的实现,特别是解决了 DnsServer 所在实例可能 no_tun 的问题,缺点是:
- 性能更差
- 操作路由表或 /etc/resolv.conf 时会有多个 instance 同时修改,修改结果没有确定性
- DnsServer 仍然需要得知 addresses 以进行回环检测
- debug 更麻烦
- 难以实现策略 DNS,比如不同来源的 DNS 请求走不同的 zone
另外任何关于系统 DNS 的操作,清理都参考现有的 magic dns。
已知但无需/无计划解决的问题
- the ttl option isn't working because of https://github.com/hickory-dns/hickory-dns/pull/3450
- [minor] address 路由绑定必须在有 tun 的实例上做;listener 绑定则与 tun 无关,现有竞选机制无法保证有 tun 的实例能优先启动 DnsServer
- 不妨假设大多数情况下一台机器上所有实例的 no_tun 设置相同,这时候这个问题实际上不存在
- [minor] DnsServer 更新 zone 的时候需要更精细的合并/去重控制,如延迟低者/本地优先
- [minor] 更新 forwarder 时还需要检查间接回环,如 DNS 请求发送给某个 Peer,这个 Peer 又把请求转发回自己了
- [minor] 防止死锁/挂起的 DnsServer 占用 socket
[minor] RoutePeerInfo 可能不能过大- [minor] 增量 Zone 更新
TODO
- 配置解析
- DnsClient
- DnsServer
- 用
RoutePeerInfo传播配置 hash,用 RPC 拉取配置 - cli 输出状态
Related Issues
- related to https://github.com/EasyTier/EasyTier/issues/742
- related to https://github.com/EasyTier/EasyTier/issues/771
- related to https://github.com/EasyTier/EasyTier/issues/1071
- related to https://github.com/EasyTier/EasyTier/issues/1142
- related to https://github.com/EasyTier/EasyTier/issues/1322
- related to https://github.com/EasyTier/EasyTier/issues/1488
- related to https://github.com/EasyTier/EasyTier/issues/1597
- related to https://github.com/EasyTier/EasyTier/issues/1645
- related to https://github.com/EasyTier/EasyTier/issues/1764
- related to https://github.com/EasyTier/EasyTier/issues/1814
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/937
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/1873