# 插件及其参数

## 写在最前

### mosdns 的插件流水线概念

<figure><img src="https://3579691848-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCFG3I5tOk87v1Mcd3a3X%2Fuploads%2FeybbtwInswGyD2jLurB9%2Fplugin_workflow.svg?alt=media&#x26;token=7db6da64-f19c-4a39-a67b-5844c2ba40e3" alt=""><figcaption><p>一个能正常工作的简单的流水线示例。能将应答转发至上游。同时支持缓存。</p></figcaption></figure>

什么是插件: 插件接受并返回一个“请求上下文”。在内部处理请求/应答。

“请求上下文”包含了

* 客户端来源 IP 等信息。由服务端赋值。不可修改。
* 客户的请求。插件可以自由修改请求的数据。
* 应答。初始为空。插件可以自由修改应答的数据

插件流水线: 将多个插件串在一起，就构成了一条流水线。

### 配置文件中的插件

每个插件都是独立的，互不影响。同类型 `type` 插件可以初始化任意多个，只要插件的 `tag` 不重复就行。

<details>

<summary>多个同类型插件配置示例。点击展开</summary>

两个 `forward` 插件。`tag` 不同，参数不同。

```yaml
plugin:
  - tag: forward_google
    type: forward
    args:
      upstream:
        - addr: https://dns.google/dns-query

  - tag: forward_cloudflare
    type: forward
    args:
      upstream:
        - addr: https://1.1.1.1/dns-query
```

</details>

一些常用的插件配置已经被 mosdns 预先初始化好了，`tag` 以 `_` 开头以便区分。可以直接调用。

{% hint style="warning" %}
mosdns 会按照配置文件中顺序将插件初始化。有些插件需要调用其他插件，比如 `sequence，其调用的插件必须在之前配置。否则会因为找不到插件而初始化失败。`
{% endhint %}

## 可执行插件

### \_return

预初始化的插件。什么都不做。不会将请求上下文传递给后续流水线。可以用于结束 sequence 流水线。见下文。

流水线逻辑：

* 正向：跳过后续流水线。
* 反向：无。

### sequence 将多个插件组合成流水线

`sequence` 可以将多个插件组成一个流水线。`sequence` 也是插件，所以 `sequence` 里运行别的`sequence` 是可以的。

`_return` 是一个特殊的预初始化插件，可配合 `sequence` 使用，它只会跳过后续流水线。(注意: “跳过后续流水线”并不是直接将应答返回客户端，而是正向流水线运行结束，开始反向运行。)。

sequence 插件中调用的插件 tag 必须在 sequence 前定义，否则 sequence 找不到对应插件。

<figure><img src="https://3579691848-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCFG3I5tOk87v1Mcd3a3X%2Fuploads%2F4AO7e2M1eRPa0Z3IkZS7%2Fsequence_workflow.svg?alt=media&#x26;token=4087921a-92f5-4616-a079-3a0456d95f32" alt=""><figcaption><p>sequence 流水线图示。</p></figcaption></figure>

初始化参数说明:

```yaml
tag: ""
type: "sequence"
args:
  exec:
    # 可以时可执行插件的 tag。比如:
    - "hosts"
    - "cache"
    - "ecs"

    # 可以是 if 条件。
    # if 为条件表达式。支持 && ||，() 以及 ! 取反。
    # 预定义插件(以 _ 开头的插件)，需要用 [] 括住。
    - if: "(matcher && ! matcher) || [_preset_matcher]"
      exec:      # 条件满足，使用此分支流水线。
        - "ecs"
        - "forward"
      else_exec: # 否则，使用此分支流水线。
        - "ecs"
        - "forward"

    # 可以是 load_balance。
    # 轮换使用其内部的分支流水线。
    - load_balance:
        - - "ecs_1"     # 分支流水线 #1。
          - "forward_1"
        - - "ecs_2"     # 分支流水线 #2。
          - "forward_2"
        # ...           # 不限数量，可继续添加。
```

<details>

<summary>高级操作: 并发、失败自动回滚(故障转移)</summary>

```yaml
tag: ""
type: "sequence"
args:
  exec:
    # 可以是 parallel。
    # 并行运行其内部的独立流水线。取最先返回的有效(0 RCODE)应答。
    - parallel:
        - - "ecs_1"     # 独立流水线 #1。
          - "forward_1"
        - - "ecs_2"     # 独立流水线 #2。
          - "forward_2"
        # ...           # 不限数量，可继续添加。

    # 可以是 fallback。
    # fallback 有两种触发方式。详见下文。
    - primary:            # 主要流水线(独立的)。
        - "ecs_1"
        - "forward_1"
      secondary:          # 次要流水线(独立的)。
        - "ecs_2"
        - "forward_2"
      stat_length: 10   # 常规 fallback 统计长度。默认 0，禁用常规 fallback。可省略。
      threshold: 5      # 常规 fallback 触发阈值。不可大于统计长度。默认 0。可省略。
      fast_fallback: 200    # 快速 fallback 触发阈值。单位: 毫秒。默认: 0，禁用快速 fallback。可省略。
      always_standby: true # 快速 fallback secondary 始终待命。可省略。
```

