# Sequence 插件

sequence 是一个 [可执行插件](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ru-he-pei-zhi-mosdns/ke-zhi-xing-cha-jian) ，可以被其他插件调用。因其功能较多，单独写在本页面。

## 写在最前

### 参数帮助说明

* `{ arg1 | arg2 | ... }` 代表这几个参数必需选择其中一个(多选一)。
* `[ arg ]` 代表该参数是可选的。可省略。
* `...` 表示之前的参数可以无限重复。`*n` 表示之前的参数可以重复 n 次 。
* 没有符号的 `arg` 表示该参数不可省略。

## 参数

初始化参数说明:

```yaml
tag: ""
type: "sequence"
args:  # []rule
  - matches:                # []string, 匹配条件。多个匹配条件为 AND 关系。
    - qname $ad_domain      # [!]{ 内置匹配名称 | $已定义的匹配器插件tag } [参数]
    - "!qname $ad_domain"   # !前缀代表取反。
    exec: reject 3          # string, { 内置操作名称 | $已定义的可执行插件tag } [参数]
  - exec: $forward_google   # matches 条件可省略。
```

sequence 插件引用的所有已定义的插件 tag 必须在 sequence 前定义，否则 sequence 找不到对应插件。

一个 sequence 由多个 rule 构成。一个 rule 由零个或多个条件和一个操作构成。sequence 会依次执行每个 rule 。

可以把 sequence 近似成处理 DNS 请求的 iptables / nftables 的 chain 。复杂的处理逻辑可以配置多个 sequence 然后配合 `accept/reject/jump/goto/return` 操作在 sequence 之间跳跃。

可执行插件列表见：

{% content-ref url="ke-zhi-xing-cha-jian" %}
[ke-zhi-xing-cha-jian](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ru-he-pei-zhi-mosdns/ke-zhi-xing-cha-jian)
{% endcontent-ref %}

错误处理:

如果遇到插件抛出错误，sequence 不会继续执行后续规则，并继续抛出该错误。

{% code title="配置示例:" lineNumbers="true" %}

```yaml
tag: ""
type: "sequence"
args:
  # 屏蔽某些域名，比如广告域名。
  - matches:
      - qname $ad_domain
    exec: reject 3
  - ...
  
  # 将内网请求转交给 handle_local_query sequence 另行处理。
  - matches:
      - qname local internal.my.domain
    exec: goto handle_local_query
  - matches:
      - ptr_ip 10.0.0.0/8
    exec: goto handle_local_query

  # 让某些域名跳过 cache，比如的 DDNS 域名。然后仅
  # 修改 DDNS 域名的 TTL。
  - matches:
      - "!qname my.ddns.domain"
    exec: cache 1024
  - exec: forward 8.8.8.8
  - matches:
      - qname my.ddns.domain
    exec: ttl 1

  # env 实现"开关功能"。
  # 运行 mosdns 时设定 MOSDNS_ENABLE_CACHE 环境变量即可开启 cache 功能。
  - matches:
      - env MOSDNS_ENABLE_CACHE
    exec: cache 1024
  - ...
```

{% endcode %}

## 内建匹配器

### resp\_ip

匹配应答 A 和 AAAA 记录的 IP 。如果应答有多个 IP 只要任意 IP 匹配即为 true 。

参数: &#x20;

```
resp_ip { ip | $ip_set_tag | &ip_list_file } ...
```

示例: &#x20;

```
resp_ip 192.168.1.1/24 8.8.8.8 $local_ips &./local_ips.txt
```

