Trojan
Trojan 是一种以密码认证、设计上运行在 TLS 之内的代理协议。客户端建立连接后,先发送密码的哈希,再发送要访问的地址,此后连接上承载的就是该目标的数据。etemenanki-app 在两个方向上都实现了它:trojan 入站接受 Trojan 客户端,trojan 出站经由 Trojan 服务器转发。两者都承载 TCP 和 UDP。
当你要搭建 Trojan 服务器、让 etemenanki-app 连接已有的 Trojan 服务器,或者把 Trojan 配置从 Xray 迁移过来时,请参考本页。实现移植自 Xray 的 proxy/trojan,线路格式相同,同一个密码在两边的效果也一样。
最小的服务端和客户端
Section titled “最小的服务端和客户端”下面的服务端在所有网卡的 443 端口监听,用你的证书终结 TLS,接受两个用户,并把他们的流量直接发出。一条规则把发往私有、回环和链路本地地址的请求送到 blackhole。这条规则只匹配直接写明 IP 地址的请求:路由器在匹配 cidr 之前不会解析域名,所以对一个解析到私有地址的域名(例如 localhost)的请求仍会经 direct 发出。
# A Trojan server over TLS on port 443, with two users and a direct exit.# Replace the certificate paths and the passwords before you use it.
[log]level = "info"
[[inbound]]tag = "trojan-in"protocol = "trojan"listen = "0.0.0.0"port = 443
# Trojan sends the password hash in the clear: always put TLS under it.[inbound.stream]network = "tls"
[inbound.stream.tls]cert_file = "/etc/etemenanki/fullchain.pem"key_file = "/etc/etemenanki/privkey.pem"
[inbound.settings]users = [ { password = "replace-with-a-long-random-password", email = "alice@example.com" }, { password = "replace-with-another-long-random-password", email = "bob@example.com" },]
[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "block"protocol = "blackhole"
[route]default = "direct"
# Refuse requests for private, loopback and link-local addresses. This rule# only sees requests that name an IP: a domain is not resolved before routing.[[route.rule]]outbound = "block"cidr = [ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16", "fc00::/7", "fe80::/10", "::1/128",]对应的客户端在本地运行一个 SOCKS 代理,并把所有流量经服务端发出。server 是要拨号的地址;tls.server_name 是用来校验服务端证书的名称。当 server 本身就是这个名称时,可以省略 server_name。
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[[outbound]]tag = "trojan-out"protocol = "trojan"server = "203.0.113.10"port = 443
[outbound.stream]network = "tls"
[outbound.stream.tls]server_name = "proxy.example.com"
[outbound.settings]password = "replace-with-a-long-random-password"
[route]default = "trojan-out"启动前先用 etemenanki-app --test -c <file> 检查两个文件。生成密码可以运行 openssl rand -base64 32,直接使用其输出即可。
Trojan 入站接受通用的入站键(tag、protocol、listen、port、stream、sniffing 等,见入站),外加下面这个 [inbound.settings] 表:
| 键 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
users | array of tables | 否 | [] | 该入站接受的账户,每个用户一个表。列表为空或不写时 --test 仍能通过,但此后每个连接都会以 trojan: invalid user 被拒绝。没有顶层的 password 键;Xray 的 clients 和 fallbacks 键会作为未知字段导致配置失败。 |
users[].password | string | 是 | — | 用户的密钥。客户端发送 SHA224(password) 的小写十六进制值,服务端在用户中查找这个哈希。任何字符串都会被接受,包括空字符串,所以请使用足够长的随机值。如果两个用户密码相同,以排在后面的为准。flow 和 level 会作为未知字段被拒绝。 |
users[].email | string | 否 | "" | 用户的标签,作为用户名随该用户的每个流一起传递。它不会发送到线路上,也不必是电子邮件地址。etemenanki-app 中没有任何路由规则或流量计数器会读取它。katana 根据面板而不是这个表来构建用户,它会自行填写同一个标签,并用它把流量归属到对应的面板用户。 |
users 是一个表数组,两种 TOML 写法都可以:
[inbound.settings]users = [ { password = "replace-with-a-long-random-password", email = "alice@example.com" }, { password = "replace-with-another-long-random-password" },][[inbound.settings.users]]password = "replace-with-a-long-random-password"email = "alice@example.com"
[[inbound.settings.users]]password = "replace-with-another-long-random-password"etemenanki-app 在构建配置时对每个密码计算一次哈希。要增删用户,修改配置文件,由热重载生效即可。重载会替换整个 generation(一代实例),因此会断开所有进行中的连接,而不只是被修改用户的连接。
Trojan 出站需要通用的出站键 server 和 port,通常还需要一个 [outbound.stream] 块,见出站。它的 [outbound.settings] 表只有一个键:
| 键 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
password | string | 是 | — | 服务端用来识别你的密码。出站在它打开的每个连接开头发送 SHA224(password) 的小写十六进制值。缺少它时出站无法构建,并报出指明 password 的 invalid settings: missing field 错误。这是唯一的键,写入任何其他键(例如 email)都会报错。 |
缺少 server 时报错 outbound trojan-out: missing server,缺少 port 时报错 outbound trojan-out: missing port。该出站没有多路复用选项:它承载的每个 TCP 流都会单独建立一条到服务端的连接。
Trojan 出站有可供探测的 TCP 地址,因此可以作为负载均衡器的成员。
Trojan 可以运行在任意一种流式传输层上。请从下面的 TLS 形态中选择一种:
network |
security |
线路上的内容 | 额外的键 |
|---|---|---|---|
"tls" |
不需要 | TCP 上的 TLS。经典的 Trojan 部署方式,相当于 Xray 中的 network: "tcp" 加 security: "tls"。 |
无 |
"ws" |
"tls" |
TLS 内的 WebSocket | ws.path(默认 "/")、ws.host |
"grpc" |
"tls" |
TLS 内的 gRPC(HTTP/2) | grpc.service_name(必填) |
"tcp"(默认) |
无 | 明文 TCP。路径上的任何人都能看到密码哈希。 | 无 |
在入站上,每种 TLS 形态都需要 tls.cert_file 和 tls.key_file。在 ws 和 grpc 下,只有 security = "tls" 才会启用 TLS:没有它时,[stream.tls] 表会被接受但被忽略,传输层以明文运行。在出站上,默认用系统根证书校验服务端证书。设置 tls.ca_file 可改为信任私有 CA,设置 tls.allow_insecure = true 则跳过校验;两者不能同时使用。传输层页面介绍了所有键。
[inbound.stream]network = "ws"security = "tls"
[inbound.stream.ws]path = "/trojan"
[inbound.stream.tls]cert_file = "/etc/etemenanki/fullchain.pem"key_file = "/etc/etemenanki/privkey.pem"[outbound.stream]network = "ws"security = "tls"
[outbound.stream.ws]path = "/trojan"# tls.server_name 默认取 `server`;ws.host 默认取 tls.server_name,其次取 `server`。[outbound.stream]network = "grpc"security = "tls"
[outbound.stream.grpc]service_name = "trojan"# tls.server_name 默认取 `server`;grpc.authority 默认取 tls.server_name,其次取 `server`。监听 Unix socket
Section titled “监听 Unix socket”当 listen 是以 / 开头的路径时,入站会监听 Unix socket,并在其上使用明文 Trojan,例如放在一个负责终结 TLS 的本地代理后面。此时不要写 port;配合 socket 路径写了 port 会报错(a unix socket listen has no port; remove port)。Unix socket 不承载传输层,因此 [inbound.stream] 中除 tcp 以外的任何 network,以及除 "none" 以外的任何 security,都会被拒绝:
inbound trojan-in: protocol trojan over a unix socket does not support stream network "tls"Unix socket 不提供客户端地址,因此 source_cidr 规则永远不会匹配这些连接。
连接的处理过程
Section titled “连接的处理过程”每个 Trojan 连接就是一个请求。客户端最先发送的字节是请求头;服务端不作任何应答,要么开始中继,要么关闭连接。
flowchart TB
A["传输层已接受:TLS、ws 或 grpc"] --> B["读取请求头:哈希、命令、地址"]
B --> C{"哈希匹配某个用户?"}
C -- 否 --> X["关闭连接,不作应答"]
C -- 是 --> D{"命令"}
D -- "0x03 UDP" --> U["UDP 关联:每个数据包按自己的地址路由"]
D -- "0x01 CONNECT" --> E{"地址是 v1.mux.cool?"}
E -- 是 --> M["mux.cool 载体:每个子流分别路由"]
E -- 否 --> T["TCP 流:先路由,再中继"]
请求头在线路上的格式:
| 字段 | 长度 | 内容 |
|---|---|---|
| 用户哈希 | 56 字节 | SHA224(password) 的小写十六进制 |
| CRLF | 2 字节 | \r\n |
| 命令 | 1 字节 | 0x01 CONNECT,0x03 UDP ASSOCIATE |
| 地址 | 可变 | SOCKS5 编码:类型(0x01 IPv4,0x03 域名,0x04 IPv6)、地址、端口 |
| CRLF | 2 字节 | \r\n |
载荷紧跟在请求头之后;客户端发送载荷前不会等待任何应答。
没有回落网站
Section titled “没有回落网站”etemenanki-app 没有 fallbacks 选项。Xray 可以把不是合法 Trojan 的连接转交给一个 Web 服务器,让探测者看到一个普通网站。而在这里,密码错误的连接会在请求头之后被结束;完全不是 Trojan 的字节,一旦服务端发现 56 字节哈希之后不是 CRLF,连接就会被结束。在 [inbound.settings] 中写 fallbacks 会作为未知字段导致配置失败。
使用 network = "ws" 时,WebSocket 层先于 Trojan 应答:请求其他路径的 WebSocket 升级请求,或者在设置了 ws.host 时 Host 不符的升级请求,会收到 HTTP 404。不是 WebSocket 升级的普通 HTTP 请求得不到任何页面,连接直接被关闭。etemenanki-app 无法在 Trojan 端口上提供网站。
TCP 与嗅探
Section titled “TCP 与嗅探”CONNECT 请求会成为一个 TCP 流,由路由器按其地址、端口、网络、入站标签和客户端地址进行匹配。当地址是 IP 且 sniffing 开启(默认开启)时,服务端会先读取载荷开头,最多等待 300 毫秒,从中找出 TLS 服务器名或 HTTP Host,使域名规则依然能匹配。域名地址永远不会被嗅探。如果选中的出站无法建立连接,服务端会关闭客户端的连接;Trojan 没有错误应答。
UDP over Trojan
Section titled “UDP over Trojan”客户端发送命令 0x03 来建立 UDP 关联。请求头中的地址不会被使用;之后的每个数据包都携带自己的地址:
| 字段 | 长度 |
|---|---|
| 地址 | 可变,SOCKS5 编码 |
| 长度 | 2 字节,大端序 |
| CRLF | 2 字节 |
| 载荷 | Length 字节,最多 8192 |
入站始终提供 UDP 服务,没有关闭它的开关。每个数据包都按自己的目标地址路由,因此一个关联可以经由不同的出站到达多个对端,每个回复也会带上其来源地址封装后返回。UDP 流永远不会被嗅探。客户端关闭连接,或者双向都没有流量达 300 秒时,关联结束。
若要让 Trojan 入站只承载 TCP,可以把它的 UDP 路由到 blackhole。关联会保持打开,但所有数据包都会被丢弃。这条规则同样能捕获 mux 载体内的 UDP 子流(XUDP),因为它们也按 UDP 路由:
[[route.rule]]outbound = "block"inbound_tag = ["trojan-in"]network = "udp"在客户端一侧,Trojan 出站同样承载 UDP。路由器发给它的每个 UDP 关联都会建立一条到服务端的连接,该关联的所有数据包共用这条连接。
mux.cool 与 XUDP
Section titled “mux.cool 与 XUDP”Trojan 没有 mux 命令。进行多路复用的客户端(例如开启了 mux.enabled 的 Xray)改为向保留地址 v1.mux.cool 发送 CONNECT,服务端会把这条连接当作 mux.cool 载体处理。这一过程是自动的,没有相应设置。比较该地址时不区分大小写,请求中的端口会被忽略。
载体内的每个子流都按自己的目标地址路由,如同它是通过一条独立连接到达的一样,并沿用载体的用户。发往 IP 地址的子流只会根据其首帧携带的载荷进行嗅探;服务端不会为了等待更多数据而阻塞载体。UDP 子流(XUDP)在同一个载体上提供服务。一个载体同时最多容纳 256 个子流;超出时服务端会拒绝新的子流,并保持载体打开。
这只是服务端功能。etemenanki-app 自己的出站从不进行多路复用。
与 Xray 的兼容性
Section titled “与 Xray 的兼容性”协议代码移植自 Xray 的 proxy/trojan,以下形态在项目测试中与真实的 Xray 二进制进行了验证:
| 形态 | 与 Xray 的测试情况 |
|---|---|
| 开启 mux 的 Xray 客户端,经 ws + TLS 连接 etemenanki-app 的 Trojan 入站 | 端到端:一条经 mux 载体的 TCP 流 |
tls、ws 和 grpc 传输层,带或不带 TLS,etemenanki-app 分别作为客户端和服务端 |
端到端,上层为 VLESS;Trojan 使用相同的传输层 |
| mux 载体内的 XUDP | 端到端,基于 VLESS 和 VMess 载体;Trojan 载体使用相同的解复用器 |
| etemenanki-app 的 Trojan 出站连接 Xray 服务端 | 未与 Xray 测试。出站的客户端代码仅与项目自身的 Trojan 服务端代码进行了 TCP 和 UDP 测试。 |
迁移 Xray 配置:
| Xray | etemenanki-app |
|---|---|
入站 settings.clients[].password、email |
[inbound.settings] users[].password、email |
clients[].level、flow |
不支持;未知字段 |
fallbacks |
不支持;未知字段 |
出站 settings.servers[0].address、port |
[[outbound]] 表上的 server、port |
出站 settings.servers[0].password |
[outbound.settings] password |
streamSettings.network = "tcp" + security = "tls" |
[.stream] network = "tls" |
出站 mux.enabled = true |
不支持;出站为每个流单独建立一条连接 |
配置的其余部分见从 Xray 迁移。
| 限制项 | 值 | 超出后 |
|---|---|---|
| 传输层建立后发送第一个字节的时间 | 10 秒 | 关闭连接。 |
| 从第一个字节算起完成请求头的时间 | 10 秒 | 关闭连接。 |
| 已打开的流、关联或 mux 载体的空闲时间 | 300 秒 | 关闭连接。 |
| 客户端发送的每个 UDP 包的载荷 | 8192 字节 | 整个关联结束。 |
| 每个 mux 载体的子流数 | 256 | 拒绝新的子流;载体保持打开。 |
与所有流式入站一样,按入站设置的连接限制同样适用于 Trojan;见限制。
配置错误时,--test 会记录 configuration invalid:,启动时会记录 failed to start:,后面跟着下表中的消息;两者都以状态码 1 退出。这些消息中入站的标签为 trojan-in,出站为 trojan-out。用户条目内的错误会在消息之后多打印一行 in `users`。
| 消息 | 原因 | 解决方法 |
|---|---|---|
inbound trojan-in: invalid settings: missing field `password` |
某个用户条目没有 password。 |
为每个条目设置 password。 |
inbound trojan-in: invalid settings: unknown field `fallbacks`, expected `users` |
使用了 Xray 的键,例如 fallbacks 或 clients。 |
把 clients 改名为 users;删除 fallbacks。 |
inbound trojan-in: invalid settings: invalid type: string "…", expected struct TrojanUserCfg |
写成了 users = ["password"],即字符串列表。 |
每个用户都是一个表:users = [{ password = "…" }]。 |
outbound trojan-out: invalid settings: missing field `password` |
出站没有 [outbound.settings],或其中没有 password。 |
添加 password。 |
outbound trojan-out: invalid settings: unknown field `email`, expected `password` |
在出站上写了入站才有的键。 | 出站只接受 password。 |
inbound trojan-in: tls stream needs tls.cert_file(或 tls.key_file) |
TLS 形态缺少证书或私钥。 | 设置 tls.cert_file 和 tls.key_file。 |
No such file or directory (os error 2) |
证书或私钥路径不存在。消息中不会给出文件名。 | 检查 cert_file 和 key_file。 |
no certificate in PEM bundle |
cert_file 中不是 PEM 证书,常见的是误放了私钥。 |
让 cert_file 指向证书链。 |
inbound trojan-in: unknown stream security "tsl" (expected "tls" or "none") |
入站或出站的 security 拼写错误。 |
写成 "tls"。 |
outbound trojan-out: grpc stream needs grpc.service_name |
任意一侧使用了 network = "grpc" 但没有 grpc.service_name。 |
在客户端和服务端设置相同的 service_name。 |
outbound trojan-out: tls.allow_insecure and tls.ca_file cannot both be set |
同一个出站上同时设置了两种校验覆盖选项。 | 只保留一个。 |
运行时,被拒绝的连接只在 debug 级别记录,每个连接一行:
trojan connection from Some(198.51.100.7) ended: proxy core: trojan: invalid user设置 [log] level = "debug" 才能看到这些日志。ended: 之后可能出现的其他原因:
| 原因 | 含义 |
|---|---|
proxy core: trojan: not trojan protocol (missing CRLF after hash) |
客户端说的不是 Trojan,或者对明文 TCP 入站套了 TLS 再说 Trojan。 |
proxy core: client did not complete its request in time |
客户端发送了部分请求头,但在第一个字节后 10 秒内没有发完。 |
inbound handshake timed out after 10s |
客户端连接后 10 秒内没有发送任何 Trojan 字节。 |
proxy core: trojan: oversize payload |
某个 UDP 包超过 8192 字节。 |
TLS、WebSocket 或 gRPC 握手失败的客户端根本到不了 Trojan 层;这种情况会记录为 inbound transport failed:,同样在 debug 级别。