跳转到内容

路由

katana 配置中的每个 [[node]] 都有自己的路由表。用户每打开一条流,katana 就从上到下遍历路由表,把这条流交给第一条匹配规则所指定的出站。如果没有规则匹配,流就走节点的默认出站;除非你另行修改,默认出站是 direct。

如果你想让节点屏蔽广告或内网地址、让部分网站经由 WireGuard 或代理出站,或者想弄清某条规则为什么不匹配,请阅读本页。出站本身在整个进程范围内只定义一次,见出站。

路由表是本地配置,任何面板都不会下发路由表。面板能下发的是审计规则:在 Xboard 和 V2board 上,面板中动作为 block 的路由会变成审计规则,详见审计。

下面这个节点屏蔽广告域名和 SMTP,其余流量全部直连:

/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"
[node.route]
default = "direct"
geosite = "/etc/katana/geosite.dat"
[[node.route.rule]]
outbound = "block"
geosite = ["category-ads-all"]
[[node.route.rule]]
outbound = "block"
port = ["25", "465", "587"]

在 katana 加载之前先检查路由:

终端窗口
katana --test -c /etc/katana/config.toml

--test 会构建每个节点的路由器,因此会读取 geodata 文件并解析每个出站 tag。它输出 Configuration OK,或者输出 configuration error: 加上发现的第一个问题。

入站解码一条流之后,这条流会经过一条固定的处理路径,路由是其中一步。katana 支持的所有协议都走同一条路径,包括 mux 子流和 Hysteria 2 流。

flowchart TB
  D["入站解码请求"] --> S["嗅探首批字节:仅限目标为 IP 的 TCP"]
  S --> A{"准入:用户是否仍在列表中?"}
  A -- 否 --> X["拒绝"]
  A -- 是 --> N{"TCP 还是 UDP?"}
  N -- TCP --> R["路由:第一条匹配的规则,否则用 default"]
  R --> B{"出站是 block?"}
  B -- 是 --> X
  B -- 否 --> AU{"命中审计规则?"}
  AU -- 是 --> X
  AU -- 否 --> DL["经由出站拨号"]
  DL --> M["计量:为用户计费并限速"]
  N -- UDP --> P["逐包:路由、审计、发送、计量"]
  1. 嗅探。 对目标为 IP 的 TCP 流,入站先读取客户端最开始发送的字节,从中寻找域名。见嗅探。
  2. 准入。 katana 检查该用户是否仍在面板的用户列表中。上一次刷新已移除的用户会被拒绝。
  3. 路由。 路由器根据目标地址、嗅探到的域名(如果有)和端口选出一个出站。
  4. 审计。 katana 用节点的审计规则匹配目标地址。只有当路由没有选中 block 时才会执行审计,所以被路由屏蔽的流永远不会记为审计命中。
  5. 拨号。 选中的出站连接目标地址。
  6. 计量。 经过出站的每个字节都计入该用户的流量,并受该用户的限速约束。被拒绝的流不会拨号,因此不消耗用户任何流量。
键类型必填默认值说明
defaultstring否"direct"没有任何规则匹配时流使用的出站 tag。出站池里的任意 tag 都可以:内置的 direct、block、freedom、blackhole,或者顶层某个 [[outbound]] 的 tag。tag 区分大小写。未知的 tag 报错 route references unknown outbound tag: <tag>。
geoippath视情况—v2ray 格式 geoip.dat 的路径。只要本节点有规则用到 geoip 就必须填写,否则构建失败,报 a geoip matcher is used but no geoip file is configured。katana 只在有规则引用时才读取该文件,没被用到的路径不会被打开。内存里只保留被引用的代码。
geositepath视情况—v2ray 格式 geosite.dat 的路径(v2fly 的 dlc.dat 也可以)。只要本节点有规则用到 geosite 就必须填写,否则构建失败,报 a geosite matcher is used but no geosite file is configured。和 geoip 一样,只在被引用时读取。
rulearray of tables否[]路由规则,每个 [[node.route.rule]] 块一条,从上到下依次检查,第一条匹配的规则决定出站。键名是单数:rules 会被当作未知字段。

[node.route] 是可选的。不写时,节点没有任何规则,所有流都走 direct。

出站池由所有节点共享。其中始终包含四个内置 tag,每个顶层 [[outbound]] 再加入自己的 tag:

Tag 作用
direct 从本机连接目标地址,使用进程级的 [dns] 解析器。
freedom direct 的别名,为兼容 Xray 风格的配置而保留。
block 拒绝 TCP 流,丢弃 UDP 包。
blackhole block 的别名。
任意 [[outbound]] 的 tag 经由该上游转发流:SOCKS、HTTP、VMess、VLESS、Shadowsocks、WireGuard,或者另一个 direct。

[[outbound]] 不能使用内置 tag;katana 会拒绝并报 duplicate/reserved outbound tag <tag>。

键类型必填默认值说明
outboundstring是—匹配的流使用的出站 tag。它必须在出站池里(direct、block、freedom、blackhole,或者顶层某个 [[outbound]] 的 tag),否则构建失败,报 route references unknown outbound tag: <tag>。不写时解析失败,报 missing field 错误。
domain_suffixarray of strings否[]匹配该域名本身及其所有子域名:example.com 匹配 example.com 和 www.example.com,不匹配 notexample.com。katana 会把每一项转为小写。匹配对象是客户端请求的域名,以及嗅探到的 TLS SNI 或 HTTP Host。不要写开头的点:.example.com 什么都匹配不到。
cidrarray of strings否[]目标 IP 段,IPv4 或 IPv6 均可,例如 192.0.2.0/24 或 2001:db8::/32。只写地址表示单个主机。解析是严格的:主机位不为零(192.0.2.1/24)或前缀长度超出范围都会报错 invalid cidr "<value>": <reason>。只匹配客户端直接用 IP 指定的目标;katana 在路由之前不解析域名。
portarray of strings否[]目标端口,写成字符串:单个端口 "443",或闭区间 "6881-6889"。数字两边的空格会被忽略。写成 TOML 整数(例如 80)会解析失败,报 invalid type: integer,后面跟着 expected a string。不是合法端口的值报 invalid port spec: "<value>";下界大于上界的区间报 invalid port spec: "<value>" has a lower bound above its upper bound。
geositearray of strings否[][node.route].geosite 文件中的条目:写 code,或写 code@attr 只保留带该属性的域名,例如 category-ads-all、google@ads。代码和属性都不区分大小写。和 domain_suffix 一样,匹配请求的域名和嗅探到的域名。文件中没有的代码报错 geosite code not found: <code>;没有任何域名带有的属性不会报错,但什么都匹配不到。
geoiparray of strings否[][node.route].geoip 文件中的条目:写 code,例如 private、cn;或者写 !code,匹配该列表之外的所有 IP。代码不区分大小写。和 cidr 一样,只匹配用 IP 指定的目标,!code 也不会匹配域名目标。文件中没有的代码报错 geoip code not found: <code>。
  • 第一条匹配的规则生效。 katana 按规则在文件中出现的顺序逐条检查,遇到第一条匹配的规则就停止。请把范围窄的规则放在范围宽的规则之前。
  • 同一条规则内的匹配条件是“或”关系。 只要任一键的任一条目匹配,整条规则就匹配。domain_suffix = ["example.com"] 加上 port = ["443"],会匹配所有访问 example.com 的流(不论端口),以及所有访问任意地址 443 端口的流。
  • 没有“与”关系。 一条规则无法表达“这个域名,并且只在这个端口”。
  • 没有任何匹配条件的规则永远不匹配。 只设置了 outbound 的规则能通过 --test,但不起任何作用。需要兜底规则时请用 [node.route].default。

目标地址要么是域名,要么是 IP 地址,取决于客户端如何指定它。katana 在路由之前不解析域名;域名稍后由选中的出站去解析(如果需要的话)。因此每种匹配条件只能看到一种目标地址:

匹配条件 匹配对象 大小写 能否看到嗅探到的域名
domain_suffix 请求的域名 两边都转为小写 能
geosite 请求的域名 代码和属性不区分大小写 能
cidr 目标 IP 不适用 不能
geoip 目标 IP 代码不区分大小写 不能
port 目标端口 不适用 不适用

一个条目匹配该域名本身及其所有子域名,并且按标签边界匹配:example.com 匹配 example.com 和 cdn.example.com,但不匹配 badexample.com。请写不带前缀的域名。以点开头的写法(如 .example.com)什么都匹配不到。

条目是 IPv4 或 IPv6 网段。只写地址(如 192.0.2.10)表示单个主机。katana 会拒绝主机位不为零的网段,避免笔误悄悄变成另一个范围:

configuration error: invalid cidr "192.0.2.1/24": host part of address was not zero

端口写成字符串,因为同一个键既要容纳单个端口也要容纳范围:"443" 或 "6881-6889",两端都包含在内。数组中写整数会导致 TOML 解析失败,下界大于上界的范围会导致构建失败:

configuration error: invalid port spec: "90-80" has a lower bound above its upper bound

geosite 条目指定 [node.route].geosite 文件中的列表名。公开的 v2fly domain list 可以构建出兼容的文件(dlc.dat)。

  • code 匹配该列表中的所有域名,例如 category-ads-all。
  • code@attr 只保留带有该属性的域名,例如 google@ads。每个条目只能写一个属性。
  • 列表中可以包含 suffix、full、keyword 和 regex 四类条目,katana 全部支持。文件中无法编译的 regex 条目会被跳过,并在日志中记录一条警告。

未知的代码会导致构建失败,报 geosite code not found: <code>。列表中没有任何域名带有的属性不算错误:该条目会被接受,但什么都匹配不到。如果这样的规则始终不生效,请到列表的源文件中核对属性名。

geoip 条目指定 [node.route].geoip 文件中的列表名,例如公开的 v2fly geoip 发布文件。

  • code 匹配该列表中的所有 IP,例如 private 或 cn。
  • !code 匹配不在该列表中的所有 IP。它仍然只匹配 IP 目标。

未知的代码会导致构建失败,报 geoip code not found: <code>,其中的代码不带 !。

内核支持但 katana 未开放的匹配条件

Section titled “内核支持但 katana 未开放的匹配条件”

katana 使用的路由引擎还支持 domain_keyword、domain_full、domain_regex、source_cidr、network 和 inbound_tag。katana 的配置不接受这些键,写了会解析失败并报 unknown field。etemenanki-app 开放了全部这些键,见 etemenanki-app 指南中的路由。在 katana 中,使用 keyword、full 或 regex 域名匹配的唯一途径是 geosite 列表。

客户端常常连接的是 IP 地址,而它实际想访问的是某个域名:域名已在客户端侧解析过,或者应用本身就直接拨 IP。仅凭目标地址,这样的流永远无法匹配域名规则。嗅探从客户端发送的首批字节中找回域名,让 domain_suffix 和 geosite 规则仍然能匹配它。

属性 值
默认 开启;在 [node.controller] 中设置 disable_sniffing = true 可关闭
嗅探哪些流 目标为 IP 地址的 TCP 流;已经指定域名的流直接按该域名路由,不做等待
读取什么 TLS ClientHello 中的 SNI,其次是 HTTP 请求的主机名(绝对形式的请求 URI,否则取 Host 头)
预算 最多收集前 4 KiB,最长等待 300 ms;找到域名、收满 4 KiB 或等满 300 ms 时流立即建立。mux 子流只从打开它的那一帧中的字节嗅探,不做等待
使用者 只有 domain_suffix 和 geosite,作为请求目标之外的补充

嗅探只是路由提示,别无他用:

  • katana 仍然拨号客户端请求的 IP 地址,嗅探到的域名从不替换目标地址。
  • 审计规则匹配的是客户端指定的目标地址,而不是嗅探到的域名。
  • SNI 或 Host 中的 IP 字面量会被忽略,含有 DNS 名称不允许字符的名称也会被忽略。
  • 既不是 TLS 也不是 HTTP 的载荷,或者到得太晚的载荷,不会产生嗅探结果;这条流按目标地址路由,与关闭嗅探时相同。嗅探永远不会导致流失败。

在运行中的节点上修改 disable_sniffing 会立即重建该节点的监听器,这会断开该节点上的所有连接。新监听器接受的每条流都按新值处理。

TCP 和 UDP 以不同的形式到达路由器,因此屏蔽的表现也不同:

TCP 流 UDP 关联
路由 流建立时一次 逐包进行,按每个包自己的目标地址
嗅探 是,目标为 IP 时 从不
被路由或审计规则屏蔽 在任何拨号之前拒绝 丢弃该包,关联保持打开
被屏蔽时的计费 无 被丢弃的包不计费
出站无法承载 UDP 不适用 丢弃该包

UDP 关联没有单一的目标地址,因为每个数据报都带有自己的目标。因此 katana 在每个包发出时分别路由、审计和计费,一个关联可以同时与多个出站通信。关联每用到一个出站就为它打开一条链路,最多同时保持 64 条,需要新链路时关闭最久未使用的那条。出站链路打开失败时,对应的包会被丢弃,因此一个不可达的上游不会阻塞整个关联。

http 和 Shadowsocks 出站不承载数据报。路由到它们的 UDP 包会被丢弃,所以你在意的 UDP 流量请路由到 direct、SOCKS、VMess、VLESS 或 WireGuard。

下面的片段只展示与各场景相关的表,并不是完整文件。其中的路径、地址、密钥和 tag 都是占位值。

[node.route]
geosite = "/etc/katana/geosite.dat"
[[node.route.rule]]
outbound = "block"
geosite = ["category-ads-all"]

借助嗅探,客户端即使用 IP 连接广告服务器,只要它发送的 TLS SNI 或 HTTP Host 是列表中的域名,这条规则也能拦住它。

在文件顶层定义一次 WireGuard 出站,然后在任意节点中路由到它的 tag:

[[outbound]]
tag = "wg"
protocol = "wireguard"
server = "203.0.113.10"
port = 51820
private_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" # 生成方式:wg genkey
public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" # 对端的公钥
local_address = ["10.8.0.2/32"]
# ... [[node]], [node.api] ...
[node.route]
geosite = "/etc/katana/geosite.dat"
[[node.route.rule]]
outbound = "wg"
geosite = ["netflix"]
domain_suffix = ["video.example.com"]

其余流量仍然走 direct。WireGuard 的所有配置项见出站。

当用户用 IP 指定目标时,阻止他们访问本机所在网络、回环地址和链路本地地址:

[[node.route.rule]]
outbound = "block"
cidr = [
"127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"169.254.0.0/16", "100.64.0.0/10",
"::1/128", "fc00::/7", "fe80::/10",
]

两种写法都只匹配客户端用 IP 指定的目标,原因见各匹配条件检查什么。

顺序很重要:屏蔽规则放在最前面,这样即使后面的某条规则也能匹配,内网地址仍会被拒绝。

/etc/katana/config.toml
[[outbound]]
tag = "wg"
protocol = "wireguard"
server = "203.0.113.10"
port = 51820
private_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
local_address = ["10.8.0.2/32"]
[[node]]
panel_type = "NewV2board"
[node.api]
host = "https://panel.example.com"
node_id = 1
key = "replace-with-the-panel-key"
node_type = "V2ray"
[node.route]
default = "direct"
geoip = "/etc/katana/geoip.dat"
geosite = "/etc/katana/geosite.dat"
[[node.route.rule]]
outbound = "block"
geosite = ["category-ads-all"]
[[node.route.rule]]
outbound = "block"
geoip = ["private"]
[[node.route.rule]]
outbound = "wg"
geosite = ["netflix"]
[[node.route.rule]]
outbound = "block"
port = ["25", "465", "587"]

katana 会监视自己的配置文件。保存对某个节点 [node.route] 的修改后,该节点会先编译新规则,再做任何改动。编译成功后,节点切换到新路由器,立即执行一次面板轮询,并重建自己的监听器,这会断开该节点上的所有连接。仍在启动中的节点同样先检查新规则,再保存它们,并立即重试启动。修改任意 [[outbound]] 会重新编译每个节点的路由器,并断开所有节点上的连接。各类修改的具体影响见热重载。

katana 比较的是配置,而不是 geodata 文件。在磁盘上用同一路径替换 geosite.dat 或 geoip.dat 不会触发任何路由器重新编译。节点在下一次编译路由器时才读取新文件:修改 [node.route]、修改 [[outbound]] 或重启时。

如果运行中节点修改后的路由无法编译,该节点会拒绝这次修改,并记录 node <id>: config edit refused, keeping the running one: <error>。它保留之前的规则和监听器,因此不会断开任何连接。该节点这次修改中的其他内容也都不会生效:修正路由后再次保存,其余修改才会应用。文件中的其他节点仍会应用各自的修改。

只有当 [[outbound]] 的修改破坏了你没有改动的规则时,才会出现 route rebuild failed, keeping current,例如删除了某条规则仍在引用的出站。此时节点继续用之前的规则和之前的出站路由,但仍会重建监听器,所以其连接会断开。如果新的 [[outbound]] 条目无法构建,katana 会记录 reload: bad outbounds, keeping current config,这次修改的内容一概不生效。

重载新增的节点,或者因面板身份变化而由 katana 重新启动的节点,必须先构建成功,katana 才会应用任何修改。如果其路由无法编译,katana 会记录 reload: node <node>: build router: <error>; keeping current config,其中 <node> 以面板类型、面板主机、节点 ID 标识该节点,在 NewV2board 和 V2board 上还包括 katana 请求的节点类型。这次重载的内容一概不生效,所有节点都按原样继续运行。

启动时,无法编译的路由只会让该节点无法启动:katana 记录 node <id>: build router: 及原因,并运行其他节点。如果没有任何节点能够启动,katana 以 no nodes could be started 退出。保存前请先运行 katana --test。

消息 原因 解决方法
route references unknown outbound tag: wg 某条规则的 outbound 或路由的 default 引用了出站池中不存在的 tag。 定义带有该 tag 的 [[outbound]],或修正拼写。tag 区分大小写。
a geosite matcher is used but no geosite file is configured 规则用到了 geosite,但 [node.route] 中没有 geosite 路径。 在该节点的 [node.route] 中加入 geosite = "/etc/katana/geosite.dat"。每个节点都需要单独设置路径。
a geoip matcher is used but no geoip file is configured 同上,针对 geoip。 加入 geoip 路径。
geosite code not found: <code> 文件中没有该名称的列表。geosite 不支持 ! 前缀。 对照文件的源列表核对代码。
geoip code not found: <code> 文件中没有该名称的列表。 核对代码;消息中显示的代码不带 !。
No such file or directory (os error 2) 引用的 geodata 路径不存在。消息中不会给出文件名。 检查你修改过的节点的 geoip 和 geosite 路径。
geosite decode: failed to decode Protobuf message: … 该文件不是 geosite 列表,例如把 geoip.dat 填进了 geosite 键。 对调路径,或下载正确的文件。
invalid cidr "192.0.2.1/24": host part of address was not zero cidr 条目的主机位不为零。 写成网络地址 192.0.2.0/24,或单个主机 192.0.2.1。
invalid port spec: "http" port 条目不是数字或范围。 使用 "80" 或 "8000-8100" 这样的数字。
invalid type: integer `80`, expected a string port 条目写成了 TOML 整数。 加上引号:port = ["80"]。
unknown field、expected one of … katana 不接受的键,例如 domain 或 domain_keyword,或者把 rule 写成了 rules。 使用本页列出的键。

规则始终不生效,通常是以下原因之一:前面的规则先匹配了;这是一条 IP 规则,而客户端指定的是域名;geosite 属性没有标记任何域名;或者域名条目以点开头。