* `ip` 可以是 IPv4/6, CIDR 。
* `ip_set_tag` 是类型为 [ip\_set](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/shu-ju-cha-jian#ip_set) 插件的 tag 。&#x20;
* `ip_list_file` 是文本类型的 IP/CIDR 表。

### client\_ip

匹配客户端的 IP 。

参数和示例和上文的 [resp\_ip](#resp_ip) 相同。

### ptr\_ip

匹配 PTR 请求的 IP。

参数和示例和上文的 [resp\_ip](#resp_ip) 相同。

### qname

匹配请求的 QNAME 。

参数:  `qname { domain_rule | $domain_set_tag | &domain_list_file } ...`

示例:  `qname google.com full:google.com $local_domains &./local_domains.txt`

* `domain_rule` 是域名表达式。支持扩展。详见 [域名匹配规则](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ru-he-pei-zhi-mosdns/yu-ming-pi-pei-gui-ze)。
* `domain_set_tag` 是类型为 [domain\_set](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/shu-ju-cha-jian#domain_set) 插件的 tag 。
* `domain_list_file` 是文本类型的域名表达式表 。

### cname

匹配应答的 CNAME 记录。如果应答有多个 CNAME 只要任意 CNAME 匹配即为 true 。

参数和示例和上文的 [qname](#qname) 相同。

### qclass

匹配请求的 QCLASS。

参数:  `qclass [uint16] ...`

示例:  `qclass 1 2 3`

### qtype

匹配请求的 QTYPE。

参数和示例和上文的 [qclass](#qclass) 相同。

### rcode

匹配应答的 RCODE。

参数和示例和上文的 [qclass](#qclass) 相同。

### has\_resp

如果有应答，返回 true 。

### has\_wanted\_ans

如果有期望的 ANSWER，返回 true 。"期望的ANSWER "指应答的 ANSWER 中包含和 QUESTOIN 类型相同的 record。比如请求 A 记录，应答里返回了 A 记录，则该判断为 true。请求 AAAA 记录，虽然返回了SUCCESS 应答但无 AAAA 记录，返回 false 。

### mark

参数:  `mark [uint32] ...`

示例:  `mark 1 2 3`

请求存在任意指定的标记即为 true 。

### string\_exp

(实验性) 字符串匹配器。

参数:  `string_exp string_src op [arg_string]`

示例:  `string_exp` server\_name`eq a.b.c d.e.f`

`string_src`: 匹配的字符串来源。可以是&#x20;

* url\_path: 来自 DoH 服务端请求的 url path 。
* server\_name: 来自服务端 TLS (DoH,DoT,DoQ) 的 SNI。
* $env: 以 $ 开头，环境变量的值。

`op` 是匹配方式。可以是 `eq` 相等，`prefix` 包含前缀，`suffix` 包含后缀，`contains` 包含，`regexp` 正则，`zl` 字符串长度为零。&#x20;

`arg_string` 是匹配参数，目前除了 zl 无参数，其余匹配方式均可包含多个字符串作为匹配规则。任意规则匹配即返回 true 。

注意: 受格式限制，目前不支持带空格的规则参数。

### 其他匹配器

#### \_true 和 \_false

永远返回 true 和 false 。方便脚本生成占位符。

`_` 前缀没有别的意义，只是防止匹配器名在某些配置格式里被误识别成 bool 类型。

#### env

参数:  `env env_key [env_value]`

示例:  `env CACHE_ENABLED` ， `env NETWORK_TYPE lan` 。

启动时查找 `env_key` ，如果值和 `env_value` 一致则返回 true。如果 `env_value` 省略，则 `env_key` 存在(包括零值的情况)即返回 true 。

#### random

参数:  `random prob_float64`

示例:  `random 0.5`&#x20;

以 `prob_float64` 的概率返回 true 。

## 内建操作

### forward

转发请求至上游服务器获取应答。

参数:  `forward upstream [upstream]...`

示例:  `forward 8.8.8.8 tls://8.8.8.8`

可配置多个上游。会并发请求，取最先返回的应答。如果超过 3 个上游，会随机选取 3 个上游并发。

`upstream` 支持四种协议。格式示例:

* UDP: `8.8.8.8`, `208.67.222.222:443`
* TCP: `tcp://8.8.8.8`
* DoT: `tls://8.8.8.8:853`, `tls://dns.google`
* DoH (RFC8484 GET): `https://8.8.8.8/dns-query`, `https://dns.google/dns-query`
* Tips: 使用 IP 地址能更快建立连接，因为不用解析服务器的 IP。除非情况特殊，否则不要使用带域名的地址。

这是个便捷设置。如需更多高级功能 (HTTP3，Socks5，SO\_MARK...)，可配置独立的 [forward](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ke-zhi-xing-cha-jian#forward) 插件。

### cache

DNS 缓存。如果请求命中缓存，则将缓存的应答放入请求。

当某一请求经过 cache 后，cache 会在这个请求处理流程结束时自动将应答放入缓存。用户无需介入。

不支持扩展 DNS 协议 (即 EDNS0)。

参数:  `cache [size]`，`size` 是缓存大小，单位: 条，默认 1024。

示例:  `cache 1024`

这是个便捷设置。如需更多高级功能 (lazy cache，缓存存盘...)，可配置独立的 [cache](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ke-zhi-xing-cha-jian#cache) 插件。

### black\_hole

生成包含特定 IP 的应答。

参数:  `black_hole [ip] ...`

示例:  `black_hole 127.0.0.1 ::1 0.0.0.0`

会自动识别参数中的 IPv4/6 。如果参数中指定了多个 IPv4/6，会全部包含在应答内。如果某一 IP 类型未指定，则不会生成对应类型的应答。

### drop\_resp

丢弃应答。用于触发 fallback。

### ecs

附加 ECS (EDNS0 Client Subnet) 至请求。支持 IPv4/6。IPv4 会使用 /24 mask，IPv6 使用 /48 。

参数 (v5.3+):  `ecs [ip]`&#x20;

参数 (v5.2 以前)(弃用):  `ecs [ip/mask] [ip/mask]` 。v5.3+ 后只会用第一个 IP，mask 会忽略。

示例:  `ecs 1.2.3.4`

### forward\_edns0opt

&#x20;(实验性)(v5.3+) 允许客户端的这些 EDNS0 Option 转发至上游。

参数:  `forward_edns0opt [option_code] ...`&#x20;

注意:&#x20;

* 此插件不可以执行多次! 否则转发至上游的请求中 EDNS0 Option 将出现重复，导致协议错误。
* 并不是所有 EDNS0 Option 都能被转发。此插件设计的目是转发 10 (Cookie) 和 15 (Extended DNS Error)。

### ipset

(实验性) 写入应答 IP 至 ipset 。仅支持 Linux 。插件会直接用 netlink 和内核通信，不依赖也不运行 ipset 命令，效率很高。

参数:  `ipset [set_name,set_family,mask]*2`

示例:  `ipset my_set,inet,24 my_set6,inet6,48`

* `set_family` = `{ inet | inet6 }`
* `mask` 是写入 IP 时使用的掩码

### nftset

(实验性) 写入应答 IP 至 nftables 。仅支持 Linux 。插件会直接用 netlink 和内核通信，不依赖也不运行 nft 命令，效率很高。

参数: &#x20;

```
nftset [table_family,table_name,set_name,set_dtype,mask]*2
```

示例: &#x20;

```
nftset inet,my_table,my_set,ipv4_addr,24 inet,my_table,my_set,ipv6_addr,48
```

* `table_family` = `{ ip | ip6 | inet }` table 表的类型。
* `set_dtype` = `{ ipv4_addr | ipv6_addr }` set 的类型。
* `mask` 是写入 IP 时使用的掩码。插件会自动判断 set 是否带有 interval 这个 flag，如果 set 有 interval flag 的话，会以 IP 段的形式写入 set 。

<details>

<summary>ipset/nftables 配合 cache 示例</summary>

以 ipset 为例:

```yaml
tag: ""
type: "sequence"
args:
  - exec: cache 1024
  - matches:        # 如果缓存命中，则不会转发请求至上游。
    - "!has_resp"
    exec: forward 8.8.8.8
  - exec: ipset ...  # ipset 每次请求都能被刷新
```

</details>

### query\_summary

以 info 级别在 log 打印请求的摘要。包含: 请求的域名，类型，客户端 IP，应答的 RCODE，错误信息，后续规则处理请求的用时。

参数: `query_summary [ msg_title ]`

示例: `query_summary query from server 1`

* `msg_title` 的文字会写在 log 的 `msg` 键上。

如果一个请求经过了多个 query\_summary，log 打印的顺序是反的，即先打印最后经过的 query\_summary 的 log。这不是 bug 。

### metrics\_collector

(实验性) 收集 prometheus metrics 数据。

参数: `metrics_collector collector_name`&#x20;

* `collector_name` 不可重复。

暴露的 metrics 数据项名称为

`mosdns_metrics_collector_<metric_name>{name=collector_name}`。

插件会收集以下 metric:

* query\_total 经过该收集器的总请求数。
* err\_total 该收集器的后续规则的总出错数。
* thread 该收集器后续规则的线程数。(此时有多少请求正在被后续规则处理中)
* response\_latency\_millisecond 该收集器后续命令返回的延时。

### prefer\_ipv4/6

希望客户端解析双栈域名 IP 时，只返回 IPv4/6 。但不影响纯 IPv6/4 (单栈)域名的解析。

`prefer_ipv4` 和 `prefer_ipv6` 是两个操作。下面为了表述方便，仅以 `prefer_ipv4` 为例。`prefer_ipv6` 同理。

`prefer_ipv4` 让下游客户端支持双栈时优先使用 IPv4。`prefer_ipv4` 处理 AAAA 请求时会自动发送 A 请求测试域名是否是双栈，该新产生的 A 请求会和原 AAAA 请求一并继续执行后续规则。如果最后 A 和 AAAA 请求都获得到了 IP (域名为双栈)，则原 AAAA 请求被屏蔽。这样客户端只能收到 A 记录，进而强迫客户端使用 IPv4

(实验性特性)(v5.3+) 插件会缓存域名的双栈支持情况，以避免频繁测试。缓存大小 65535 。缓存时长 1 小时。暂不支持自定义。

使用位置要求: 后续规则必须能生成应答 (比如有 forward)。

sequence 示例:

```yaml
# 所有请求都优先返回 ipv4，屏蔽 ipv6。
- exec: prefer_ipv4
- ...
- exec: forward 8.8.8.8

# 配合匹配器实现仅特定域名生效。
- matches:
    - qname my.domain
  exec:
    - prefer_ipv4
- ...
- exec: forward 8.8.8.8
```

### ttl&#x20;

修改应答的 TTL。

参数: `ttl { fix | min-max }`

示例: `ttl 300` 将 ttl 固定在 300， `ttl 300-600` 限制 ttl 在 300 到 600 之间。`ttl 300-0` 限制 ttl 不小于 300 。

### mark

在请求上打标记。

参数:  `mark [uint32] ...`

示例:  `mark 1 2 3`

### 调试用

#### sleep

等待一段时间。增加/模拟延时用。

参数: `sleep millisecond`

#### debug\_print

以 info 级别在 log 打印完整的请求和应答信息。

参数: `debug_print [msg_title]`

示例: `debug_print query from server 1`

* `msg_title` 的文字会写在 log 的 `msg` 键上

## 内建跳转操作

### accept

停止后续规则执行。如果本 sequence 存在上级 sequence，所有上级的 sequence 也不再继续执行后续规则。

### reject

参数: `reject [rcode]`

示例: `reject 5`

和 accept 一样，如果本 sequence 存在上级 sequence，所有上级的 sequence 也不再继续执行后续规则。同时生成 `rcode` 应答。不指定 `rcode` 默认为 5 (REFUSED) 。

### return

停止后续规则执行。如果所在 sequence 是从另一 sequence jump 过来的，则返回源 sequence 继续执行。

### goto

参数: `goto sequence_tag`

示例: `goto my_next_sequence`

`sequence_tag` 是目标 sequence 插件的 tag。将不再执行本 sequence 后续规则，开始执行目标 sequence 。

### jump

参数: `jump sequence_tag`

示例: `jump my_next_sequence`

`sequence_tag` 是目标 sequence 插件的 tag。将开始执行目标 sequence，目标 sequence 结束或 return 后，返回本 sequence 继续执行。目标 sequence accept/reject 后，本 sequence 和所有上级的 sequence 不再继续执行后续规则。

## mark 高级用法示例

实现逻辑或，单次匹配后执行多个 exec，以及实现 if ... else。

```yaml
- tag: main
  type: sequence
  args:
  
    # 匹配成功后打标
    # 打相同标签可实现"逻辑或"
    - matches:
      - "match_A"
      exec: mark 123456789
    - matches:
      - "match_B"
      exec: mark 123456789

    # 连续执行
    - matches: mark 123456789
      exec: do_something_1
    - matches: mark 123456789
      exec: do_something_2
    # ...
    # 连续执行 else
    - matches: "!mark 123456789"
      exec: else_do_something_1
    - matches: "!mark 123456789"
      exec: else_do_something_2
```