`fallback`，`parallel` 涉及到多线程运行，其内部流水线其实是一个**独立的** sequence 插件，拥有**独立的**请求上下文，为了配置方便嵌套在这里。它们和当前 sequence 没有关系！在独立的 sequence 内对请求上下文修改，不会影响其他并行的流水线。同样 `_return`，`cache` 等能跳过后续流水线运行的插件，也只会终止自身流水线，不会对其他流水线产生影响。但最终 `fallback` 和 `parallel` 会把合适的应答放在当前 sequence 的请求上下文内。比如 `parallel` 会取最快返回的流水线的请求上下文的应答放在当前 sequence 的请求上下文。

fallback 两种触发方式:

* 常规 fallback: 监控主流水线最近 `stat_length` 次的请求状态，如果出现 `threshold` 次问题(没有应答或者有报错)，则下次请求会一并执行次流水线。取最先返回的应答。
* 快速 fallback: 对于每一个请求，如果主流水线运行结束了但没有返回应答，或者在 `fast_fallback` 毫秒后仍在运行无应答，则立即开始执行次流水线，取最先返回的应答。如果任意流水线返回了应答，这个请求就不会失败。比常规 fallback 策略更激进，次流水线可能运行的更频繁。
  * secondary 始终待命: 如果设为 `true`，则对于每一个请求，次流水线都会和主流水线一并执行，其结果将会在需要回滚时(没有返回应答或 `fast_fallback` 超时)，直接被使用。进一步降低响应时间。
* 快速和常规 fallback 可以同时启用。

</details>

### cache 缓存

缓存 DNS 应答的插件。

流水线逻辑：

* 正向：如果请求命中缓存，则将缓存的应答放入“请求上下文”，跳过后续流水线。如果没有命中，则继续执行后续流水线。
* 反向：缓存应答。

以下预初始化的插件 `tag` 可直接使用:

* `_default_cache`: 默认配置的内存缓存。大小 1024。无 lazy cache。

初始化参数说明:

```yaml
tag: ""
type: "cache"
args:
  size: 1024  # 内置内存缓存大小。单位: 条。默认: 1024。每个 cache 插件的内存缓存是独立的。
  compress_resp: false # (实验性)(v4.4+)启用应答压缩。大缓存场景可能会稍微降低内存占用。
                       # 注意: 如果使用 redis 外部缓存，切换该选项必须清空 redis 缓存。 
  
  # redis-server 外部缓存地址。填入外部缓存会自动禁用内置的内存缓存。上文的 size 参数会被忽略。
  # redis-server 需要 v2 及以上版本。如果多个 cache 插件连接到同一个 redis 数据库，数据是共享的。
  # cache 会为设定记录的 TTL，不会主动删除 redis-server 内的记录。内存大小清理方式等由
  # redis-server 负责。
  # cache 假设 redis-server 的距离很近/延时很低而且性能足够。
  # TCP 连接: `redis://<user>:<password>@<host>:<port>/<db_number>`
  # Unix 连接: `unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>`
  redis: "redis://localhost:6379/0"

  # redis-server 超时阈值。单位: 毫秒。默认: 1000。一般无需修改。
  # 如果 redis-server 在该时间内无响应，cache 会临时停用缓存。同时间歇性尝试 ping
  # redis-server，直至再次成功连接。
  redis_timeout: 1000

  # lazy cache 设定。lazy_cache_ttl > 0 会启用 lazy cache。
  # lazy cache 的设计参考了 RFC 8767。所有应答都会在缓存中存留 lazy_cache_ttl 秒，但自身的 TTL 仍然有效。
  # 如果命中过期的应答，则缓存会立即返回 TTL 为 lazy_cache_reply_ttl 的应答，然后自动在后台发送请求更新数据。
  # 相比强行增加应答自身的 TTL 的方法，lazy cache 能提高命中率，同时还能保持一定的数据新鲜度。
  lazy_cache_ttl: 0        # lazy cache 生存时间。单位: 秒。默认: 0 (禁用 lazy cache)。
                           # RFC 8767 的建议值 86400（1天）~ 259200（3天）
  lazy_cache_reply_ttl: 5  # 返回过期应答的 TTL。默认 5。

  # cache 默认只缓存有且只有一个 Question 的请求(简称"简单请求")的应答。请求的状态以运行到 cache 时为准。
  # 比如一个简单请求在 cache 后被插入格外信息(比如 ECS)，不会影响 cache 缓存这个请求，也不会影响 cache 后续
  # 查找这个简单请求。
  # cache_everything 会缓存所有请求的应答。会将完整的请求作为 key。能保证带有特殊标识的请求(比如 ECS，Cookie)
  # 即使 Question 相同仍然能被区分。
  # 注意: "将完整的请求作为 key" 意味着 cache 不会"精细"处理各种 EDNS0。用做公用服务器可能会大幅降低 cache 效率。
  # 如需处理 EDNS0 建议使用 unbound 等专业软件。
  cache_everything: false 
  
  # 当缓存命中后，将应答放入请求上下文后，需要执行的插件。
  when_hit: ""
