跳转到内容

目标审计

目标审计让 katana 节点拒绝连接你不希望它访问的主机,例如 BitTorrent tracker,或服务条款禁止的网站。每条规则是一个正则表达式。katana 用它匹配每个流所请求的主机,在第一次匹配时拒绝该流,并记住哪个用户命中了哪条规则,以便告知保存检测日志的面板。

规则有两个来源,可以同时使用:面板,以及 rule_list_path 指定的本地文件。如果你的节点所对接的面板会管理审计规则,或者你想要一份不依赖面板的固定屏蔽列表,请阅读本页。

以下步骤为现有节点添加一份本地屏蔽列表,两种面板都适用。

  1. 编写规则文件。 每行写一条正则。空行和以 # 开头的行会被跳过。

    /etc/katana/rules.txt
    # katana destination audit rules: one regex per line.
    # Hosts named tracker.*, announce.* or open.tracker.*, a common naming
    # pattern for public BitTorrent trackers
    (?i)^(tracker|announce|open\.tracker)\.
    # One site and every subdomain of it, with or without a trailing dot
    (?i)(^|\.)blocked\.example\.com\.?$
    # Any destination given as an IPv4 literal in 203.0.113.0/24
    ^203\.0\.113\.\d+$
  2. 让节点指向该文件。 在节点的 [node.api] 表中设置 rule_list_path:

    /etc/katana/config.toml
    [[node]]
    panel_type = "newV2board"
    [node.api]
    host = "https://panel.example.com"
    node_id = 1
    key = "replace-with-the-panel-key"
    node_type = "V2ray"
    rule_list_path = "/etc/katana/rules.txt"
  3. 检查配置。 katana --test -c /etc/katana/config.toml 会检查该配置项拼写正确且值为字符串。它不会打开规则文件,所以即使路径错误或正则有误,也仍然输出 Configuration OK。

  4. 应用修改。 如果 katana 已在运行,保存配置即可:热重载会立即加载新路径,并保留已打开的连接。否则请启动 katana。节点在启动结束时读取规则,之后每次轮询面板时再读取一次,因此之后修改文件内容既不需要重载也不需要重启:见规则变化时。

  5. 查看日志。 katana 无法读取的路径和每条无法编译的正则都会产生一行 WARN 日志,SSPanel 规则请求失败时也是如此。日志行表列出了这些日志。

两个配置项都属于同一个 [[node]] 条目。每个节点有自己的规则集,因此两个节点可以使用不同的文件,也可以使用同一个文件。

配置项 表 类型 默认值 说明
rule_list_path [node.api] path "" 本地规则文件。为空表示没有本地规则。katana 在每次刷新规则时读取该文件。相对路径以 katana 的工作目录为基准解析,所以请使用绝对路径。
disable_get_rule [node.controller] bool false 设为 true 会完全关闭规则刷新,面板规则和本地文件都不再刷新。如果在启动时就设置,节点将不做任何审计。

这两个表都会拒绝未知配置项,因此拼写错误会让 katana 停止运行,而不是悄悄关闭审计:

configuration error: config parse error: TOML parse error at line 9, column 1
|
9 | disable_get_rules = true
| ^^^^^^^^^^^^^^^^^
unknown field `disable_get_rules`, expected one of `listen_ip`, `send_ip`, `update_periodic`, `disable_upload_traffic`, `disable_get_rule`, `disable_sniffing`, `cert`

[node.controller] 中的 update_periodic(默认 60 秒)决定节点轮询面板的频率,因此也决定了刷新规则和上报命中的频率。katana 配置文件页面与其他 controller 配置项一起介绍了它。

每次刷新时,katana 会构建一个有序列表:先是本地规则,按文件中的顺序排列,然后是面板的规则。面板提供哪些规则取决于面板类型。

使用 panel_type = "newV2board"(或 "v2board")时,katana 从 UniProxy API 返回的节点配置中的 routes 数组获取规则,不会为此额外发请求。

Part of the panel's node config response
"routes": [
{ "id": 3, "match": ["(?i)(^|\\.)blocked\\.example\\.com$", "(?i)^tracker\\."], "action": "block" },
{ "id": 7, "match": ["example.org"], "action": "dns", "action_value": "198.51.100.53" }
]
  • 只有 action 恰好为 block 的条目才会成为审计规则。katana 忽略其他 action。
  • 一个 route 的 match 列表中的各项用 | 连接成一条正则。上面第一个 route 会变成 (?i)(^|\.)blocked\.example\.com$|(?i)^tracker\.。
  • 规则 id 是该 route 在 routes 数组中的位置,从 0 开始计数,而不是面板的 id 字段。上面第一个 route 的 id 为 0。id 只对上报有意义,而这种面板类型没有上报。
  • 如果连接后的正则无法编译,katana 会记录一条警告并跳过整个 route,而不只是跳过出错的那一项。
  • katana 把每一项原样当作正则使用。它不解释其他一些节点 agent 能识别的前缀,例如 regexp: 或 protocol:,所以请写普通正则。
  • katana 保留它最后收到的节点配置中的 routes。面板返回 304 Not Modified 或请求失败时,继续使用之前的 routes。
  • 修改节点的 [node.api] 表后,热重载会为节点创建新的面板客户端,新客户端会接管这些 routes。即使此时面板无法访问,由它们生成的规则也仍然有效。

