从 Xray 迁移
etemenanki-app 沿用 Xray 的模型:入站接受客户端,路由器为每条流选择一个出站,出站再把流量转发出去。大多数协议的线上格式与 Xray 相同,所以 Xray 客户端可以连接 etemenanki-app 服务端,反之亦然。但配置文件是另一种写法:它用 TOML 而不是 JSON,键名是 snake_case,Xray 中的若干嵌套对象变成了扁平的键,还有一些 Xray 功能是有意不提供的。
如果你已经有一份能用的 Xray 配置,想在 etemenanki-app 上运行同样的服务,这一页就是为你准备的。它把 Xray 的各种常见写法对应到 TOML,指出默认值不同的地方,列出没有对应项的功能,并说明哪些组合经过了与真实 Xray 程序的对测。katana 是基于同一内核的面板节点 agent,它的节点设置来自面板而不是文件,所以本页只适用于 etemenanki-app。
| Xray JSON | etemenanki-app TOML | 说明 |
|---|---|---|
log |
[log],只有一个键 level |
日志输出到标准输出。没有访问日志。 |
inbounds[] |
[[inbound]] |
每个入站对应一个表数组条目。 |
outbounds[] |
[[outbound]] |
与 Xray 一样,第一个出站就是默认路由。 |
inbounds[].streamSettings |
[inbound.stream] |
出站为 [outbound.stream]。共四种 network:tcp、tls、ws、grpc。 |
inbounds[].sniffing |
入站上的 sniffing = true |
布尔值。默认开启,且从不改写目标地址。 |
routing.rules[] |
[[route.rule]] |
首个匹配的规则生效,但同一条规则内的条件是 OR 而不是 AND。 |
routing.domainStrategy |
无 | 路由器从不解析域名,行为相当于 "AsIs"。 |
routing.balancers[] |
顶层的 [[balancer]] |
健康状态来自内置的 TCP 探测,而不是 observatory。 |
dns |
[dns] |
只有一个上游解析器,不能按域名分配服务器,也没有 hosts。 |
policy |
无 | 超时时间是固定的。唯一可以设置的连接上限在 hysteria2 和 tun 入站上,见上限。 |
observatory、burstObservatory |
无 | 每个负载均衡器自行探测其成员。 |
stats、api、metrics |
无 | 没有流量计数器,也没有控制 API。 |
fakedns、reverse、transport |
无 | |
多个配置文件、-confdir |
一个 TOML 文件,用 -c 指定 |
不带 -c 时读取工作目录下的 config.toml。 |
xray run -test -c config.json |
etemenanki-app --test -c config.toml |
检查所有内容,读取所有引用的文件,但不绑定任何端口。 |
TOML 文件中的每个表都拒绝未知键。从 Xray 照搬而没有转换的键不会被忽略,而是让配置报错停止,错误信息会指出这个键并列出可接受的键。唯一的例外是 freedom 和 blackhole 上的 [outbound.settings],见下文。
会改变行为的差异
Section titled “会改变行为的差异”大多数转换错误都会让 --test 报错。下面这些则不会,或者报出的错误容易误读。切换流量之前,请逐条检查。
-
入站默认监听回环地址。 Xray 在没有
listen时绑定0.0.0.0,etemenanki-app 则绑定127.0.0.1。因此需要从外部访问的服务端必须显式写出listen = "0.0.0.0"(或某个具体地址)。 -
TCP 上的 TLS 写作
network = "tls"。 Xray 的"network": "tcp"加"security": "tls"会被拒绝,而不是被转换。security = "tls"只用于在ws或grpc下面加一层 TLS。 -
同一条规则内的条件是 OR。 Xray 只有在每个字段都匹配时(domain AND port AND network)才匹配一条规则。etemenanki-app 只要任意一个匹配条件满足就匹配该规则。一条带有
network = "udp"和port = ["443"]的规则会命中所有 UDP 流量和所有 443 端口的流量,而不仅仅是 QUIC。见规则内的条件是 OR 而非 AND。 -
嗅探默认开启,且只影响路由。 Xray 只有在
sniffing.enabled为 true 时才嗅探,而且除非设置了routeOnly,否则会把目标地址改写为嗅探到的域名。etemenanki-app 会嗅探每一条以 IP 为目标的 TCP 流,除非你设置sniffing = false;嗅探到的域名只用于路由,相当于 Xray 的"routeOnly": true。 -
freedom和blackhole忽略自己的settings。 这两个出站从不读取[outbound.settings],所以写在那里的 XraydomainStrategy、redirect、fragment和response能通过--test,但不起任何作用。请在出站上用address_family代替domainStrategy,其余几项没有对应项。 -
SOCKS 入站默认中继 UDP。 Xray 的 SOCKS 入站默认关闭
udp,需要手动开启;etemenanki-app 则默认开启,除非你写udp = false。 -
"warning"不是日志级别。 Xray 的默认级别是"warning"。在 etemenanki-app 中,这个词会被当作日志 target 名称,结果所有日志行都被静默,包括说明--test为何失败的那条错误。请写level = "warn"。
服务端转换示例
Section titled “服务端转换示例”下面是一份典型的 Xray 服务端配置:443 端口上的 VLESS over WebSocket 和 TLS、一个直连出口,以及两条拦截规则。两个标签页分别是 Xray 原文件和 etemenanki-app 的转换结果。只要证书和 geodata 文件存在于给定路径,这份 TOML 就能通过 --test。
{ "log": { "loglevel": "warning" }, "inbounds": [ { "tag": "vless-ws-in", "listen": "0.0.0.0", "port": 443, "protocol": "vless", "settings": { "clients": [ { "id": "11111111-2222-3333-4444-555555555555", "email": "alice@example.com" }, { "id": "11111111-2222-3333-4444-666666666666", "email": "bob@example.com" } ], "decryption": "none" }, "streamSettings": { "network": "ws", "security": "tls", "tlsSettings": { "certificates": [ { "certificateFile": "/usr/local/etc/xray/fullchain.pem", "keyFile": "/usr/local/etc/xray/privkey.pem" } ] }, "wsSettings": { "path": "/ray", "host": "proxy.example.com" } }, "sniffing": { "enabled": true, "destOverride": ["http", "tls"], "routeOnly": true } } ], "outbounds": [ { "tag": "direct", "protocol": "freedom" }, { "tag": "block", "protocol": "blackhole" } ], "routing": { "domainStrategy": "AsIs", "rules": [ { "type": "field", "ip": ["geoip:private"], "outboundTag": "block" }, { "type": "field", "domain": ["geosite:category-ads-all"], "outboundTag": "block" } ] }}[log]level = "warn" # Xray 的 "warning" 在这里写作 "warn"
[[inbound]]tag = "vless-ws-in"protocol = "vless"listen = "0.0.0.0" # 默认是 127.0.0.1port = 443sniffing = true # 默认值;只用于路由,相当于 "routeOnly": true
[inbound.stream]network = "ws"security = "tls"
[inbound.stream.ws]path = "/ray"host = "proxy.example.com"
[inbound.stream.tls]cert_file = "/etc/etemenanki/fullchain.pem"key_file = "/etc/etemenanki/privkey.pem"
[inbound.settings]users = [ { id = "11111111-2222-3333-4444-555555555555" }, { id = "11111111-2222-3333-4444-666666666666" },]
[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "block"protocol = "blackhole"
[route]default = "direct"geoip = "/etc/etemenanki/geoip.dat"geosite = "/etc/etemenanki/geosite.dat"
[[route.rule]]outbound = "block"geoip = ["private"]
[[route.rule]]outbound = "block"geosite = ["category-ads-all"]从上到下,改动如下:
loglevel变成level,"warning"变成"warn"。streamSettings变成[inbound.stream]表,WebSocket 和 TLS 设置分别放在它的ws和tls子表中。certificates[0]变成cert_file和key_file两个键。clients变成users。email标签被去掉了,因为这里的 VLESS 和 VMess 用户只带一个id。decryption也被去掉了,因为只有普通 VLESS 这一种模式。sniffing对象变成一个布尔值。destOverride没有对应项:总是尝试 TLS 和 HTTP,其他协议一律不嗅探。- geodata 文件在
[route]中写明了路径。Xray 会在其程序旁边寻找geoip.dat和geosite.dat;etemenanki-app 只读取你给出的路径,而且只在有规则用到时才读取。 geoip:和geosite:前缀移进了键名。"type": "field"和domainStrategy被去掉了。- 这里的
[route] default = "direct"是可选的,因为第一个出站本来就是默认路由。显式写出来,可以保证调整出站顺序后文件依然正确。
由于同一条规则内的匹配条件是 OR,这两条拦截规则也可以合并为一条:在一个 [[route.rule]] 中同时写 geoip = ["private"] 和 geosite = ["category-ads-all"]。包括证书和测试请求在内的完整步骤,见 VLESS over WebSocket 和 TLS 示例。
客户端转换示例
Section titled “客户端转换示例”同一服务的客户端:一个本地 SOCKS 端口,把所有流量发往上面的服务端。
{ "inbounds": [ { "tag": "socks-in", "listen": "127.0.0.1", "port": 1080, "protocol": "socks", "settings": { "auth": "noauth", "udp": true } } ], "outbounds": [ { "tag": "proxy", "protocol": "vless", "settings": { "vnext": [ { "address": "proxy.example.com", "port": 443, "users": [{ "id": "11111111-2222-3333-4444-555555555555", "encryption": "none" }] } ] }, "streamSettings": { "network": "ws", "security": "tls", "tlsSettings": { "serverName": "proxy.example.com" }, "wsSettings": { "path": "/ray", "host": "proxy.example.com" } } } ]}[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[inbound.settings]auth = "none" # Xray 的 "noauth"udp = true # 这里的默认值
[[outbound]]tag = "proxy"protocol = "vless"server = "proxy.example.com" # vnext[0].addressport = 443 # vnext[0].port
[outbound.stream]network = "ws"security = "tls"
[outbound.stream.ws]path = "/ray"host = "proxy.example.com" # 可选:依次回退到 server_name、server
[outbound.stream.tls]server_name = "proxy.example.com" # 可选:回退到 server
[outbound.settings]id = "11111111-2222-3333-4444-555555555555"vnext 数组没有了:server 和 port 直接写在 [[outbound]] 上,唯一用户的 id 移到 [outbound.settings]。一个出站只连接一台服务器。要把流量分散到多台服务器,请为每台服务器定义一个出站,再把它们放进一个负载均衡器。
入站通用字段
Section titled “入站通用字段”| Xray | etemenanki-app | 区别 |
|---|---|---|
tag(可选) |
tag(必填) |
在所有入站中必须唯一。 |
listen,默认 "0.0.0.0" |
listen,默认 "127.0.0.1" |
以 / 开头的值是 Unix socket 路径。抽象 socket(@name)会被拒绝。 |
port:一个数字、"10000-10010" 这样的范围,或一个列表 |
port:一个数字 |
范围和列表会产生类型错误。请每个端口定义一个入站。 |
protocol |
protocol |
见协议名称。 |
settings |
[inbound.settings] |
因协议而异,见用户与凭据。 |
streamSettings |
[inbound.stream] |
见传输层设置。 |
sniffing 对象 |
sniffing 布尔值,默认 true |
见嗅探。 |
allocate |
无 |
入站键的完整列表见入站。
Xray protocol |
入站 | 出站 |
|---|---|---|
socks |
socks(SOCKS4、4a 和 5) |
socks(SOCKS5) |
http |
http |
http(仅 CONNECT) |
vless |
vless |
vless |
vmess |
vmess |
vmess |
trojan |
trojan |
trojan |
shadowsocks |
shadowsocks,仅 TCP |
shadowsocks,仅 TCP |
wireguard |
拒绝:wireguard cannot be used as an inbound (no server implementation) |
wireguard |
freedom |
不适用 | freedom,别名 direct |
blackhole |
不适用 | blackhole,别名 block |
dokodemo-door、tunnel |
拒绝:unknown protocol |
不适用 |
dns、loopback |
不适用 | 拒绝:unknown protocol |
hysteria(版本 2) |
hysteria2,别名 hysteria 和 hy2 |
同左 |
tun |
tun |
不适用 |
协议名区分大小写:"Freedom" 会被拒绝。
Hysteria 2 的配置结构与 Xray 不同。hysteria2 上的 [inbound.stream] 和 [outbound.stream] 会被拒绝,它的证书、TLS 和认证相关的键改为写在 [inbound.settings] 或 [outbound.settings] 中。见 Hysteria 2。
出站通用字段
Section titled “出站通用字段”| Xray | etemenanki-app | 区别 |
|---|---|---|
tag(可选) |
tag(必填) |
必须唯一,且不能与负载均衡器的 tag 相同。 |
protocol |
protocol |
|
settings.vnext[0].address、.port(VLESS、VMess) |
server、port |
直接写在 [[outbound]] 表上。每个出站只能有一台服务器。 |
settings.servers[0].address、.port(Trojan、Shadowsocks、SOCKS、HTTP) |
server、port |
同上。 |
streamSettings |
[outbound.stream] |
键与入站一侧相同。 |
mux |
无 | 作为未知字段被拒绝。出站为每条流单独建立一个连接。 |
sendThrough、proxySettings、streamSettings.sockopt |
无 | 作为未知字段被拒绝。不支持出站链式代理。 |
freedom 的 settings.domainStrategy |
出站上的 address_family |
见下表。 |
address_family 是与 Xray 在 freedom 上的 domainStrategy 最接近的对应项。除 blackhole 外的所有出站都支持它:在 freedom 上,它对目标的地址进行筛选和排序;在代理出站上,它对 server 域名做同样的处理。
Xray freedom domainStrategy |
address_family |
|---|---|
AsIs、UseIP、ForceIP |
"auto"(默认值) |
UseIPv4、ForceIPv4 |
"ipv4_only" |
UseIPv6、ForceIPv6 |
"ipv6_only" |
UseIPv4v6、ForceIPv4v6 |
"prefer_ipv4" |
UseIPv6v4、ForceIPv6v4 |
"prefer_ipv6" |
域名始终由 [dns] 中的解析器解析,因此 Xray 的 AsIs(系统解析器)和 UseIP(内置 DNS)在这里没有区别。可接受的写法和细节见出站。
| 协议与方向 | Xray | etemenanki-app | 去掉或拒绝的字段 |
|---|---|---|---|
| VLESS 入站 | settings.clients[].id |
users = [{ id = "…" }] |
flow、email、level、decryption、fallbacks |
| VLESS 出站 | vnext[0].users[0].id |
id |
flow、encryption |
| VMess 入站 | settings.clients[].id |
users = [{ id = "…" }] |
alterId、email、level |
| VMess 出站 | vnext[0].users[0].id、.security |
id、security |
alterId |
| Trojan 入站 | settings.clients[].password、.email |
users = [{ password = "…", email = "…" }] |
level、flow、fallbacks |
| Trojan 出站 | servers[0].password |
password |
|
| Shadowsocks 入站 | settings.method、.password、.clients[] |
method、password、users(也接受 clients 这个名字) |
network、level |
| Shadowsocks 出站 | servers[0].method、.password |
method、password |
uot、level |
| SOCKS 入站 | auth = "noauth" 或 "password"、accounts[]、udp、ip |
auth = "none" 或 "password"、accounts、udp、udp_bind |
userLevel |
| HTTP 入站 | accounts[]、allowTransparent |
accounts、allow_transparent |
timeout、userLevel |
| SOCKS 和 HTTP 出站 | servers[0].users[0].user、.pass |
user、pass |
有些取值规则与 Xray 不同:
- UUID 必须是真正的 UUID。 Xray 会把
"my-custom-id"这样的任意短字符串转换成 UUID,etemenanki-app 则以invalid uuid "my-custom-id"拒绝它。请为这类用户分配正式的 UUID,并相应更新客户端。 - VMess 只支持 AEAD。
alterId = 0不需要替代项,直接删除即可。使用大于 0 的alterId的客户端无法连接。在出站上,security = "auto"始终表示 AES-128-GCM,"none"和"zero"会以unknown vmess security被拒绝。入站只接受 AES-128-GCM 和 ChaCha20-Poly1305 加密的数据,因此设置为"none"或"zero"的 Xray 客户端无法连接。 - Shadowsocks 不支持 UDP。 两侧都只承载 TCP,也没有流加密方式。2022 系列加密方法的密钥写法与 Xray 相同:服务端 PSK 写在
password中,每个用户的 PSK 写在users[].password中,连接多用户服务端时,出站的password写作"server-psk:user-psk"。
详见各协议页面:VLESS、VMess、Trojan、Shadowsocks、SOCKS 和 HTTP。
WireGuard 出站
Section titled “WireGuard 出站”Xray settings |
[outbound.settings] |
区别 |
|---|---|---|
secretKey |
private_key |
Base64 或十六进制,32 字节。 |
address,写成 "10.0.0.2/32" 这样的 CIDR |
address,写成 "10.0.0.2" 这样的纯 IP |
带前缀长度会以 invalid IP address syntax 被拒绝。 |
peers[0].publicKey |
peer_public_key |
只支持一个 peer。 |
peers[0].preSharedKey |
preshared_key |
|
peers[0].endpoint |
endpoint |
host:port,用系统解析器解析。 |
peers[0].keepAlive |
keepalive |
单位为秒。 |
peers[0].allowedIPs |
无 | 路由到该出站的所有流量都进入隧道。 |
mtu |
mtu |
默认 1420。 |
reserved |
reserved |
必须恰好三个字节。 |
domainStrategy |
出站上的 address_family |
作用于隧道内的目标地址。 |
noKernelTun |
无 | 隧道始终运行在用户态。 |
其余内容见 WireGuard。
streamSettings 变成 [inbound.stream] 或 [outbound.stream] 表。network 与 security 的组合按下表转换:
Xray network + security |
[.stream] network + security |
|---|---|
tcp 或 raw,无 security |
省略 [.stream],或写 network = "tcp" |
tcp 或 raw + tls |
network = "tls",省略 security |
ws,无 security |
network = "ws" |
ws + tls |
network = "ws"、security = "tls" |
grpc,无 security |
network = "grpc" |
grpc + tls |
network = "grpc"、security = "tls" |
任意 + reality |
不支持:unknown stream security "reality" (expected "tls" or "none") |
httpupgrade、splithttp、xhttp、h2、http、kcp、quic |
不支持:unknown stream network "…" |
各传输方式的设置对象对应到子表:
| Xray | etemenanki-app | 说明 |
|---|---|---|
wsSettings.path |
[.stream.ws] path |
默认 /。缺少开头的 / 时会自动补上。与 Xray 一样,路径中的 ?ed=2048 会在出站上开启 early data;入站接受任何客户端的 early data。 |
wsSettings.host,或 wsSettings.headers.Host |
[.stream.ws] host |
在入站上,Host 不同的请求会得到 404。在出站上,依次回退到 tls.server_name、server。 |
wsSettings.headers(其他请求头)、heartbeatPeriod、acceptProxyProtocol |
无 | |
grpcSettings.serviceName |
[.stream.grpc] service_name |
必填。只支持普通的服务名形式:路径为 /<service_name>/Tun 和 /<service_name>/TunMulti。 |
grpcSettings.authority |
[.stream.grpc] authority |
仅出站。依次回退到 tls.server_name、server。 |
grpcSettings.multiMode |
无 | 入站两种模式都接受。出站始终使用默认的(Tun)模式,因此无论 Xray 服务端如何设置都能配合使用。 |
grpcSettings.idle_timeout、health_check_timeout、user_agent 等调优项 |
无 | 使用固定值。 |
tlsSettings.serverName |
[.stream.tls] server_name |
出站:用作 SNI,也是校验证书时使用的名称,默认为 server。在入站上被忽略。 |
tlsSettings.allowInsecure |
[.stream.tls] allow_insecure |
仅出站,在入站上被忽略。不能与 ca_file 同时使用。 |
tlsSettings.certificates[0].certificateFile、.keyFile |
[.stream.tls] cert_file、key_file |
仅入站。每个入站只有一对证书和私钥,均为 PEM 文件;证书文件可以包含完整证书链。不支持内联的 certificate 和 key 数组。 |
带 "usage": "verify" 的证书 |
[.stream.tls] ca_file |
仅出站,在入站上被忽略。该 CA 会被加入系统根证书集合。 |
alpn |
无 | 固定值:ws 为 http/1.1,grpc 为 h2,tls 不设置。 |
fingerprint、pinnedPeerCertSha256、minVersion、maxVersion、cipherSuites、rejectUnknownSni、ECH |
无 | 两侧都使用 TLS 1.2 或更高版本,加密套件设置固定。 |
tcpSettings.header(HTTP 伪装) |
无 | |
sockopt |
无 |
传输层设置适用于 http、trojan、vless 和 vmess 入站,以及 socks、http、trojan、vless、vmess 和 shadowsocks 出站。每个键的说明见传输层。
每条 Xray 规则变成一个 [[route.rule]] 表。无论目标是出站还是负载均衡器,都写成 outbound;每个条件变成一个或多个匹配条件键。
| Xray 规则字段 | [[route.rule]] 键 |
说明 |
|---|---|---|
outboundTag |
outbound |
|
balancerTag |
outbound |
负载均衡器与出站共用同一套 tag。 |
domain(以及 domains) |
domain_suffix、domain_keyword、domain_full、domain_regex、geosite |
按前缀拆分,见下表。 |
ip |
cidr、geoip |
纯 IP 和 CIDR 写入 cidr,geoip: 条目写入 geoip。 |
port |
port |
字符串数组,每项是一个端口或一个范围:"53,443,1000-2000" 变成 ["53", "443", "1000-2000"]。 |
network |
network |
只能有一个值,"tcp" 或 "udp"。若是 "tcp,udp",省略该键。 |
source、sourceIP |
source_cidr |
客户端的地址。 |
inboundTag |
inbound_tag |
|
type: "field"、ruleTag |
无 | 省略即可。 |
sourcePort、localIP、localPort、user、protocol、attrs、process、vlessRoute、webhook |
无 | 未知字段。 |
域名条目按前缀拆分:
Xray domain 条目 |
etemenanki-app | 匹配范围 |
|---|---|---|
"domain:example.com" |
domain_suffix = ["example.com"] |
example.com 及其所有子域名,不包括 notexample.com |
"full:example.com" |
domain_full = ["example.com"] |
仅 example.com 本身 |
"keyword:example" |
domain_keyword = ["example"] |
包含该文本的任意域名 |
"example"(无前缀) |
domain_keyword = ["example"] |
同上:Xray 把不带前缀的字符串视为关键字 |
"regexp:\\.example\\.com$" |
domain_regex = ['\.example\.com$'] |
Rust regex 语法,与 Go 一样不支持环视(look-around) |
"geosite:cn" |
geosite = ["cn"] |
代码名不区分大小写 |
"geosite:google@ads" |
geosite = ["google@ads"] |
只匹配带有该属性的条目 |
"ext:file.dat:tag" |
无 | 每份配置只能有一个 geosite 文件 |
"dotless:" |
domain_regex = ['^[^.]*$'] |
需要自己写正则 |
IP 条目:
Xray ip 条目 |
etemenanki-app |
|---|---|
"10.0.0.0/8"、"192.0.2.1" |
cidr = ["10.0.0.0/8", "192.0.2.1"] |
"geoip:cn" |
geoip = ["cn"] |
"geoip:!cn" |
geoip = ["!cn"] |
"geoip:private" |
geoip = ["private"],你的 geoip.dat 中必须有这个代码 |
"ext:file.dat:tag" |
无 |
域名匹配条件按小写比较:domain_suffix、domain_keyword 和 domain_full 的值会被转成小写,被匹配的域名也是如此。domain_regex 的模式则不会,所以请用小写书写。cidr 的主机位必须为零:"10.0.0.1/8" 会以 invalid cidr "10.0.0.1/8": host part of address was not zero 失败,而 Xray 会接受它。
geodata 文件与 Xray 使用的 v2ray 格式 geoip.dat 和 geosite.dat 相同。在 [route] 中用 geoip 和 geosite 设置它们的路径。只有当某条规则用到文件时才会读取它;如果规则用到了文件却没有配置路径,会以 a geosite matcher is used but no geosite file is configured 失败。
规则内的条件是 OR 而非 AND
Section titled “规则内的条件是 OR 而非 AND”在 Xray 中,带有多个字段的规则只有在所有字段都匹配时才匹配。在 etemenanki-app 中,只要规则中任意一个匹配条件匹配(无论它在哪个键下),整条规则就匹配。与 Xray 一样,规则仍然按顺序尝试,首个匹配的规则生效。
下面这条 Xray 规则只拦截 QUIC:
{ "type": "field", "network": "udp", "port": "443", "outboundTag": "block" }逐键转换后,它会拦截所有 UDP 流量,以及所有发往 443 端口的 TCP 流量:
[[route.rule]]outbound = "block"network = "udp" # 匹配每一条 UDP 流……port = ["443"] # ……并且另外匹配每一条发往 443 端口的流这样的组合条件无法表达。转换带有多个字段的规则之前,先判断每个字段单独生效是否可行,再拆分规则或删去字段,直到满足要求。只有一个字段、或者一个字段中有多个值的规则,可以原样转换。
没有 domainStrategy
Section titled “没有 domainStrategy”etemenanki-app 的路由器从不解析域名。它的行为相当于 Xray 的 "domainStrategy": "AsIs",没有 IPIfNonMatch 或 IPOnDemand:
cidr或geoip匹配条件永远不会匹配以域名为目标的流。- 域名匹配条件(包括
geosite)永远不会匹配以 IP 为目标的流,除非嗅探为它找回了域名。
flowchart TB
F["流到达"] --> D{"目标是 IP 吗?"}
D -- "否,是域名" --> R["按顺序检查规则:域名匹配条件看到该域名"]
D -- "是" --> S{"是 TCP,且 sniffing = true?"}
S -- "是" --> N["读取 TLS SNI 或 HTTP Host"]
S -- "否" --> I["按顺序检查规则:IP 匹配条件看到该地址"]
N --> I2["按顺序检查规则:IP 匹配条件看到该地址,域名匹配条件看到嗅探到的域名"]
R --> O["第一条有任一匹配条件命中的规则生效"]
I --> O
I2 --> O
O -- "都不匹配" --> DEF["route.default"]
如果你的 Xray 配置用 IPIfNonMatch 来按 IP(geoip:cn)把国内网站直连,请把同一规则的域名一侧也加上(geosite = ["cn"]),这样以域名为目标的流也能匹配。
Xray sniffing |
etemenanki-app |
|---|---|
enabled |
入站上的 sniffing = true 或 false,默认为 true。 |
destOverride |
无。总是尝试 TLS SNI 和 HTTP Host;不嗅探 QUIC、fakedns 和 BitTorrent。 |
routeOnly |
实际上始终开启:嗅探到的域名用于路由,流仍然发往原始地址。 |
metadataOnly、domainsExcluded、ipsExcluded |
无 |
只有目标是 IP 地址的 TCP 流才会被嗅探,最多等待 300 毫秒、读取首批载荷的 4 KiB。以域名为目标的流直接按该域名路由,不需要等待。见入站和路由。
Xray 的负载均衡器位于 routing 之内,依赖 observatory 获取健康状态。在 etemenanki-app 中,[[balancer]] 是一个顶层表,自行探测它的成员。
{ "routing": { "balancers": [ { "tag": "auto", "selector": ["proxy-"], "strategy": { "type": "leastPing" }, "fallbackTag": "direct" } ], "rules": [{ "type": "field", "network": "tcp,udp", "balancerTag": "auto" }] }, "observatory": { "subjectSelector": ["proxy-"], "probeUrl": "https://www.gstatic.com/generate_204", "probeInterval": "1m" }}[[balancer]]tag = "auto"outbounds = ["proxy-a", "proxy-b"] # 精确的 tag,按优先级排列strategy = "failover" # 或 "round_robin"probe_interval = 60 # 秒probe_timeout = 5 # 秒
[route]default = "auto"| Xray | etemenanki-app |
|---|---|
selector(tag 前缀) |
outbounds,精确出站 tag 的列表。其他负载均衡器不能作为成员。 |
strategy.type = "random"(默认值)、"roundRobin"、"leastPing"、"leastLoad" |
strategy = "failover"(默认值:按列表顺序选第一个健康的成员)或 "round_robin"。其他值会以 unknown balancer strategy 失败。 |
fallbackTag |
无。所有成员都不可用时,使用第一个成员。 |
observatory.probeUrl、probeInterval |
每隔 probe_interval 秒(默认 30)向每个成员的 server 和 port 发起一次 TCP 连接,超时为 probe_timeout(默认 5)。 |
由于探测是向 server 和 port 发起 TCP 连接,只有上游是 TCP 的出站才能作为成员。freedom、blackhole 和 wireguard 没有 server,hysteria2 监听的是 UDP,所以这四种都会以 balancer auto: outbound direct has no upstream a TCP health probe can reach, so it cannot be balanced 被拒绝。见负载均衡器和故障转移示例。
Xray 的 dns 对象列出多个服务器,并可以按域名在其中选择。etemenanki-app 的 [dns] 只配置一个上游,代理自己需要解析的域名几乎都交给它:freedom 的目标、代理出站的 server 名称、WireGuard 隧道内的目标,以及负载均衡器的探测。唯一的例外是 WireGuard 的 endpoint,它始终交给主机解析器。在默认的 backend = "system" 下,[dns] 本身就是主机解析器,会遵循 /etc/hosts。
Xray dns.servers 条目 |
[dns] |
|---|---|
"localhost" |
backend = "system",默认值 |
"1.1.1.1" |
backend = "udp"、server = "1.1.1.1:53" |
"https://cloudflare-dns.com/dns-query" |
backend = "https"、server = "1.1.1.1:443"、url = "https://cloudflare-dns.com/dns-query" |
"tcp://…"、"quic+local://…"、"fakedns" |
无 |
| — | backend = "tls"、server = "1.1.1.1:853"、server_name = "cloudflare-dns.com"(DNS over TLS) |
server 始终是带端口的 IP 地址:只写 "1.1.1.1" 会以 dns: invalid server address: invalid socket address syntax 失败。hosts、按服务器设置的 domains 和 expectIPs、queryStrategy、clientIp 以及 disableCache 都没有对应项。查询结果总是会被缓存。见 DNS。
不支持的功能
Section titled “不支持的功能”下列 Xray 功能没有对应项。其中大多数会明确报错,所以 --test 会帮你找出来。标为“接受但忽略”的几行则不会报错,需要你自己在配置中搜索。
| Xray 功能 | 在 etemenanki-app 中的结果 |
|---|---|
XTLS flow,包括 xtls-rprx-vision |
两侧都是未知字段。仍然发送 flow 的客户端会被断开。 |
| REALITY | unknown stream security "reality"。请改用证书和 tls。 |
httpupgrade、splithttp、xhttp 传输方式 |
unknown stream network。h2、http、kcp 和 quic 也一样。 |
出站上的客户端 mux |
未知字段。VLESS、VMess 和 Trojan 入站无需任何设置即可接受来自 Xray 客户端的 mux.cool(以及其中的 XUDP)。 |
VLESS 和 Trojan 上的 fallbacks |
未知字段。认证失败的连接会被关闭。 |
Hysteria 2 的 Brutal 拥塞控制(up 和 down 带宽) |
没有带宽相关的键。连接以“速率未知”协商,由拥塞控制决定速率。 |
freedom 的 domainStrategy 或 targetStrategy、redirect、fragment、noises |
接受但忽略:freedom 从不读取自己的 settings。策略请用 address_family 设置。 |
blackhole 的 response |
接受但忽略。被拦截的流收不到任何数据,也没有 HTTP 403 页面。 |
由 observatory 或 burstObservatory 驱动的负载均衡器 |
未知顶层字段。每个负载均衡器运行自己的 TCP 探测。 |
leastPing、leastLoad 和 random 负载均衡策略 |
unknown balancer strategy。Xray 的 roundRobin 在这里写作 round_robin。 |
stats、api、metrics、按用户的流量计数器 |
未知顶层字段。katana 为面板节点提供按用户的流量计费。 |
policy 等级以及用户上的 level |
未知字段。超时时间是固定的。 |
dokodemo-door 入站,dns 和 loopback 出站 |
unknown protocol。 |
| WireGuard 入站 | 被拒绝:没有服务端实现。 |
fakedns、reverse、出站链式代理(proxySettings、dialerProxy) |
未知字段。 |
uTLS fingerprint、证书固定(pinning)、ECH |
未知字段。 |
入站端口范围、allocate |
类型错误或未知字段。 |
VLESS 和 VMess 用户上的 email |
未知字段。Trojan、Shadowsocks 和 Hysteria 2 的用户可以带 email。 |
| Shadowsocks over UDP | 两侧都不支持。 |
与 Xray 的互通测试
Section titled “与 Xray 的互通测试”项目的集成测试让 etemenanki-app 与真实的 xray-core 程序对接,让流量同时经过两者,并比对返回的字节。
| 组合 | etemenanki-app 作客户端,Xray 作服务端 | Xray 作客户端,etemenanki-app 作服务端 |
|---|---|---|
VLESS over TLS(network = "tls",Xray tcp + tls) |
已测试 | 已测试 |
| VLESS over WebSocket,明文和 TLS | 已测试 | 已测试 |
| VLESS over gRPC,明文和 TLS | 已测试 | 已测试 |
| VMess over gRPC,带 TLS | 未测试 | 已测试 |
VMess over 明文 WebSocket,带 early data(path = "/vmess?ed=2048") |
已测试 | 已测试 |
| 来自 Xray 客户端的 mux.cool:VLESS over TCP 和 over WebSocket + TLS、Trojan over WebSocket + TLS | 不适用 | 已测试 |
| 来自 Xray 客户端的 mux.cool:VMess over TCP,包括一次 64 KiB 的上传,它同时被切分成多个 VMess chunk 和多个 8 KiB 的 mux 帧 | 不适用 | 已测试 |
| 经 VLESS 和 VMess 的 XUDP(mux 内的 UDP) | 不适用 | 已测试 |
| 经 VLESS 的 XUDP:一个 UDP 关联与两个对端通信,每个回复都归属到发出它的那个对端 | 不适用 | 已测试 |
嗅探来自 Xray 客户端、以 IP 为目标的 VLESS 流,并按 HTTP Host 或 TLS SNI 路由 |
不适用 | 已测试 |
不带 mux 的 Trojan、Shadowsocks、SOCKS、HTTP 和 WireGuard 没有与 Xray 程序对测。Hysteria 2 在两个方向上都与上游 Hysteria 客户端和服务端做了测试,见 Hysteria 2。
-
列出你的配置用到的 Xray 功能,与不支持的功能对照。如果依赖 REALITY、XTLS Vision、
xhttp或客户端 mux,这部分请继续使用 Xray,或者先修改客户端。 -
借助上面的表格逐节转换文件:入站、出站、传输层设置,然后是路由。保持 tag 不变,这样规则仍然指向正确的出站。
-
逐一检查带有多个条件的规则,按 OR 语义重写。
-
把
geoip.dat和geosite.dat复制到固定位置,并在[route]中设置它们的路径。请使用绝对路径:相对路径是相对于工作目录解析的,而不是相对于配置文件。 -
检查文件,反复修改直到通过。每次只报告第一个错误。
终端窗口 etemenanki-app --test -c /etc/etemenanki/config.toml -
在文件中搜索
listen、freedom或blackhole下的[outbound.settings],以及[log],与会改变行为的差异对照。这些是--test发现不了的错误。 -
在一个空闲端口上启动 etemenanki-app,让一个客户端连接它,确认流量能通,并且每条规则都把流量送到预期的地方。然后再把监听器移到生产端口。
以下是 Xray 使用习惯最常引发的错误。与某个具体条目相关的构建错误会带有 inbound <tag>: 或 outbound <tag>: 前缀。
| 错误 | Xray 习惯 | 修正 |
|---|---|---|
security = "tls" is not valid with network = "tcp"; … |
"network": "tcp", "security": "tls" |
network = "tls" |
unknown stream security "reality" (expected "tls" or "none") |
REALITY | 使用 security = "tls" 和证书 |
unknown stream network "xhttp" |
不支持的传输方式 | 使用 ws 或 grpc |
unknown field `mux`, expected one of `tag`, `protocol`, `server`, `port`, `stream`, `address_family`, `settings` |
出站上的 mux |
删除它 |
invalid settings: unknown field `clients`, expected `users` |
VLESS、VMess 或 Trojan 入站上的 clients |
改名为 users |
invalid settings: unknown field `flow`, expected `id` |
用户上的 XTLS Vision,或 email、level |
删除该键 |
invalid settings: unknown field `alterId`, expected `id` |
入站用户上的 VMess alterId |
删除它 |
outbound <tag>: missing server |
地址写在 settings 里的 vnext 或 servers 中 |
把地址移到出站上的 server 和 port |
invalid settings: unknown field `vnext`, expected `id` |
同上,但已经设置了 server |
删除 vnext,把用户的 id 直接写在 [outbound.settings] 中 |
unknown socks auth "noauth" |
Xray 对无认证的叫法 | auth = "none" |
位于 sniffing 的 invalid type: map, expected a boolean |
Xray 的 sniffing 对象 |
sniffing = true |
invalid type: string "10000-10010", expected u16 |
入站上的端口范围 | 每个端口一个入站 |
unknown field `outboundTag` |
规则中的 outboundTag 或 balancerTag |
outbound = "…" |
unknown field `domain` |
规则中的 domain 或 ip |
拆分为上文列出的匹配条件键 |
invalid rule network "tcp,udp" (expected "tcp" or "udp") |
一个字符串里写了两种 network | 省略 network |
invalid port spec: "53,443" |
逗号分隔的端口列表 | port = ["53", "443"] |
invalid type: integer `443`, expected a string |
规则 port 中写了数字 |
port = ["443"] |
geosite code not found: geosite:cn |
取值中保留了前缀 | geosite = ["cn"] |
unknown balancer strategy "leastPing" (expected "failover" or "round_robin") |
Xray 的策略名,包括 "roundRobin" |
"failover" 或 "round_robin" |
unknown field `selector` |
负载均衡器的 tag 前缀 | 在 outbounds 中列出精确的 tag |
unknown field `servers`, expected one of `backend`, `server`, `server_name`, `url`, `ca_file` |
Xray 的 DNS 服务器列表 | 只配置一个解析器,见 DNS |
unknown field `stats` (或 api、policy、observatory) |
Xray 的顶层配置段 | 删除它们 |
--test 没有任何输出,以状态码 1 退出 |
level = "warning" |
level = "warn"。运行 RUST_LOG=info etemenanki-app --test -c <file> 查看被隐藏的错误。 |