第一个代理
本页带你完整地跑一遍 etemenanki-app。你将为本机上的 SOCKS 代理写一份简短的配置,在不启动任何东西的情况下检查它,用 curl 通过代理发出请求,然后在代理保持运行的同时添加一条屏蔽某个域名的路由规则。过程中你会看到程序实际打印的每一行输出,既有一切正常时的,也有写错配置时的。
这里不需要服务器,也不需要 root 权限。所有服务都监听在 127.0.0.1 上,因此只有运行代理的这台机器能访问它。
- 已安装
etemenanki-app,并且它在你的PATH中。运行etemenanki-app --version检查;获取二进制文件的方法见安装。 - 已安装
curl,并且这台机器可以直接访问互联网。 - TCP 端口
1080没有被其他程序监听。如果已被占用,换一个端口,并在下文所有地方使用它。
在一个空目录中操作。省略 -c 时,etemenanki-app 会读取当前目录下的 config.toml,不过本页的每条命令都显式传入了 -c config.toml。
运行本地 SOCKS 代理
Section titled “运行本地 SOCKS 代理”-
编写配置。 将以下内容保存为
config.toml:config.toml # A first etemenanki-app config: a local SOCKS proxy on 127.0.0.1:1080 that# sends every connection straight out to the internet.# Accept SOCKS 4/4a/5 clients on the loopback interface only.[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080# Connect to the requested destination directly.[[outbound]]tag = "direct"protocol = "freedom"这个文件声明了两样东西。入站(inbound)是连接进来的地方:这里是一个绑定在回环地址
1080端口上的 SOCKS 服务端。出站(outbound)是连接出去的地方:freedom会直接连接客户端请求的目标地址。没有[route]部分时,所有连接都走第一个出站。 -
检查配置。
--test会解析文件,并像真正启动时一样构建每个入站、出站和路由规则,但不绑定任何端口,也不建立任何连接:终端窗口 etemenanki-app --test -c config.toml文件有效时,它打印一行并以状态码
0退出:Configuration OK.文件无效时,它打印原因并以状态码
1退出。具体效果见下文的写错配置时。 -
运行代理。 在前台启动代理:
终端窗口 etemenanki-app -c config.toml监听器启动后,它会输出日志:
2026-09-24T20:21:58.014596Z INFO etemenanki_app::instance: inbound socks-in listening on 127.0.0.1:1080进程此时开始等待连接。让它保持运行,另开一个终端。
-
通过代理发出请求。 在第二个终端中执行:
终端窗口 curl --socks5-hostname 127.0.0.1:1080 https://example.comcurl会打印 Example Domain 页面的 HTML。请求的路径是curl→socks-in→direct→example.com。--socks5-hostname让curl把域名example.com交给代理,由代理来解析。如果用普通的--socks5,curl会自己解析域名,只把 IP 地址发给代理。两种方式都可以,而且域名规则对第二种方式同样生效。当客户端请求的是 IP 地址时,SOCKS 入站会最多等待 300 ms,读取连接最开始的几个字节,并从中取出域名(TLS 服务器名称或 HTTPHost头)。这就是嗅探,默认开启。
各配置项的作用
Section titled “各配置项的作用”这份配置只用到了少数几个配置项。etemenanki-app 会检查这些表中的每一个配置项:不认识的配置项会被当作错误,而不是被跳过。
| 配置项 | 类型 | 默认值 | 含义 |
|---|---|---|---|
[[inbound]] |
表数组 | 无 | 每个监听器一个表。配置中可以一个都没有,此时进程照常运行,但不接受任何连接。 |
tag |
字符串 | 必填 | 在日志和路由规则中使用的名称。入站之间的 tag 必须唯一,出站之间的 tag 也必须各自唯一。 |
protocol |
字符串(枚举) | 必填 | 入站使用的协议。"socks" 在同一个端口上同时接受 SOCKS 4、4a 和 5。 |
listen |
字符串 | "127.0.0.1" |
要绑定的地址。省略时入站绑定回环地址,绝不会绑定所有网卡。以 / 开头的值表示 Unix socket 路径。 |
port |
u16 | 必填 | TCP 端口。除非 listen 是 Unix socket 路径,否则必填;listen 是 Unix socket 路径时不允许设置。 |
[[outbound]] |
表数组 | 无 | 每个目的地一个表。至少需要一个。 |
tag |
字符串 | 必填 | 路由规则和 [route].default 引用的名称。 |
protocol |
字符串(枚举) | 必填 | "freedom"(别名 "direct")连接到请求的目标地址。"blackhole"(别名 "block")丢弃连接。 |
SOCKS 入站自己的选项位于 [inbound.settings] 中,这份配置使用的都是默认值:不认证(auth = "none"),启用 UDP ASSOCIATE(udp = true),开启嗅探(sniffing = true,这是入站本身的配置项)。完整列表见 SOCKS。
屏蔽一个域名
Section titled “屏蔽一个域名”现在添加第二个出站,用来丢弃连接,再添加一条规则,把某个域名发往这个出站。上一节启动的代理可以继续运行:etemenanki-app 会监视配置文件,文件变化时自动重新加载。
-
修改
config.toml,使其内容如下:config.toml # The first-proxy config extended with routing: connections to example.net# and its subdomains are dropped, everything else goes out directly.[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080[[outbound]]tag = "direct"protocol = "freedom"# Accepts a connection and closes it without sending anything.[[outbound]]tag = "block"protocol = "blackhole"[route]# Used when no rule matches. Without this line the first outbound is used.default = "direct"# Rules are tried in order; the first one that matches picks the outbound.[[route.rule]]domain_suffix = ["example.net"]outbound = "block"新增了三处内容。
block出站使用protocol = "blackhole"。[route]表把direct指定为默认出站,这正是第一份配置隐式做的事。还有一条[[route.rule]],把example.net及其下所有域名发往block。 -
保存文件,观察第一个终端。 片刻之后,正在运行的代理会记录这次变更并重启监听器:
2026-09-24T20:21:59.323617Z INFO etemenanki_app::instance: config reload: outbounds +[block]; route changed2026-09-24T20:21:59.323905Z INFO etemenanki_app::instance: inbound socks-in listening on 127.0.0.1:1080+[block]表示新增了一个带该 tag 的出站。同一行中,-[…]表示被删除的 tag,~[…]表示被修改的 tag。 -
测试规则的两种情况。
example.net下的域名现在会被丢弃:终端窗口 curl --socks5-hostname 127.0.0.1:1080 https://www.example.netcurl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to www.example.net:443具体的报错信息取决于你的
curl构建版本。SOCKS 握手本身是成功的;随后 blackhole 出站一个字节都不发就关闭了连接,所以curl看到的是 TLS 握手被中断。如果是普通 HTTP,同一条规则的表现是curl: (52) Empty reply from server。其他域名仍然直接连接:
终端窗口 curl --socks5-hostname 127.0.0.1:1080 https://example.com
每个连接的路由过程如下:
flowchart LR
C["curl"] --> I["入站 socks-in"]
I --> R{"规则 1:domain_suffix example.net?"}
R -- "匹配" --> B["出站 block"]
R -- "不匹配" --> D["route.default: direct"]
D --> N["目标地址"]
规则如何匹配
Section titled “规则如何匹配”- 首个匹配生效。 规则按照在文件中出现的顺序依次尝试。第一条匹配的规则决定出站,后面的规则不再检查。没有任何规则匹配的连接走
[route].default;未设置default时,走第一个[[outbound]]。 domain_suffix遵循标签边界。"example.net"匹配example.net和www.example.net,但不匹配notexample.net。匹配时不区分大小写。- 嗅探到的域名同样有效。 域名规则针对客户端请求的域名进行检查。如果客户端只发送了 IP 地址,则改为针对嗅探从连接中读到的域名进行检查。所以只发送 IP 地址的
curl --socks5同样会被屏蔽。完全不带域名的连接,例如直接连到某个 IP 地址的原始 TCP 连接,永远不会匹配域名规则。 - 未知的 tag 是错误。 如果某条规则的
outbound指向不存在的 tag,整份配置都无法通过。
| 配置项 | 类型 | 默认值 | 含义 |
|---|---|---|---|
[route].default |
字符串 | 第一个 [[outbound]] 的 tag |
没有任何规则匹配的连接所使用的出站。 |
[[route.rule]] |
表数组 | 无 | 路由规则,按文件中的顺序依次尝试。 |
outbound |
字符串 | 必填 | 匹配的连接所使用的出站(或负载均衡器)的 tag。 |
domain_suffix |
字符串数组 | [] |
要匹配的域名,每个域名都包含其所有子域名。 |
etemenanki-app 采用 fail closed(出错即拒绝)策略:遇到不认识的配置项时会停止运行,而不是忽略它。这是出于安全考虑。如果拼错的 listen 被直接跳过,入站就会悄悄绑定到一个与你所写不同的地址上。
把第一份配置中的 listen 改成 lisen,再检查一次:
etemenanki-app --test -c config.toml2026-09-24T20:23:41.522763Z ERROR etemenanki_app: configuration invalid: TOML parse error at line 8, column 1 |8 | lisen = "127.0.0.1" | ^^^^^unknown field `lisen`, expected one of `tag`, `protocol`, `listen`, `port`, `stream`, `address_family`, `sniffing`, `settings`报错信息给出了行号和列号,指出了出错的配置项,并列出了该位置上所有合法的配置项。退出状态码为 1。
其他错误也以同样的方式报告。文件外层结构上的错误,例如未知配置项、类型错误和缺少必填项,会带有行号。之后在 etemenanki-app 构建入站、出站和路由时才发现的错误则没有行号,其中大多数会指出是哪个入站或出站。[inbound.settings] 和 [outbound.settings] 是在这个后期阶段检查的,所以那里的拼写错误报告时不带行号:
| 错误 | etemenanki-app 在 configuration invalid: 之后打印的内容 |
|---|---|
| 配置项拼写错误 | TOML parse error at line 8, column 1 … unknown field `lisen`, expected one of … |
表名拼写错误,例如 [[route.rules]] |
unknown field `rules`, expected one of `default`, `geoip`, `geosite`, `rule` |
| 端口写成了字符串 | invalid type: string "1080", expected u16 |
| 端口大于 65535 | invalid value: integer `70000`, expected u16 |
缺少 tag |
missing field `tag` |
| 协议名拼写错误 | outbound block: unknown protocol "blackhol" |
[inbound.settings] 中的配置项拼写错误 |
inbound socks-in: invalid settings: unknown field `udp_bnd`, expected one of `auth`, `accounts`, `udp`, `udp_bind` |
| 规则指向不存在的出站 | route references unknown outbound tag: blocked |
listen 是 IP 地址但没有 port |
inbound socks-in: port is required |
完全没有 [[outbound]] |
config defines no outbounds |
| 两个出站使用了相同的 tag | duplicate outbound tag: direct |
-c 路径错误 |
No such file or directory (os error 2) |
停止与重新加载
Section titled “停止与重新加载”Ctrl-C
Section titled “Ctrl-C”在运行代理的终端中按 Ctrl-C,或者向它发送 SIGTERM。两者效果相同:etemenanki-app 输出日志
2026-09-24T20:22:00.756740Z INFO etemenanki_app: shutting down然后关闭监听器,并以状态码 0 退出。它不会等待已打开的连接结束,而是立即关闭它们。
修改配置文件
Section titled “修改配置文件”运行期间,etemenanki-app 会监视配置文件所在的目录,因此即使编辑器是先写入新文件、再重命名覆盖原文件,保存操作也能被检测到。检测到变化后,它会等待约 200 ms,让保存操作完成,然后重新读取文件。如果读到的内容与上次逐字节相同,则什么也不做。如果无法监视该目录,etemenanki-app 会输出 config hot-reload disabled: 及原因,并在不支持重新加载的情况下继续运行。
接下来的行为取决于新文件:
-
新文件有效。 etemenanki-app 输出
config reload:以及变更内容,停止所有监听器并关闭所有已打开的连接,然后启动新的监听器。客户端需要重新连接。只要文件内容有任何变化,哪怕只改了一行注释,都会这样处理:这种修改会输出config reload: no changes,但仍然会重启全部服务。如果某个新监听器无法绑定,例如端口已被占用,etemenanki-app 会输出inbound <tag> bind <address> failed:及原因,照常启动其他入站,并在缺少这个入站的状态下运行,直到文件再次变化。 -
新文件无效。 etemenanki-app 记录错误,并继续使用旧配置运行,不会中断任何服务:
2026-09-24T20:22:11.344292Z ERROR etemenanki_app::instance: reload: parse failed, keeping current config: TOML parse error at line 5, column 1解析之后才发现的错误,例如未知的出站 tag,会以
reload: build failed, keeping current config:的形式记录。修正文件后再保存一次即可。
由于重新加载会关闭所有连接,在修改一个有其他人在使用的代理的配置之前,先运行 etemenanki-app --test -c config.toml,再保存。详细说明见热重载,其中也包括哪些内容不会被重新加载:日志级别只在启动时读取一次,如果设置了 RUST_LOG 环境变量则取自该变量,否则取自 [log].level(默认为 info)。
现在你已经有了一个可用的代理,也大致了解了配置是如何检查的。接下来可以阅读:
- 配置文件:介绍文件的整体结构以及每个顶层表。
- 入站和出站:列出通用配置项以及所有协议。
- 路由:介绍其他规则条件,包括 IP 段、端口、网络类型、geodata 等。
- 运行 etemenanki-app:介绍如何将它作为服务运行。