对两种面板类型,本地文件的处理方式相同:

  • katana 先去掉每行首尾的空白,如果该行为空或以 # 开头则跳过。不支持行内注释:foo # bar 就是正则 foo # bar。
  • 其余每一行都编译为一条正则。无法编译的行会被记录并跳过,文件其余部分照常加载。
  • 每条本地规则的 id 都是 -1。
  • 如果文件无法读取,katana 会记录一条警告,本次刷新在没有本地规则的情况下继续进行。面板的规则仍会加载。
Xboard / V2board SSPanel 本地文件
来源 action = "block" 的 routes 条目 /mod_mu/func/detect_rules rule_list_path
每条规则对应 一个 route(其 match 列表用 | 连接) 一项 一行
规则 id 在 routes 中的位置,从 0 开始 该项的 id -1
命中是否上报给面板 否 是,仅 id 为 0 及以上的规则 否

规则匹配的是客户端请求的主机,即不带端口的字符串:

  • 域名,与客户端发送的完全一致,例如 www.blocked.example.com;
  • 或 IP 字面量,例如 203.0.113.7 或 2001:db8::1。IPv6 地址不带方括号,并写成压缩的小写形式,因此 2001:0DB8:0:0::1 会以 2001:db8::1 参与匹配。

匹配是不加锚点的搜索,使用 Rust regex 语法。这意味着:

  • blocked\.example\.com 也会匹配 notblocked.example.com.evil.test。请用 ^ 和 $ 加锚点,并用 (^|\.) 同时覆盖一个域名及其子域名。
  • katana 不会去掉末尾的点,所以请求 blocked.example.com. 的客户端不会匹配以 com$ 结尾的模式。模式以 \.?$ 结尾即可覆盖两种形式。
  • . 匹配任意字符。在域名和地址中请转义为 \.。
  • 匹配区分大小写。模式以 (?i) 开头可忽略大小写,因为客户端不一定会把域名转成小写。
  • 不支持环视((?=…)、(?<!…))和反向引用。这样的正则无法编译,会被跳过。
  • 端口不在字符串中,因此审计规则无法针对端口。请改用路由规则。

任何入站打开的每个流都经过同样的检查:TCP 请求、每个多路复用子流、每个 Hysteria 2 stream,以及每个 UDP 关联。katana 先对用户做准入,再为流选路,最后才审计。

flowchart TB
  A["新的流"] --> B{"用户仍在注册列表中?"}
  B -- 否 --> R1["拒绝"]
  B -- 是 --> C{"UDP?"}
  C -- 是 --> U["逐包检查"]
  C -- 否 --> D{"路由选择 block?"}
  D -- 是 --> R2["拒绝,不记录命中"]
  D -- 否 --> E{"有审计规则匹配?"}
  E -- 是 --> R3["拒绝,记录命中"]
  E -- 否 --> F["经路由选出的出站拨号"]
  • 第一条匹配的规则生效。 katana 按列表顺序测试规则,本地规则在前,并只记录第一条匹配的规则。
  • 被拒绝的流看起来与路由屏蔽一样。 katana 拒绝该流的方式与拒绝被路由到 block 的流完全相同,所以客户端无法区分两者。katana 不会写出任何指明规则或用户的日志行。
  • 被路由屏蔽的流不做审计。 如果路由已经选择了 block,katana 会直接拒绝该流而不测试审计规则,因此不会记录命中。

UDP 关联没有单一的目标地址,因为每个数据报都带有自己的地址。因此 katana 对每个发出的数据包分别做路由和审计。目标被路由屏蔽或匹配规则的数据包会被静默丢弃。它不计入流量,关联对其他目标地址仍保持开放。匹配规则的数据包会记录一次命中,与流的情况相同。

每个 TCP 流和每个发出的 UDP 数据包都会逐条测试整个规则列表,直到有规则匹配,所以请只保留需要的规则。

一次命中是一对用户 id 和规则 id。katana 维护一个集合,保存自上次上报以来出现过的所有组合,所以同一用户在两次轮询之间命中同一规则一千次,也只出现一次。在每次轮询结束时,以及节点停止时再一次,katana 取出整个集合,将其清空,并发送给面板:

面板 katana 发送的内容
SSPanel POST /mod_mu/users/detectlog,请求体为 {"data":[{"list_id":5,"user_id":42}]},每次命中一项。规则 id 小于 0 的命中(即本地规则)会被排除。如果没有剩余命中,则不发请求。
Xboard / V2board 不发送。katana 没有针对这种面板类型的检测日志上报,因此命中会被清空并丢弃。

上报失败会记录日志但不会重试:它携带的命中已经被清空。这与流量上报不同,后者会把未上报的流量留到下次轮询。

每次轮询按顺序执行以下步骤:

  1. 获取节点配置和用户列表,并应用它们。
  2. 刷新规则,除非 disable_get_rule 为 true:读取本地文件,并按上文所述收集面板的规则。
  3. 上报流量。
  4. 上报并清空命中。

因此,新规则会在一个 update_periodic 间隔内生效。启动时,katana 先启动节点的监听器,随后立即加载规则。当热重载修改了运行中节点的 [node.api] 表或其监听器设置(例如 listen_ip)时,katana 会立即执行一次轮询,而不是等待定时器,因此那次轮询也会刷新规则。

katana 按 id 和正则文本逐条比较新列表与当前生效的列表。如果两者完全相同,就保留当前规则集。任何差异(包括顺序变化)都会一次性替换整个规则集。已经打开的 TCP 流不会被重新检查;之后的每个 UDP 数据包按其发出时生效的规则集检查。

有三种情况不遵循这一规则:

  • 使用 SSPanel 时,如果规则请求得到 304 Not Modified 响应,katana 会保留当前规则集,而不重新读取本地文件。如果你的面板会这样响应,对本地文件的修改要等到面板规则下次变化、你保存对节点 [node.api] 表的修改,或 katana 重启时才生效。修改 [node.api] 会让节点获得一个不持有任何 ETag 的新面板客户端,因此面板会完整响应它的第一次规则请求,katana 也会重新读取本地文件。
  • 监听器变化时。 katana 按监听器保存规则,监听器由节点类型、listen_ip 和端口确定。当面板把节点改到另一个端口时,新监听器会在同一次轮询的刷新中获得规则。当热重载修改了 listen_ip 时,katana 会重建监听器,并从热重载立即执行的那次轮询中获得它的规则。使用 SSPanel 时,如果这两种情况下的那次刷新得到 304 Not Modified 响应,新监听器将没有规则,直到面板规则变化、修改 [node.api] 或重启。面板改端口和修改 listen_ip 都不会让节点获得新的面板客户端,节点保留原有的 ETag,所以面板在这里仍可能返回 304。
  • 在 Xboard / V2board 上设置了 [node.hysteria].port 的 Hysteria 2 节点,katana 根据配置文件描述该节点,从不获取面板的节点配置,因此节点收不到面板的 routes。只有本地文件生效。

要让运行中的节点改用另一个文件,修改 rule_list_path 并保存配置即可。热重载会为节点创建新的面板客户端并立即执行一次轮询,那次轮询的刷新会读取新文件。新的流会立即按新规则检查,这次修改也不会断开任何已打开的连接。使用 SSPanel 时,新文件会在第一次成功的规则请求中加载;在此之前,当前规则集保持生效。修改节点已指向的文件的内容无需重载:下次刷新就会读取。disable_get_rule 在每次轮询时读取,同样可以通过热重载修改。

katana 以 WARN 级别记录规则问题,并继续运行。1 代表节点的 node_id。

日志行 原因与处理
cannot read rule_list_path /etc/katana/rules.txt: No such file or directory (os error 2) 文件不存在,或 katana 用户无法读取。在后续某次刷新能读取它之前,节点只使用面板的规则运行。
invalid local rule "(foo": regex parse error: … 本地文件中有一行无法编译。该行被跳过;修正后会在下次刷新时加载。
invalid block rule ["(foo", "bar"]: regex parse error: … Xboard / V2board 的某个 block route 连接后的 match 列表无法编译。整个 route 被跳过。
invalid panel rule "(foo": regex parse error: … 某条 SSPanel 检测规则无法编译。该项被跳过。
node 1: node_rule: … SSPanel 规则请求失败。当前生效的规则集保持不变。
node 1: report illegal: … 向 SSPanel 上报检测日志失败。它携带的命中被丢弃。

正则解析错误会跨越多行,并在出错位置下方标出脱字符:

invalid local rule "(foo": regex parse error:
(foo
^
error: unclosed group

规则从不匹配。

  • 客户端可能在用 IP 地址连接。添加 IP 模式,或使用能看到嗅探域名的路由规则。
  • 检查大小写:加上 (?i)。
  • 对照确切的主机字符串检查锚点:^blocked\.example\.com$ 既不匹配 www.blocked.example.com,也不匹配 blocked.example.com.。
  • 查找与该规则相关的 WARN 日志行,并确认 disable_get_rule 不是 true。
  • 使用 SSPanel 时,查找 node_rule 警告:规则请求失败期间,本地文件也不会加载。
  • 如果某条路由规则已经把该目标发往 block,流会被拒绝,但不会产生审计命中。

所有连接都被拒绝。

  • 查找空的或过于宽泛的模式:block route 上为空的 match 列表或空条目、regex 为空或缺失的 SSPanel 项,或者 .、.* 这样的本地规则。
  • 逐条注释掉本地规则;每次修改在下次刷新时生效,但要注意规则变化时中的 SSPanel 304 情况。

SSPanel 中没有检测日志条目。

  • 本地规则(id 为 -1)的命中永远不会上报。
  • 命中在每次轮询结束时发送,所以请等待一个 update_periodic 间隔。
  • 查找 report illegal 警告。