```

lazy cache，也叫“乐观缓存”，的优缺点：

* 优点：可大幅提高缓存命中率。流行域名几乎 100% 命中。
* 缺点：可能影响 DNS 更新频繁的域名正常使用，比如 DDNS，CDN。
* 仅建议个人使用场景，有相关知识，了解其弊端的专业用户开启。

Metrics 数据:

* query\_total: 总请求数。
* hit\_total: 总命中数。(包括 lazy cache 的命中)
* lazy\_hit\_total: lazy cache 总命中数
* cache\_size: 当前缓存大小。单位: 条。

### hosts 域名映射 IP

流水线逻辑：

* 正向：如果请求的域名在 hosts 内，则将应答放入“请求上下文”，跳过后续流水线。如果不在，则继续执行后续流水线。
* 反向：无。

初始化参数说明:

```yaml
tag: ""
type: "hosts"
args:
  hosts:
    - "google.com 108.177.122.113" # 可以是一条记录
    - "provider:hosts"             # 可以是由 data-provider 提供的外部数据，格式 "provider:tag"。
```

{% hint style="warning" %}
虽然都叫 hosts，但本插件所用的格式和平常 Win，Linux 系统内的那个 hosts 文件不一样。
{% endhint %}

格式:

* 域名规则在前，IP 在后。支持一行多个 IP，支持 IPv6。
* 如果域名匹配规则的方式被省略，则默认是 `full` 完整匹配。域名匹配规则详见 [这里](#yu-ming-pi-pei-gui-ze)。

格式示例:

```
# 支持一行多个 IP
dns.google 8.8.8.8 2001:4860:4860::8888 ...
```

### \_prefer\_ipv4/6 优先使用 ipv4/6

`_prefer_ipv4` 和 `_prefer_ipv6` 是预初始化插件，可直接使用。下面为了表述方便，仅以 `_prefer_ipv4` 为例。`_prefer_ipv6` 同理。

`_prefer_ipv4` 可用让下游客户端支持双栈时优先使用 IPv4。`_prefer_ipv4` 会自动探测域名是否是双栈，然后只屏蔽双栈域名的 AAAA 请求。相比于强行禁用所有 AAAA 请求的方案，该插件不会影响纯 IPv6 域名的解析。

使用位置要求: 插件的后续序列中必须有转发请求的插件。

流水线逻辑：

* 正向：同时请求 A 和 AAAA。
* 反向：根据 A 和 AAAA 的结果，判断域名双栈支持状况，决定屏蔽策略。

sequence 示例:

```yaml
- _prefer_ipv4
- ...
- forward_xxx

# 配合 if 可以实现仅特定域名生效
- if: domain_matcher
  exec:
    - _prefer_ipv4
- ...
- forward_xxx
```

### ecs 为请求附加 ECS

流水线逻辑：

* 正向：附加 ECS 至请求。
* 反向：保证请求与应答 ECS 一致。如果源请求中无 ECS (现有 ECS 是插件新附加的)，则删除应答中的 ECS 回应。

初始化参数说明:

```yaml
tag: ""
type: "ecs"
args:
  auto: false             # 是否自动将用户请求的来源地址作为 ECS 附加到请求。
                          # 如果为 true，则预设地址不会被使用。
                          # 仅适用于公网的服务器。本地运行不要启用这个功能。
  ipv4: "1.2.3.4"         # 预设的 IPv4 地址。会优先附加在 A 请求里。
  ipv6: "2001:dd8:1a::"   # 预设的 IPv6 地址。会优先附加在 AAAA 请求里。
  force_overwrite: false  # 如果请求已有 ECS，是否强制覆盖。
  mask4: 24               # 用于 ipv4 地址的掩码。默认: 24。
  mask6: 48               # 用于 ipv6 地址的掩码。默认: 48。
```

### \_no\_ecs 删除请求和应答中的 ECS

`_no_ecs` 是预初始化插件的 tag，可直接使用。

流水线逻辑：

* 正向：删除请求中的 ECS。
* 反向：删除应答中的 ECS。

一般放在转发插件前。

### ttl 修改应答的生存时间

流水线逻辑：

* 正向：修改应答的生存时间。
* 反向：无。

使用位置：一般放在转发插件后。

初始化参数说明:

```yaml
tag: ""
type: "ttl"
args:
  minimal_ttl: 300  # 最小TTL。如果非零，小于这个数的应答的TTL会被修改成这个值。
  maximum_ttl: 3600 # 最大TTL。如果非零，大于这个数的应答的TTL会被修改成这个值。
```

### blackhole 丢弃应答或生成假应答

`blackhole` 会丢弃应答，或生成特定应答来屏蔽请求。

以下预初始化的插件 `tag` 可直接使用:

* `_drop_response`: 丢弃已存在的应答。
* `_new_empty_response`: 生成 Rcode 为 0 的空应答。放入“请求上下文”。(模拟域名存在但没有所请求类型的记录)
* `_new_servfail_response`: 生成 Rcode 为 2 (SERVFAIL: 服务器失败)应答。放入“请求上下文”。
* `_new_nxdomain_response`: 生成 Rcode 为 3 (NXDOMAIN: 域名不存在)应答。放入“请求上下文”。

流水线逻辑：

* 正向：丢弃或生成对应应答。
* 反向：无。

初始化参数说明:

```yaml
tag: ""
type: "blackhole"
args:
  rcode: 0        # 生成`Rcode`为该值的应答。如果负数，则将应答设为空值(丢弃已存在的应答)。
  ipv4:           # 如果请求类型是 A，则生成包含这些 IP 地址的应答。
    - "127.0.0.1"  
  ipv6:           # 如果请求类型是 AAAA，则生成包含这些 IP 地址的应答。
    - "::1"      
```

ipv4 和 ipv6 的优先级高于 rcode。如果都配置了， A/AAAA 请求会生成 IP 应答，其余类型会生成状态为 RCODE 的应答。

### fast\_forward 转发请求至上游(自带模块)

mosdns 自带模块。

流水线逻辑：

* 正向：转发请求至上游，获取应答后放入“请求上下文”。
* 反向：无。

初始化参数说明:

```yaml
tag: ""
type: "fast_forward"
args:
  # 上游服务器。至少要配置一个。可配置多个，会并发请求全部服务器。
  upstream:
    - addr: "https://dns.google/dns-query"
      dial_addr: "8.8.8.8"
      bootstrap: ""
      socks5: "127.0.0.1:1080"
      trusted: true
      idle_timeout: 30
      enable_pipeline: false
      enable_http3: false
      max_conns: 2
      insecure_skip_verify: false
      so_mark: 0
      bind_to_device: ""
    - addr: "tcp://8.8.8.8"
      enable_pipeline: true
    ca: [ ]
```

* `addr` 必需。服务器地址。省略 scheme 默认为 UDP 协议。省略端口号会使用协议的默认值。支持 IP 和域名。格式示例:
  * 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`
  * UDPME(v3.5.1+)(实验性): `udpme://8.8.8.8`
    * 这是个能过滤掉 UDP 抢答应答的方案。仍然是 UDP 协议。服务器必须支持 EDNS0。如果抢答者不支持 EDNS0，则可以过滤抢答应答。
    * 使用 dig 测试 `dig +edns cloudflare.com @服务器地址` 观察返回是否有一行 `EDNS: version: 0` 来确定服务器是否支持 EDNS0。绝大多数服务器都支持。
* 优先使用 IP 地址(如果服务器官方提供)。如果 `addr` 包含域名，插件需要将其解析成 IP 地址。
  * `dial_addr`: 为包含域名的 `addr` 手动指定其 IP 地址。支持端口号。指定后插件连接服务器时无需先解析服务器的域名。能更快建立连接。适用于 IP 万年不变的服务器。
  * `bootstrap`: (v4.2+)(实验性) 手动指定用于解析 `addr` 中域名的 bootstrap 服务器。只能是 UDP 服务器。指定后插件**每次**连接服务器时会先通过该 bootstrap 服务器解析域名。bootstrap 服务器的延时越低越好。HTTP/3 暂不支持该选项。仅支持 Win，Unix 平台。Macos，Plan9 等不支持。
  * 系统默认解析: 以上两个参数留空，会使用默认系统解析方式。
  * 优先级: `dial_addr` > `bootstrap` > 系统默认。
* `socks5`: socks5 服务器地址。格式 `host:port`。数据将会通过该代理中转。暂不支持用户名密码认证。UDP 和启用了 HTTP3 的 DoH 协议暂不支持该设定。
* `trusted`: 是否是可信服务器。可信服务器的任何应答都会被接受。其余服务器只接受 RCODE 为 0 (SUCCESS) 的应答。第一个服务器一定是可信服务器且不可修改。其余默认不可信。
* `idle_timeout` TCP/DoT/DoH 连接复用空连接保持时间。单位: 秒。默认 DoH: 30 ，TCP/DoT: 10。一般不需要改。
* `enable_pipeline`: TCP/DoT 使用 RFC 7766 新的 query pipelining 连接复用模式。
  * 启用后可大幅提高连接利用率，减少建立连接/握手的次数，进而降低延时。
  * 并非所有服务器都支持。必须确定服务器支持后再启用该选项。
  * Tips: 已知 Google 和 Cloudflare 的 TCP/DoT 都是支持的。知名的公共 DNS 服务商大多数都支持该模式。mosdns 自带一个测试命令可以测试服务器是否支持该模式。
* `enable_http3`: (v3.4.0+)(实验性) DoH 使用 HTTP/3 连接服务器。并非所有服务器都支持。必须确定服务器支持后再启用该选项。Tips: 已知 Google 和 Cloudflare 是支持的。
* `max_conns`： 默认: 2。一般不需要改。大流量场景可适当调大。
  * UDP 协议: 收发数据包使用的套接字数。
  * 启用了 query pipelining 模式的 TCP/DoT 协议: 最大连接数。
  * DoH 协议: HTTP 的最大连接数。HTTP/3 除外。
* `insecure_skip_verify`: 禁用 TLS 服务器证书验证。
* Linux 套接字设定。需要 `CAP_NET_ADMIN` 权限
  * `so_mark`:(v3.7+)。设定 SO\_MARK。
  * `bind_to_device`: (v4.3+) 设定 SO\_BINDTODEVICE。
* `ca`: 用于 TLS 验证的 CA 证书路径。是数组，可填入多个证书。默认使用系统证书池。

### forward 转发请求至上游(dnsproxy)

这个插件使用了 AdguardHome 的请求模块 [dnsproxy](https://github.com/AdguardTeam/dnsproxy/tree/master/upstream)。

流水线逻辑：

* 正向：转发请求至上游，获取应答后放入“请求上下文”。
* 反向：无。

初始化参数说明:

```yaml
tag: ""
type: "forward"
args:
  # 上游服务器。可配置多个。至少要配置一个。会并发请求全部服务器。
  upstream:
    - addr: "https://dns.google/dns-query" # 必需。服务器地址。
      # 当服务器地址为域名时，手动指定服务器的 IP 地址。可以配置多个。
      # 如果配置了，则不会使用 bootstrap 服务器。会按顺序尝试连接配置的 IP。
      ip_addr:
        - "8.8.8.8"
    - addr: "https://1.1.1.1/dns-query"

  # 用于解析上游服务器域名的 bootstrap 服务器地址。可配置多个。为空会使用系统默认方式解析域名。
  # 服务器地址不能包含域名。注意，dnsproxy 的 bootstrap 只会在启动时解析一次。服务器的 IP 在运行时不
  # 会自动更新。如需更新服务器的 IP，请重启。
  bootstrap:
    - "tls://1.1.1.1"
    - "https://223.5.5.5/dns-query"
    # ...

  timeout: 5          # 请求超时时间，单位: 秒。
  insecure_skip_verify: false  # 禁用 TLS 身份验证。
```

`addr` 地址格式和 AdguardHome 支持的格式一致。一些示例:

* UDP: `8.8.8.8`, `208.67.222.222:443`
* TCP: `tcp://8.8.8.8`
* DoT: `tls://dns.google`
* DoH: `https://dns.google/dns-query`
* DoH3: `h3://dns.google/dns-query`
* DoQ: `quic://dns.adguard.com`
* DNS stamp: `sdns://...` 详见 [dnscrypt.info](https://dnscrypt.info/stamps-specifications)

上述列表和参数说明不一定是最新的，请以 [dnsproxy 的 README](https://github.com/AdguardTeam/dnsproxy) 为准。

### ipset 将应答 IP 写入到系统 ipset

流水线逻辑：

* 正向：将应答 IP 写入 ipset。
* 反向：无。

初始化参数说明:

```yaml
tag: ""
type: "ipset"
args:
  set_name4: ""   # 将 ipv4 地址写入这个 set。这个 set 的 family 类型需为 `inet`。
  set_name6: ""   # 将 ipv6 地址写入这个 set。这个 set 的 family 类型需为 `inet6`。
  mask4: 0        # 写入 ipv4 地址时使用的掩码。默认: 24。
  mask6: 0        # 写入 ipv6 地址时使用的掩码。默认: 32。
```

* 插件添加新 IP 时相当于执行了 `ipset add <set_name> <IP>/<mask>` 命令。
* 插件使用 netlink 直接与内核通信，不依靠 `/sbin/ipset` 命令。
* 该插件不会创建 set。set 需要由用户提前创建。必须为 `hash:net` 类型。建议设置非零的 `timeout` 属性。实现自动清除长时间没有用到的地址。

创建 ipset 表命令示例:

```bash
ipset create <set_name4> hash:net family inet timeout 86400 maxelem 130050
ipset create <set_name6> hash:net family inet6 timeout 86400 maxelem 130050
```

### nftset 将应答 IP 写入到系统 nftables

流水线逻辑：

* 正向：将应答 IP 写入 nftables。
* 反向：无。

初始化参数说明:

```yaml
tag: ""
type: "nftset"
args:
  table_family4: "" # 存 IPv4 地址的 table family 类型。
  table_family6: "" # 存 IPv6 地址的 table family 类型。
  table_name4: ""   # 存 IPv4 地址 table 的名字。
  table_name6: ""   # 存 IPv6 地址 table 的名字。
  set_name4: ""     # 存 IPv4 set 的名字。
  set_name6: ""     # 存 IPv6 set 的名字。
  mask4: 0       # 如果 set 带有 interval flag。写入 IPv4 地址时使用的掩码。默认: 24。
  mask6: 0       # 如果 set 带有 interval flag。写入 IPv6 地址时使用的掩码。默认: 32。
```

* 插件添加新 IP 时相当于执行了 `nft add element <table_family> <table_name> <set_name> {<IP>/<mask>...}` 命令。
* 插件使用 netlink 直接与内核通信，并不依靠 `/sbin/nft` 命令。
* 该插件不会创建对应的 set。set 需要由用户提前创建。
* set 建议设置 `timeout` 来实现自动清除长时间没有用到的地址。
* set 建议设置 `interval` flag。插件会自动识别这个 flag，然后会以 IP 段的方式存储 IP。

创建 nftables set 命令示例:

```bash
nft add table inet <table_name>
nft add set inet <table_name> <set_name4> {type ipv4_addr\; timeout 7d3h8m45s \; flags interval\;}
nft add set inet <table_name> <set_name6> {type ipv6_addr\; timeout 7d3h8m45s \; flags interval\;}
```

### bufsize 限制请求的 EDNS0 的 UDP 负载大小

旧插件。建议使用新的 \_misc\_optm 插件。

流水线逻辑：

* 正向：修改请求的 EDNS0 UDPSIZE (如果请求有 EDNS0)。
* 反向：无。

初始化参数说明:

```yaml
tag: ""
type: "bufsize"
args:
  size: 512 # EDNS0 UDP 负载大小上限。默认 512。最大 4096。
```

### padding 填充 DNS 报文至固定长度 (实验性)

参考 RFC 8467，使用 EDNS0 的 Padding Option 将 DNS 报文用 0 填充至固定长度。据 Google 说 ([Google Plublic DNS 文档](https://developers.google.com/speed/public-dns/docs/doh#privacy_best_practices)) 能防止流量分析保护隐私。包含 3 个预定义插件。

`_pad_query`: 填充请求

* 流水线逻辑：
  * 正向：将请求填充至 128 bytes。
  * 反向：无。
* 使用位置要求: 该插件到转发插件之间不能有修改请求的插件。否则请求大小可能会变，影响效果。

`_enable_conditional_response_padding` 和 `_enable_response_padding`: 填充应答:

* 流水线逻辑：
  * 正向：无。
  * 反向：填充应答至 468 bytes。`_enable_response_padding` 是如果客户端请求包含 EDNS0，就会填充应答。而 `_enable_conditional_response_padding` 则是如果客户端对请求进行了填充，才会填充应答。
* 使用位置要求: 反向流水线填充应答后，直至流水线结束，不能有修改应答的插件。否则应答大小可能会变，影响效果。

### redirect 替换请求的域名 (实验性)

替换(重定向)请求的域名。请求域名 A，但返回域名 B 的记录。

流水线逻辑：

* 正向：替换请求的 QUESTION NAME。比如将请求域名 A 替换为 B。
* 反向：修改应答的 ANSWER 的 NAME，比如将 B 替换回 A。使之看起来像请求 A 的记录。

注意: 没有能完美替换请求域名的方法。本插件的替换方式对于常见的 A，AAAA，TXT 等仅依赖 QUESTION 和 ANSWER 的请求是有效的。但不保证兼容其他类型请求以及各种 EDNS0 扩展。

初始化参数说明:

```yaml
tag: ""
type: "redirect"
args:
  rule:
  # 格式 [域名匹配规则] [重定向至域名] 
    - www.google.com dns.google
    - provider:tag # 可以是由 data-provider 提供的外部数据。
```

* `域名匹配规则`: 如果匹配方式被省略，则默认是 `full` 完整匹配。域名匹配方式详见 [域名匹配规则](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v4/broken-reference)。

### arbitrary 返回任意记录 (实验性) (v4.0+)

arbitrary 可以载入任意记录，并在匹配到对应请求时生成包含这些记录的应答，

流水线逻辑：

* 正向：如果找到对应记录，则生成应答，终止跳过后续流水线。如果不在，则继续执行后续流水线。
* 反向：无。

初始化参数说明:

```yaml
tag: ""
type: "arbitrary"
args:
  rr:
    - example.com. 300 IN A 192.0.2.1 # 可以是文本格式的记录。
    - _sip._tcp.example.com. 86400 IN SRV 0 5 5060 sipserver.example.com.
```

不同记录的文本格式请自行搜索。关键词: `<记录类型> dns record wikipedia`。在英文 wikipedia 可以直接找到绝大多数记录的格式。

暂不支持从外部数据载入记录。

### \_misc\_optm 转发服务器优化大杂烩 (实验性) (v4.0+)

预定义插件，可直接使用。

适用于转发服务器。

流水线逻辑：

* 正向：
  * 屏蔽掉互联网公共上游不能处理的请求(多个 QUESTION 和非 IN CLASS)。(立刻返回 REFUSE，跳过后续流水线。)
  * 将 EDNS0 UDPSIZE 限定在 1200。(v4.5+)
* 反向
  * 去除 A/AAAA 应答的 CNAME 记录。减小应答长度。
  * 去除应答的 Padding。减小应答长度。
  * 随机打乱 A/AAAA 应答的 IP。负载均衡。
  * 如果请求无 EDNS0，去除上游应答的 EDNS0。保持 EDNS0 状态一致。

建议流水线位置:  流水线起始。

### reverse\_lookup IP 反查域名 API (v4.2+)(实验性)

该插件会缓存应答中的 IP 与 域名的关系。可在本地实现无延时的"有效 IP -> 域名"查询。支持 PTR 和 HTTP API。

流水线逻辑：

* 正向：如果是 PTR 请求，并且命中缓存，则生成应答，跳过后续流水线。如果没有命中缓存，则继续执行后续流水线。
* 反向：将 A/AAAA 应答的 IP 写入缓存。

初始化参数说明:

```yaml
tag: ""
type: "reverse_lookup"
args:
  size: 65535 # 内置缓存大小。默认 65535。
  
  # redis-server 外部缓存地址。填入外部缓存会自动禁用内置的内存缓存。上文的 size 参数会被忽略。
  # redis-server 需要 v2 及以上版本。如果多个 cache 插件连接到同一个 redis 数据库，数据是共享的。
  # reverse_lookup 会为设定记录的 TTL，不会主动删除 redis-server 内的记录。内存大小清理方式等由
  # redis-server 负责。
  # reverse_lookup 假设 redis-server 的距离很近/延时很低而且性能足够。
  # TCP 连接: `redis://<user>:<password>@<host>:<port>/<db_number>`
  # Unix 连接: `unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>`
  redis: "redis://localhost:6379/0"
  
  # 缓存时间。秒。默认 1800 (30min)。应答记录的 TTL 也会被自动修改，限制在该值以下。
  ttl: 1800
  # 是否主动处理/响应 PTR 请求。如果 PTR 的 IP 命中缓存，则生成应答后自动 _return，跳过后续插件运行。
  # 没有命中缓存则不会主动响应。会继续传递给后续插件。
  # 该功能的一个用途是，当下游设备使用网络监视类工具查看网络连接时，开启域名反查功能大概率会看到 IP 对应的
  # 真实域名(如果程序先请求域名再建立连接，而且域名经过了该插件处理。)
  handle_ptr: false 
                    
```

建议流水线位置:  流水线起始。务必放在 `cache` 前，防止 `cache` 命中缓存后不执行 `reverse_lookup` 导致无法记录 IP。

查找缓存中 IP 对应域名的 API：`http://<api_addr>/plugins/<plugin_tag>?ip=<ip_addr>`

返回: 如果缓存中找到对应域名，则返回域名。如果缓存中没有找到对应域名，则返回空。如果参数不对则返回 HTTP 400。

示例: `http://127.0.0.1:8080/plugins/my_reverse_lookup?ip=8.8.8.8`，返回 `dns.google.` 注意返回的是 fully qualified 域名，末尾有 `.`。

### metrics\_collector 统计数据收集器 (v4.2+)(实验性)

收集 prometheus metrics 数据。

流水线逻辑：

* 正向：query\_total =+1，thread =+1，response\_latency\_millisecond 开始计时。
* 反向：err\_total =+1 (如果出错)，thread =-1，response\_latency\_millisecond 结束计时。

```yaml
tag: ""
type: "metrics_collector"
# 没有 args 参数。
```

请求经过插件时，插件会收集

* query\_total 经过该插件的总请求数。
* err\_total 插件所在 sequence 的后续插件的总出错数。
* thread 当前经过该插件的线程数。
* response\_latency\_millisecond 插件所在 sequence 的后续插件的应答延时。

数据项名称为 `mosdns_plugin_<plugin_tag>_<name>`。

建议流水线位置: 放在流水线起始位置就可以统计本条流水线的数据。

### client\_limiter 限制客户端的 QPS (v4.2+)(实验性)

限制客户端(以 IP 或 IP 段区分)的最大每秒请求数 QPS。

流水线逻辑：

* 正向：如果客户端 QPS 超过阈值，则生成 REFUSE 应答，跳过后续流水线。
* 反向：无。

```yaml
tag: ""
type: "client_limiter"
args:
  max_qps: 100 # 最大 QPS。客户端的请求数超过时，会 REFUSED 然后 _return。
  v4_mask: 32  # ipv4 的 mask。用于限制整个 IP 段。默认 32。
  v6_mask: 48  # ipv6 的 mask。默认 48。
```

建议流水线位置: 流水线起始。

注意: 该插件只能用于向非恶意客户端“友好”地回复 REFUSED。不能防攻击。

### edns0\_filter 过滤请求的 EDNS0 OPTION (实验性)(v4.4.1+)

过滤/限制客户端可以使用 EDNS0 OPTION。

TIps: 如果后面配置有启用了 cache\_everything 的 cache 插件，该插件可用于提高缓存利用率。比如移除 Cookie 和 Padding，或只允许 ECS。

流水线逻辑：

* 正向：从请求中移除/保留指定的 EDNS0 OPTION。
* 反向：无。

以下预初始化的插件 `tag` 可直接使用:

* `_edns0_filter_no_edns0`: 删除应答的 EDNS0。
* `_edns0_filter_ecs_only`: 只保留 ECS。

初始化参数说明:

```yaml
tag: ""
type: "edns0_filter"
args:
  accept: [0,1,2]  # 仅接受这些 EDNS0 OPTION。其余的将被移除。
  discard: [0,1,2] # 仅移除这些 EDNS0 OPTION。其余的将被保留。
                   # accept 和 discard 只生效一个。优先级 accept > discard。如果全为空，则插件会
                   # 移除所有的 EDNS0 OPTION (相当于不接受任何 EDNS0 OPTION)。
```

建议流水线位置: 流水线起始，cache 前，转发插件前。

### \_query\_summary (v4.5.3+) 记录请求摘要

在 info 级别记录请求。包括请求的域名，类型，客户端地址，应答状态，请求处理时间等。

建议流水线位置: 流水线起始，cache 前。

注: 如果请求没有或有多个 QUESTION，则不会有记录。

### sleep 延时执行

调试。模拟延时用。

流水线逻辑：

* 正向：延时。
* 反向：无。

初始化参数说明:

```yaml
tag::  ""
type: "sleep"
args:
  duration: 100  # 等待时间。单位: 毫秒。
```

## 匹配器插件

匹配器插件匹配请求和应答状态，返回 `true` or `false`。

### query\_matcher 匹配请求的特征

以下预初始化的插件 `tag` 可直接使用:

* `_qtype_AAAA`: 请求的 QTYPE 是 AAAA。
* `_qtype_A_AAAA`: 请求的 QTYPE 是 A 或 AAAA。
* `_query_edns0`: 请求包含 EDNS0。

初始化参数说明:

```yaml
tag: ""
type: "query_matcher"
args:
  qtype: [1, 2, 3]          # 匹配 QTYPE
  qclass: [1, 2, 3]         # 匹配 QCLASS
  domain:                   # 匹配请求域名。这是个`域名匹配器`。详见下文。
    - "google.com"
  client_ip:                # 匹配客户端 IP 。这是个 `IP 匹配器`。详见下文。
    - "192.168.1.1/24"
  ecs:                      # 匹配 ECS 的 IP 。这是个 `IP 匹配器`。详见下文。
    - "192.168.1.1/24"
```

如果一个匹配器内设定了多个条件，则必须满足全部条件这个匹配器才会为 `true`。

### response\_matcher 匹配应答的特征

以下预初始化的插件 `tag` 可直接使用:

* `_response_valid_answer`: 应答包含有效的 ANSWER 记录(ANSWER 中有记录能与 QUESTION 对应)。(v4.0.0+)

初始化参数说明:

```yaml
tag: ""
type: "response_matcher"
args:
  rcode: [1, 2, 3]          # 匹配 RCODE。
  ip:                       # 匹配应答 A 和 AAAA 记录的 IP 。这是个 `IP 匹配器`。详见下文。
    - "192.168.1.1/24"
  cname:                    # 匹配应答 CNAME 域名。这是个`域名匹配器`。详见下文。
    - "google.com"
```

如果一个匹配器内设定了多个条件，则必须满足全部条件这个匹配器才会为 `true`。

### 域名匹配器

配置示例:

```yaml
# 可以是文本数据。域名匹配规则详见下文。
- "google.com"
- "keyword:google"
- "regexp:.+\.google\.com$"

# 可以是由 data-provider 提供的外部数据，格式 "provider:tag"。
# 外部数据可以是包含多条文本数据的纯文本数据。
- "provider:data_tag"  
# 也可以是 v2ray 域名数据。多个标签用 "," 分割，标签后支持用 "@" 只载入有对应 attr 的数据。
# 多个 "@" 为"或"关系，域名只要有其中任意 attr 就会被载入。
- "provider:geosite:cn@attr@attr,us@attr..."
```

文本域名数据一条规则一行。如果域名匹配方式被省略，则默认是 `domain` 域匹配。

域名匹配规则说明详见: [这里](#yu-ming-pi-pei-gui-ze)

### IP 匹配器

配置示例:

```yaml
# 可以是文本数据。每行一个 CIDR 或 IP，支持 IPv6。
- "192.168.1.1/24"
- "2001:250::/31"
- "192.168.1.1"
# 可以是由 data-provider 提供的外部数据，格式 "provider:tag"。
- "provider:data_tag"       # 可以是纯文本文件。包含多条文本数据。
- "provider:geoip:cn,us..." # 可以是 v2ray geoip 文件。多个标签用 "," 分割。
```

### 关于 v2ray 数据包

mosdns 支持从 v2ray 数据包 (俗称 dat 文件)载入数据，但受限于 v2ray 数据包的打包格式，mosdns 从中读取数据，无论读取多少，都要**完整解包**。多个插件读取多次会出现重复解包的情况。性能不好的设备(主要是嵌入式路由等)上可能造成 mosdns 启动时间显著变长(数秒)。其他设备影响不大。

优化 v2ray 数据包加载速度:

* (推荐)自己提前解包: `mosdns v2dat` 小工具命令可以解包数据文件成独立的文本文件。
* 尽可能使用多标签 `geoip.dat:cn,us... 一次载入全部所需数据，而非多行。写成多行会造成重复解包。`

## 补充

### 域名匹配规则

域名规则有多个匹配方式:

* 以 `domain:` 开头，域匹配。e.g: `domain:google.com` 会匹配自身 `google.com`，以及其子域名 `www.google.com`, `maps.l.google.com` 等。
* 以 `full:` 开头，完整匹配。e.g: `full:google.com` 只会匹配自身。
* 以 `keyword:` 开头，关键字匹配。e.g: `keyword:google.com` 会匹配包含这个字段的域名，如 `google.com.hk`, `www.google.com.hk`。
* 以 `regexp:` 开头，正则匹配([Golang 标准](https://github.com/google/re2/wiki/Syntax))。e.g: `regexp:.+\.google\.com$`。

如果没有指定匹配方式，不同插件有不同的缺省值。

匹配方式按如下顺序生效: `full` > `domain` > `regexp` > `keyword`。

相同匹配方式的规则按如下顺序生效:

* `domain` 规则: 子域名优先。比如如果同时存在规则 `google.com` 和 `com`。 `www.google.com` 会优先匹配 `google.com`，然后 `com`。(v3.9.0+)
* `regexp` 和 `keyword` 规则生效顺序为规则导入的顺序。

性能:

* `domain` 和 `full` 匹配使用 HashMap，复杂度 O(1)。每 1w 域名约占用 1M 内存。
* `keyword` 和 `regexp` 匹配需遍历，复杂度 O(n)。

到底了！没有找到想要的功能么？提交一个 [Feature request](https://github.com/IrineSistiana/mosdns/issues/new?assignees=\&labels=\&template=feature_request.md\&title=%5BFeature+request%5D)。
