dcompass v0.2.0 发布:真正可编程的 DNS 服务

2022-06-22 15:42:49 +08:00
 LEXUGE

dcompass 是一款高性能且可编程的 DNS 工具,使用 Rust 编写

新版本加入了基于 rhai 的脚本式处理功能,相较于之前基于 yaml 的自定义插件等增强了可读性和灵活性。

rhai 是一门类 Rust 的动态脚本语言。下面是一个使用 rhai 和 dcompass 自带的 Geo IP 插件进行防污染的范例:

script:
  init: |
    let geoip = new_geoip_from_path("../data/full.mmdb").seal();
  route: |
    let resp = upstreams.send("domestic", query);

    let ans = resp.answer[0];
    switch ans.rtype.to_string() {
      "A" if !geoip.contains(ans.to_a().ip, "CN") => { upstreams.send("secure", query) }
      "AAAA" if !geoip.contains(ans.to_aaaa().ip, "CN") => { upstreams.send("secure", query) }
      _ => resp
    }

在 v0.2.0 版本中,整个 DNS Message 的所有 sections (header, questions, answers, additional, authority) 的所有内容都被完整映射并可以在脚本中访问。结合脚本本身灵活的逻辑判断和流程控制能力,dcompass 可以更加紧凑地实现在之前冗长甚至不可能的规则定义。

目前 dcompass 自带的 utilities 有 Geo IP 插件,域名列表插件,IP CIDR 插件,并支持 DoH, DoT, 和 UDP 的上游转发。未来将支持更多上游协议,有其他插件需求也欢迎留言。

后续版本中将加入对 DNS Message 各个部分的编辑功能,来实现对各个 bitflag 和 record 的灵活控制。

欢迎批评指正!

3313 次点击
所在节点    DNS
15 条回复
adoal
2022-06-22 18:51:14 +08:00
看这架势很快会有人实现类似 dnsmasq 的 ipset 分流功能……
Buges
2022-06-22 19:06:35 +08:00
之前看到过,灵活性不够,就去用 mosdns 了。支持脚本了倒是不错。我的一个基本需求是:对每个不在已知列表的域名,并发向境外 doh 查询两条请求,一条带 ecs 一条不带,如果结果 ip 是 cn ,丢弃以上两条结果再直接向运营商 dns 查询并返回,如果不是,丢弃带 ecs 的结果返回另一个。
LEXUGE
2022-06-22 20:06:40 +08:00
@Buges 目前尚不支持添加 EDNS record ,后续更新后可以较为直接地实现你的需求。对需求有两点疑问:1. 为什么不先请求运营商的 DNS 并根据返回判断,目前的污染都是指向境外 IP 。因为可以先请求运营商,对非 CN 结果再作下一步处理 2. 为什么最后丢弃带 ECS 的结果?
Buges
2022-06-22 20:37:29 +08:00
@LEXUGE 原因很简单,直接请求境内 dns (无论是否运营商提供、无论是否 doh/dot )会把访问的**所有被代理的域名**全部暴露给有关部门,国内 isp 53 和 80 端口上行可是全部镜像的。现在是返回污染结果,日后就是照着日志上门喝茶了。
第二点则是隐私考虑。正常使用是不用 esc 的,这里提到的 esc 是从 china ip 列表中随便选一个确定是国内的 IP ,而非我自己的 ip ,用以指示境外 doh 解析器对境内有 CDN 的网站返回 cn 节点。如果返回的结果是 cn ip ,说明是境内网站,再用运营商 dns 解析以确保最优化。如果返回的不是 cn ip ,说明该网站无 cn 接入点或不支持 esc ,那么按照普通境外网站处理,返回正常境外 doh 解析的结果。

以上提到的 doh 请求全部是走代理发送,因为是并发两次所以延迟增加不大,对 cn 网站的第三次请求运营商 dns 因为是直连也很快,所以这套方案确保隐私、自动分流的同时访问延迟增加不大。
Buges
2022-06-22 20:41:59 +08:00
@LEXUGE 另外这是我之前的配置,后来经过某网友提示又写了个 mosdns 插件,会把所有经过这个流程确定归属的域名分别记录到两个单独的文件中,下次启动后直接作为分流列表加载,就不需要再次判断了。这样有了自动学习功能性能影响更小,还可以不定期的把生成的记录文件 upstream 给上游规则。你作为这类工具开发者强烈建议内置一个同类功能,并鼓励用户帮助完善分流规则,这样能极大的改善规则质量。
LEXUGE
2022-06-22 23:10:49 +08:00
@Buges 感谢建议。此外也有现成的请求域名匹配规则集如 https://github.com/felixonmars/dnsmasq-china-list/
Buges
2022-06-23 06:13:39 +08:00
@LEXUGE 当然,现成的境内域名列表和境外域名列表都是正在用的,但它们不能总是匹配每一个域名,我第一句就说了“不在已知列表的域名”。这套方案为不在列表的域名提供自动分流同时反过来学习记录提高已知列表域名的质量。
zzl22100048
2022-06-23 14:32:25 +08:00
脚本里面怎么自己构造 response 返回呢?想要实现 xip 功能
LEXUGE
2022-06-23 22:45:40 +08:00
@zzl22100048
在最新的 build 中,可以参考如下代码实现自定义 response:
```
let resp = query;
// 表明这个 DNS message 是 response
resp.qr = true;

// 在不同的 section 加入 resource record ,目前支持创建 TXT ,A ,AAAA record ,欢迎 file issue 来表明你的需求。
// query.first_question.qname 是第一个 question 的请求“域名”, 3600 是 TTL
resp.push_additional(create_record(query.first_question.qname, "IN", 3600, create_txt("vfs.global")));
resp.push_answer(create_record(query.first_question.qname, "IN", 3600, create_a("127.0.0.1")));
resp.push_answer(create_record(query.first_question.qname, "IN", 3600, create_aaaa("0000:0000:0000:0000:0000:0000:0000:0000")));
```
LEXUGE
2022-06-23 22:47:29 +08:00
也可以参考 README 中 quickstart 里的示例快速添加 EDNS Client Subnet record:

script:
route: |
let query = query;

// Optionally remove all the existing OPT pseudo-section(s)
// query.clear_opt();

query.push_opt(create_client_subnet(15, 0, "23.62.93.233"));

upstreams.send("secure", query)
LEXUGE
2022-06-23 23:00:14 +08:00
简单实现了一个 XIP 功能,对于非法格式会返回 SERVFAIL 。可以进一步判断 qtype 来正确支持 A 和 AAAA resource record 。
```yaml
script:
route: |
let resp = query;
resp.header.qr = true;

let ip = query.first_question.qname.to_string();
ip.replace(".xip.io", "");

resp.push_answer(create_record(query.first_question.qname, "IN", 3600, create_a(ip)));

resp
```
zzl22100048
2022-06-24 10:59:52 +08:00
@LEXUGE 看了下 rhai 文档,稍微改了下


```
script:
route: |
let resp = query;
resp.header.qr = true;

let ip = query.first_question.qname.to_string();
if ip.ends_with("xip.io"){
ip.replace(".xip.io","");
let ip=ip.split_rev(".");
if ip.len<4 {
return upstreams.send("domestic", query);
}
let r=r.extract(0..4);
print(r);
let ip = ip.reduce_rev(
|sum, v,i|if i==3 {v} else{sum+"."+v}
);
resp.push_answer(create_record(query.first_question.qname, "IN", 3600, create_a(ip)));
return resp;
}
upstreams.send("domestic", query)
```
LEXUGE
2022-06-24 12:24:29 +08:00
@zzl22100048 嗯,如果你需要对非 IP 前缀返回上游解析结果的话,可以这样写:


try {
resp.push_answer(create_record(qname, "IN", ttl, 3600, create_a(ip)));
} catch {
return upstreams.send(...);
}

直接把 replace 后的结果放入 create_a 尝试创建 A rdata ,如果 ip 并不是一个合法的 IPv4 地址,那么 create_a 就会出错,然后执行 catch 部分并返回上游结果。

这样做的话可以避免拆分再重建域名部分,更加简单,也可以提升性能。
zzl22100048
2022-06-24 13:30:54 +08:00
@LEXUGE 需要处理多级子域名配合 k8s 的 ingress
例如:sub.sub.example.192.168.1.1.xip.io -> 192.168.1.1 有更高效的方式吗
LEXUGE
2022-06-24 14:49:41 +08:00
@zzl22100048 那或许你目前的做法比较合适。如果需要高效的话可能直接在 Rust 中实现一个函数并在脚本中调用会更快

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/861396

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX