Skip to content

feat: add EasyTier outbound - #3194

Open
MiaM1ku wants to merge 9 commits into
MetaCubeX:Alphafrom
MiaM1ku:feat/easytier-outbound
Open

feat: add EasyTier outbound#3194
MiaM1ku wants to merge 9 commits into
MetaCubeX:Alphafrom
MiaM1ku:feat/easytier-outbound

Conversation

@MiaM1ku

@MiaM1ku MiaM1ku commented Sep 8, 2026

Copy link
Copy Markdown

说明

实现 #2875:新增 type: easytier 出站,接入官方 EasyTier/easytier-gowasm32-wasi + github.com/MetaCubeX/wazero)。

不引入 CGO / 自定义 .so,不隐式连接 public.easytier.top。底层 socket / DNS 走 mihomo C.DialerProxyServerHostResolver,由 mihomo 处理 interface-name / routing-mark / dialer-proxy

依赖的上游 API 已合入:EasyTier/EasyTier#2557CreateInstanceTOMLShowNodeInfo)。go.mod 现指向官方 commit b1f87f02

公开配置只保留结构化 YAML 字段。config / config-file 已移除。这些字段对应 EasyTier 现有 TOML 项,合并后非必要不做 breaking change。

et:// 放在 nameserver 中,写法对齐 ts://tailscale。该 DNS 仅解析 overlay A/PTR;文档注释建议放到 nameserver-policy

行为

  • 每个 outbound 一个 Host + 一个 Instance;lazy start,使用 outbound context
  • 始终 no_tun = true;结构化配置额外 bind_device = false
  • 默认 no-listener: truelisteners = []);无 listener 时必须显式配置 peers
  • peers 支持多个入口 URI
  • 可自行指定 overlay IPv4(ipv4: 10.144.0.1010.144.0.10/24);ipv4 为空时默认 dhcp: true
  • Dial 仅 overlay IPv4;MagicDNS(默认 et.net)走 ListRoute / ShowNodeInfo,其它名字走 ProxyServerHostResolver
  • 自动注册 et://<name> DNS,仅 overlay A/PTR
  • state-dir 持久化 instance_id(默认 easytier/<name>
  • no_easytier build tag 可从二进制中排除 WASM
- name: easytier
  type: easytier
  network-name: example
  network-secret: secret
  ipv4: 10.144.0.10
  peers:
    - tcp://192.0.2.10:11010
    - udp://192.0.2.11:11010
  udp: true

Test plan

  • CGO_ENABLED=0 go test ./component/easytier ./adapter/outbound ./config
  • CGO_ENABLED=0 go build -tags with_gvisor
  • CGO_ENABLED=0 go build -tags 'with_gvisor,no_easytier'

正式网络测试

网络 本机 DHCP 得到 overlay 10.33.44.1

单 peer ``:实例启动后约 4s 看到 8 个 overlay 节点。

多 peer `:同样约 4s 看到同一批 8 个节点。

可见节点

Integrate official easytier-go (wasm32-wasi + wazero) as type: easytier.
No CGO, no custom shared libraries, and no implicit public.easytier.top
peer. Overlay dial is IPv4-only; MagicDNS uses ListRoute/ShowNodeInfo.
Point github.com/EasyTier/EasyTier/easytier-go at EasyTier/EasyTier#2557
and drop the fork replace. Document that peers accepts multiple URIs.
@olicesx

olicesx commented Sep 8, 2026

Copy link
Copy Markdown

🔴 CHANGES REQUESTED — 1 个会终止进程的问题 + 4 个需修正/确认 + 6 个次要

先说方向:这个 PR 走的正是 #2875 里 wwq 给出的路线(wasm32-wasi + MetaCubeX/wazero、不引入 CGO、不加载自定义 .so、完全接管 socket 与 DNS),接线也严格对齐 Tailscale/ZeroTier 既有范式(parser.goconfig.gots:// 分支、constant/featuresdocs/config.yamldns 注册表、New 里不做任何连接)。以下都是收口问题,不是路线问题。

🔴 1. LookupTXT/LookupSRV 落到 net.DefaultResolver,会直接 os.Exit(2) 杀死 mihomo
component/easytier/platform.go:110-124net.DefaultResolver;而 main.go:81-95 明确把 net.DefaultResolver.Dial 换成了「打印全部 goroutine 栈 + os.Exit(2)」的哨兵(注释:Defensive programming: panic when code mistakenly calls net.DefaultResolver)。SDK 侧 internal/reactor/dns.go:61-65 会按 guest 请求分发 DNSAddress/DNSTXT/DNSSRV,因此只要核心发起一次 TXT/SRV 查询(例如 STUN 的 txt: 形式,SDK internal/host/instance_config.go:495 就是校验这个的),进程会被整个杀掉而不是返回错误。同文件 LookupIP 已正确用 ProxyServerHostResolver,这里应保持一致:拿不到能力就显式 return error,不要回落到 net.DefaultResolver

🟡 2. docs/config.yaml:317et://easytier 放在 nameserver 主列表里
dns/util.go:419-441batchExchange 是并发竞速取第一个「非 error」回复,NXDOMAIN 不算 error;而 adapter/outbound/easytier.go:400/410/415 对 overlay 之外的名字直接回 NXDOMAIN。这与 ts://tailscale(转发真实上游)语义不同,照抄这行会让公网域名解析间歇性失败(NXDOMAIN 因 minimalTTL 空集返回 0 而不会被缓存,所以表现为时好时坏)。建议注释里写清它只服务 et.net 区,只适合放 nameserver-policy

🟡 3. go.mod:153replace 指向个人 fork
replace github.com/EasyTier/EasyTier/easytier-go => github.com/MiaM1ku/EasyTier/easytier-go。base 里唯一的 replacemetacubex/protobuf-gogo.mod:151),其余 39 个 fork 全在 metacubex 组织下(对照 #2786metacubex/tailscale)。且上游 EasyTier/EasyTier#2557CreateInstanceTOML/ShowNodeInfo)此刻仍 open 未合并,fork 仓库与该 PR 均为 09-08 11:07/11:08 新建。建议先落到 MetaCubeX 组织或等上游合并,否则构建可用性绑在一个个人账号上。

🟡 4. socket 接管不完整——正是 #2875 最担心的回环面

  • platform.go:48-55 ListenTCP 用裸 net.ListenConfiginterface-name/routing-mark/dialer-proxy 全不生效;默认 no-listener=true 时无碍,但配了 listeners 就是在透明代理机上 0.0.0.0:11010 起监听。
  • platform.go:32-55 丢掉 Bind.LocalAddr(源地址/源端口)、BindDeviceSocketMarkReuseAddr/ReusePort(注释声明是有意忽略)。但 SDK 的 SocketContext.SocketMark 本来就是给打洞绕开 TUN 用的,请说明在 TUN + auto-route 下靠什么保证不回环。
  • platform.go:70-91 udpBind 有 bug:LocalAddr 为「IP 为空 + 指定端口」(wildcard bind)时算出了 address 却丢弃,落到 0.0.0.0:0。实测 {Port:45678}0.0.0.0:0{IP:0.0.0.0,Port:45678}0.0.0.0:45678;SDK 参考实现 platform/netstd 是直接 net.ListenUDP(..., options.LocalAddr),端口不会丢。
  • platform.go:131-147 LocalAddrForRemote 为了读 LocalAddr 真的 dial 一次(配 dialer-proxy 时还会经代理建会话)再关掉:端口不持有,且拿到的是代理侧本地地址。

🟡 5. component/easytier/toml.go 的行扫描改写会把合法 TOML 改成非法 TOML
用 PR 自身代码实测:[flags] # tun flagsisTOMLSectiontoml.go:213-215)要求行尾是 ] 而不被识别,末尾追加出第二个 [flags] 表 → 重复表 → CreateInstanceTOML 报错 → startErrsync.Once 固化,该出站直到重载配置都是死的。同类:"no_tun" = false(带引号 key,flagKey 不匹配)会同时留下两个键 → 重复键;多行字符串里出现 [flags] 会被当 section 改写。一个几行的单测即可覆盖。

🔵 6. 次要(不阻塞)

  • no_easytier 实测有效(默认构建 85.9MB → 73.8MB,go list -deps 下 wazero/internal/artifact 消失),但 adapter/outbound/easytier_option.go 无 tag 且 component/easytier/platform.go 仍 import easytier-go/platform,模块依赖与下载仍存在;CI/release(build.yml 只带 with_gvisor)从不构建该 tag,等于没有回归保护。
  • IPv4-only 只写在 commit message 里,docs/config.yaml 的 easytier 段未提;同时 BasicOption.IPVersion 照收,但 resolveIPv4easytier.go:237-263)只查 A、完全不看 prefer
  • IsMagicDNSoverlay.go:48-58)把所有无点主机名都当 MagicDNS,http://nas:5000 这类单标签名走该出站会直接失败、不回退 DNS。
  • overlayNodeseasytier.go:198-235)每次域名 dial、每次 A/PTR 查询都要跑 ShowNodeInfo + ListRoute 两个 WASM RPC(internal/host/rpc.go),无缓存;SDK 有 Events() 可用于失效。
  • 重载时旧实例只靠 NewAutoCloseProxyAdapter 的 finalizer 关闭(base.go:393-399),新旧实例可能短暂共用 easytier/<name> 与同一 instance_id
  • .golangci.yaml 的 gci 段序(standard → prefix(github.com/metacubex/mihomo) → default)不通过:easytier.go/platform.gogithub.com/EasyTier/... 排在 mihomo 组之前;dns/easytier.godns/tailscale.go 的逐行副本。
  • SDK 自带 PERFORMANCE.md/README 数据:TUN 下单流约为原生 35–45%,1 Gbit/s UDP 丢包 30–65%(Intel N100 上 TCP 436/517 Mbit/s)。建议 docs 加一句量级提示,避免用户拿它当通用代理出口。

已实测的事实(Alpha d5f57a5e + 本 patch,CGO_ENABLED=0):-tags with_gvisor-tags with_gvisor,no_easytier 均构建通过;go test ./component/easytier ./adapter/outbound ./config 全绿(live test 无 env 跳过);gofmt/vet 干净;交叉编译 linux/{386,arm,arm64,mips,mipsle,mips64,mips64le,ppc64le,s390x,riscv64}、windows/amd64、darwin/arm64、android/arm64、freebsd/amd64 全 OK(loong64 失败源于 utls/internal/cpu 的 loong64 汇编,需你们的 custom Go 工具链,与本 PR 无关);easytier-gometacubex/wazero 的 go.mod 均为 go 1.20,不会顶掉仓库下限。

@MiaM1ku

MiaM1ku commented Sep 8, 2026

Copy link
Copy Markdown
Author

@olicesx fix

@wwqgtxx

This comment was marked as resolved.

@MiaM1ku
MiaM1ku force-pushed the feat/easytier-outbound branch from 1cf2200 to ec87743 Compare September 8, 2026 13:10
@wwqgtxx

wwqgtxx commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

感谢您的贡献

代码整体上还是不错的,目前的有些小问题:

第一,adapter/outbound目录中请移除不必要的test
第二,文档部分,dns可以统一放在nameserver,只要在后面追加注释说明“仅解析 overlay A/PTR”,建议放在nameserver-policy中;WASM性能特性说明没有存在的必要也请删除;“始终以 no_tun 运行,不加载自定义 .so”这句话也没有存在的必要
第三,请在PR说明部分补充当前正式测试的结果,增加可信度
第四,关于配置,你得确认两件事,当前的配置项是否足够稳定,因为大概率我们合并后非必要不会做出break changing;其次,为什么要支持config-file/config,我们不倾向于提供这种配置

Internal TCP reservations now bind on the host when dialer-proxy is set,
structured configs enable DHCP if ipv4 is omitted, and MagicDNS reads
flags.tld_dns_zone from raw TOML.
Structured YAML is the public contract. et:// stays in nameserver with
an overlay A/PTR note, matching ts://.
@MiaM1ku

MiaM1ku commented Sep 8, 2026

Copy link
Copy Markdown
Author

@wwqgtxx

  1. 已删除 adapter/outbound 里 EasyTier 的测试。
  2. et:// 放回 nameserver,对齐 ts://tailscale;注释写明仅解析 overlay A/PTR,并建议放到 nameserver-policy。WASM 性能说明和 no_tun / .so 注释已删。
  3. PR 说明已补正式网络测试结果(单 peer / 多 peer 均加入 easytier网络,看到 8 个 overlay 节点)。
  4. 公开配置只保留结构化 YAML,合并后非必要不做 breaking change。config / config-file 已移除,不再提供原生 TOML 入口。

@wwqgtxx

wwqgtxx commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

鉴于 easytier-go 实际上是由 @KKRainbow 开发完成的,所以在合并前,我们希望征求其意见

Comment thread adapter/outbound/easytier.go
Comment thread adapter/outbound/easytier.go
Render [secure_mode] and per-peer peer_public_key from structured YAML so
public shared nodes can be pinned without changing easytier-go.
EasyTier/EasyTier#2560 renamed the Go module to
github.com/easytier/easytier/easytier-go. Bump to that merge commit so
imports match the published module path.

@wwqgtxx wwqgtxx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我还是觉得既然 peers 内部本来就是个uri,那么peer-public-key直接作为uri参数放在string内部就够了,不需要搞结构化配置这么麻烦

把这个再改一下差不多就可以合了

Drop the structured peer object. peers stay URI strings; peer-public-key
is parsed from the query and rendered as TOML peer_public_key.
@MiaM1ku
MiaM1ku requested a review from wwqgtxx September 10, 2026 16:32
Comment thread docs/config.yaml
# peer-public-key: "base64-x25519-public-key" # 锁定共享节点公钥,防止中间人
# secure-mode: true # Noise E2EE;配置 local-*-key 或 peer-public-key 时会自动开启
# - "tcp://relay.example.com:11010?peer-public-key=base64-x25519-public-key" # 锁定共享节点公钥,防止中间人
# secure-mode: true # Noise E2EE;配置 local-*-key 或 peer URI 中的 peer-public-key 时会自动开启

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wwqgtxx done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants