# 变更日志 (CHANGELOG)

## 2026-07-15 仓库拆分：core 独立为 `meshray/core`，wg-go 集成迁移至 `internal/ctr/corebind`

### 背景（架构决策）
core（转发引擎）本质是**去中心化 frp**——9 层传输只转发字节 A↔B，不关心载荷是应用流量
（形态 A：独立 frp 隧道）还是 WG 密文（形态 B：增强传输）。此前 core 直接 `import wg-go`
并内置 `EnhancedBind`，导致「集成干扰核心」：核心层被 wg-go / contract 绑架，难以独立演进，
也无法作为纯粹的去中心化 frp 基地。

故按用户决策：
- 项目命名统一为 **meshray**；wg-go 集成属于 manager / UI / GUI 范畴，不污染 core。
- 把原 `core/` 目录拆为**两个隔离仓库**：
  - `Meshray`（模块 `git.zkcoi.com/zkcoi/meshray/core`）：零 wireguard、零 contract 依赖的纯转发底座。
  - `Meshray-Manager`（模块 `git.zkcoi.com/zkcoi/meshray`）：wg-go / contract / UI / 管控面。

### 改动（核心仓库 `Meshray`）
- 模块路径由 `git.zkcoi.com/zkcoi/meshray`（与 manager 冲突）改为
  `git.zkcoi.com/zkcoi/meshray/core`；`core/` 目录重命名为 `engine/`，全仓
  `git.zkcoi.com/zkcoi/meshray/` → `git.zkcoi.com/zkcoi/meshray/core/` 替换。
- `engine/engine.go`：
  - `NewEngine(logger, metrics, plugin ...transport.ProtocolPlugin)`——插件改为**可选变参**，
    形态 A 可直接空插件启动。
  - 移除 `bind *EnhancedBind` 字段与 `wg.NewWGPlugin()` 导入。
  - 新增**集成钩子**（core 不依赖 wg，由外部注入）：
    - `PeerRegistrar` 接口 + `SetPeerRegistrar`：外部 bind 注册对端地址供 NAT 学址。
    - `SetRelayInboundHandler(func(peerKey string, packet []byte))`：中继入站回调交给外部。
    - `SetDirectByBind(bool)` / `SetListenPort(int)`：声明由 bind 接管直连与端口。
  - `NotifyPeerInfo` 改调 `e.peerRegistrar.RegisterPeer(addr, peerKey)` 而非 `e.bind.RegisterPeer`。
- `transport/relay_test.go`：移除对 `plugins/wg` 的依赖，改用本地 `mockPlugin`
  （实现 `IsControlPacket`/`IsDataPacket` 返回 false）。
- `transport/`、`connect/` 仅注释提及 WireGuard，无任何 import 依赖。
- `go.mod`：仅依赖 gorilla/websocket、pion/stun、pion/turn/v2、pion/webrtc/v3、
  quic-go、zap；**移除 wireguard-go / wgctrl / meshray-contract**。

### 改动（manager 仓库 `Meshray-Manager`）
- `internal/ctr/corebind/`（`package corebind`，**新增**）：把 wg-go 集成从原 core 迁出：
  - `bind.go`：移植 `EnhancedBind`，`NewEnhancedBind` 改为通过 core 钩子接线
    （`SetPeerRegistrar` / `SetRelayInboundHandler` / `SetDirectByBind(true)` / `SetListenPort`），
    实现 `conn.Bind`（`Open`/`Send`/`Close`/`ParseEndpoint`/`BatchSize`/`SetMark` 等）。
  - `registry.go`：移植注册逻辑 + `getOrCreateEngine`（用 `core.NewEngine(logger, core.NewMetrics())`），
    `init()` 注册 `contract.RegisterEnhancedBindFactory` / `contract.RegisterEngineProvider`。
  - `adapter.go`：meshray-contract ↔ meshray/core 类型互转
    （`toCoreCandidates` / `toCoreICEConfig` / `toCoreLayerOrder` / `toContractStatus`）。
- `internal/ctr/core_enable.go`：空白导入改为
  `_ "git.zkcoi.com/zkcoi/meshray/internal/ctr/corebind"`。
- `cmd/mr-wg/`：**由原 `core/cmd/meshray-core/` 迁移**，导入修正为
  `git.zkcoi.com/zkcoi/meshray/core` 与 `internal/ctr/corebind`，使用
  `corebind.NewEnhancedBind`、候选类型 `meshraycore.Candidate`。
- `go.mod`：
  - 删除旧 `replace git.zkcoi.com/zkcoi/meshray-core => ./core` 与 `require meshray-core`。
  - 新增 `require git.zkcoi.com/zkcoi/meshray/core v0.0.0` +
    `replace git.zkcoi.com/zkcoi/meshray/core => ../Meshray`（本地兄弟模块）。
- `internal/ctr/wg.go`、`ctr.go`、`interface.go`：仅注释/字符串提及 `meshray-core`，无 import 依赖，保持不变。

### 验证
- `Meshray`：`go build ./...` → 0；`go test ./transport/... ./connect/...` → ok（零 wg / contract 依赖）。
- `Meshray-Manager`：`go build ./...` → 0；`go build ./cmd/mr-wg/... ./internal/ctr/...` → 0。
- 已知：manager `go mod tidy` 在 Go 1.26 下因「兄弟模块 replace + 本地 replace」触发内部 panic，
  已用 `go mod download` + 直接路径 `go build ./path/...` 绕过，未执行 tidy。

## 2026-07-15 重构：Direct-UDP 改为共享监听 socket + WG 学址，原生支持 NAT 穿透（核心）

### 背景（用户诉求）
用户指出：NAT 后的机器与公网 VPS 通信本不应有限制（标准 UDP/WireGuard 语义：主动侧发包后
NAT 记录会话，回包可返回）。且希望 core 成为「地址权威」——集成时对端地址从 core 获取再喂给
wg-go，而非依赖 wg-go 预配置的 endpoint 可达性。

### 根因（旧实现为何 NAT 场景不通）
增强模式此前用「每 peer 一个 connected UDP socket」（`DirectFactory.Dial` →
`net.DialContext`）。connected socket 只接收所连对端的包：
- NAS（NAT 后）拨 VPS:51820 成功，但其源地址经 NAT 改写为 `WAN:随机端口`；
- VPS 的 connected socket 连的是 `NAS内网:51820`，收到的 `WAN:随机端口` 源地址不匹配 → 丢弃。

即公网侧「收得到、回不去」。这不是 NAT 的固有限制，而是 connected socket 语义所致。

### 修复（回归标准 WireGuard Bind 语义）
Direct-UDP 改由 `EnhancedBind` 的**共享未连接 UDP socket**（`directSock`，绑定 listen_port）承载：
- `core/bind.go`：
  - 新增 `directSock *net.UDPConn`、`listenPort`；`inboundPacket` 增加 `ep conn.Endpoint`。
  - `Open` 用 `net.ListenUDP` 在 listen_port 建共享 socket（listen_port=0 时由 OS 分配并回填
    引擎实际端口），启动 `readDirectLoop`。
  - `readDirectLoop` 从任意来源收包，把**真实源地址**作为 Endpoint 投入 `inboundCh`
    （NAT 穿透学址核心：公网侧据此学到对端 NAT 映射后地址）。
  - `receiveFunc`/`endpointForPacket`：Direct 入站用学得的真实源地址；中继入站按 peerKey 查缓存。
  - `Send` → `directSendTo`：经共享 socket `WriteToUDP` 发往目标地址（源端口固定 listen_port，
    满足对称直连）。移除旧的随机端口兜底 socket（`fallbackSock`/`fallbackSend`）。
- `core/engine.go`：新增 `directByBind` 标志（由 `NewEnhancedBind` 置位）。增强模式下
  `NotifyPeerInfo` 不再对 Direct 层主动拨号（WG 启动后自动向 endpoint 发握手经 `Send` 承载），
  避免 `DirectFactory.Dial` 再绑 listen_port 造成 UDP 端口冲突。

### 效果
- 双公网对称直连：`A:port↔B:port` 原生可通。
- NAT 后主动侧 ↔ 公网侧：公网侧收包学到对端真实地址，回包自然返回，**无需对端预先公网可达、
  无需路由器端口映射**。core 成为地址权威，集成侧可从 core 取对端地址喂给 wg-go。
- TURN/WebRTC/WS 等中继/信令层不受影响，仍经 connMgr 承载（Direct 不可用时补充）。
- TCP 直连族（FakeTCP/RealTCP）为 TCP，与 UDP `directSock` 不冲突。

### 验证
- `go build ./... && go vet ./... && go test ./...` → 全部通过（3 个测试包 ok）。

## 2026-07-15 修复：直连传输 socket 未绑定本地端口，两台机器互传失败（核心 P0）

### 背景（用户排查「两台机器传不了数据」）
此前已修复「初始直连缺入站读取」，但端到端仍不通。进一步排查数据路径（Bind→Relay→
DirectFactory）发现更底层缺陷：增强模式下 WG 经 `EnhancedBind` 接管，**并不在 listen_port
上监听真实 UDP**（`Open` 仅从 `inboundCh` 取包，无 `ListenUDP`）。

### 根因
`DirectFactory.Dial` 用 `dialer.DialContext(ctx,"udp",candidate)`，**本地端口为随机临时端口**：
- A 拨 B:51820 → A 的 socket 本地 `A:临时`、远端 `B:51820`；
- B 拨 A:51820 → B 的 socket 本地 `B:临时`、远端 `A:51820`。

A 发往 `B:51820` 的包到达 B 的 OS 时，B 在 51820 无任何监听 socket（B 的传输 socket 本地是
临时端口），**被 OS 丢弃**；B→A 同理。即两端都出得去、进不来——这正是「frp 能传、meshray-core
不行」的本质：frp 是中心中继（两端都出向连 server），而本仓增强模式要求对称直连却没把
本地端口绑到 listen_port。

### 修复
让 Direct-UDP 传输 socket **把本地端口绑到 listen_port（WG listen_port）**，形成
`A:port↔B:port` 对称直连：
- `connect/direct.go`：`DirectFactory` 增加 `localPort` 与 `SetLocalPort`；`Dial` 时
  `dialer.LocalAddr = &net.UDPAddr{Port: bindPort}`（优先 `DialConfig.LocalPort`，其次工厂值）。
- `connect/strategy.go`：`DialConfig` 增加 `LocalPort int`。
- `core/engine.go`：`Engine` 持有 `listenPort` 与 `directFactory` 引用，新增 `SetListenPort`
  回填工厂；`initiateConnection` 把 `listenPort` 注入 `DialConfig.LocalPort`。
- `core/bind.go`：`NewEnhancedBind` 内调用 `eng.SetListenPort(listenPort)`，
  使 standalone 与受管模式（均经此入口）都能拿到本端端口。

### 验证
- 临时单测模拟两机（回环两端口）：A:15180↔B:15181 对称直连，双向 Write/Read 均成功
  （修复前临时端口下对端收不到）。已删除该临时测试。
- `go build ./... && go vet ./...` → 通过。

### 注
NAT 后的机器仍依赖 TURN/WebRTC 中继/打洞层（需配置 `ice` 服务器），本修复解决的是
「直连（独立 IP / 公网 / 同局域网）对称互通」这一基础能力。

## 2026-07-15 独立 meshray-core 改用 TOML 配置文件（类 frp）

### 需求
独立运行（类 frp 风格）希望像 frp 一样使用 TOML 配置文件，而非 JSON。

### 改动
- 新增依赖 `github.com/BurntSushi/toml`（与 frp 同款）。
- `core/cmd/meshray-core/config.go`：
  - `Config` 各字段增加 `toml` 标签（键名与 JSON 一致，snake_case）。
  - `loadConfig` 按扩展名自动选解码器：`.toml`/`.conf` → TOML；其它（含 `.json`）→ JSON，
    旧 JSON 配置继续可用。判断前先剥离 `.sample` 模板后缀，避免 `meshray-core.toml.sample`
    被误判为 JSON。
- `main.go`：默认配置路径由 `meshray-core.json` 改为 `meshray-core.toml`；`-c` 仍可指定任意路径。
- 新增样例 `core/cmd/meshray-core/meshray-core.toml.sample`。

### 注意（TOML 语义坑）
TOML 中 `[[peers]]` 数组表之后、下一个表头之前的键会被归入该 peers 元素。
故 `layer_strategy` / `layers` 等顶层键**必须置于 `[[peers]]` 之前**，否则会被误塞进 peers 表。
样例已据此排布，并加注释提醒。

### 验证
- 临时单测加载 `meshray-core.toml.sample`，确认 network_id / interface / mode / peers /
  ice / layer_strategy / layers 全部正确映射（已删除该临时测试）。
- `go build ./... && go vet ./...` → 通过。

## 2026-07-15 新增：传输层级支持手动指定（auto / manual）

### 需求
除默认的 9 层自动降级外，需支持手动指定传输层级，便于在「已知网络环境」下锁定链路
（如强制走 TURN-TCP 中继、或强制 Direct-UDP 直连），也便于排障时固定单链路复现。

### 配置入口（独立 meshray-core）
- `core/cmd/meshray-core/config.go`：`Config` 新增两字段
  - `layer_strategy`：`"auto"`（默认，9 层自动降级）或 `"manual"`（仅用下方 layers）。
  - `layers`：`[]string`，仅 manual 生效。单元素=强制固定层、禁用降级；
    多元素=按列表顺序自定义降级链。
- 支持别名解析（`core/connect/strategy.go` 新增 `ParseLayer`）：如 `udp`/`tcp`/`wss`/
  `webrtc`/`turn` 等均可识别。
- 示例：`{ "layer_strategy":"manual", "layers":["TURN-TCP"] }` 强制 TURN-TCP 中继；
  `{ "layer_strategy":"manual", "layers":["Direct-UDP","TURN-TCP"] }` 直连优先、不通再中继。

### 联动修复（降级顺序感知）
- `FallbackController` 原本按 `Layer` 枚举下标（`currentIndex+1`）降级/恢复，手动自定义顺序下
  会越界到错误层级。改为按 `StrategyScheduler.layerOrder` 顺序索引取上一/下一层
  （新增 `nextLayerInOrder` / `prevLayerInOrder`）。
- 由此，手动单层级场景下「无下一层」自然退化为不降级，语义正确。

### ICE 配置补齐（手动 TURN/WebRTC 必需）
- 独立二进制此前从未调用 `Engine.SetICEConfig`，导致 TURN/WebRTC 工厂拿不到 STUN/TURN 地址。
- `main.go` 现已把配置里的 `ice.stun` / `ice.turn` 下达到引擎，手动 TURN-TCP/TURN-TLS/
  WebRTC 层级方可真正建连。

### 管控面（ctr）
- `internal/ctr` 已通过 `update.EnabledLayers` → `SetLayerOrder` 支持自定义顺序，
  等价「手动多层级」，本次无需改动；后续若需「auto/manual」语义可在此扩展。

### 构建验证
- `core` 独立：`go build ./... && go vet ./...` → 通过。

## 2026-07-14 核心路基缺陷修复 (P0 + P1)

### 背景
此前验证集中在 HTTP/前端接口与页面，属表层验证。核心组网路基（WireGuard 增强模式
P2P 劫持转发链路）实际断裂。本次以单元测试钉死缺陷后，按 P0→P1 全修。

### P0-1 Bind 空指针崩溃
- 文件: `core/transport/conn_manager.go`
- 问题: `Add`/`Remove`/`CloseAll` 在 `conn==nil` 时调用 `conn.Close()`/`RemoteAddr()`
  空指针 panic。增强模式每次 `AddPeer` 都会直接崩。
- 修复: 三处全部对 `nil` conn 做安全处理（仅登记 peerKey，不调用 conn 方法）。

### P0-2 建连永不触发
- 文件: `core/engine.go`、`core/connect/direct.go`、`internal/ctr/ctr.go`、`internal/ctr/wg.go`
- 问题: `ctr` 从不调用 `engine.NotifyPeerInfo` 注入对端候选，`candidateStore` 恒空，
  `initiateConnection` 直接 return，9 层策略拨号永不触发；`DialConfig` 也无 `Candidates`，
  `DirectFactory` 拿不到对端地址。
- 修复:
  - `DialConfig` 增加 `Candidates` 字段。
  - `DirectFactory.Dial` 优先使用信令下发的候选地址。
  - `engine.initiateConnection` 将 `candidateStore` 转为 `DialConfig.Candidates`。
  - `WGManager.ListPeers` 从 `wgctrl` 读取真实 Endpoint 作为候选来源。
  - `ctr.SwitchMode` 切增强模式时注入对端真实 Endpoint 触发建连；
    新增 `NotifyPeerCandidates` 供信令层注入。

### P1-1 routeID 体系断裂（双向转发只通一半）
- 文件: `core/transport/relay.go`、`core/engine.go`
- 问题: `engine.Bind` 用 `extractRouteID(publicKey)`（前4字符 ASCII 和）注册本地端口，
  而 `relay.forwardIncoming` 用 WG 包 `receiver index` 查表，两套 routeID 无映射，
  对端→本地转发全部 miss。
- 修复: 转发路由统一改为 `peerKey`。删除伪造的 `extractRouteID`；
  `RegisterLocalPort`/`UnregisterLocalPort`/`StartReadFromLocalPort`/`forwardIncoming`
  全部以 `peerKey` 为索引。

### P1-2 UpdateCoreConfig 桩
- 文件: `internal/ctr/ctr.go`、`core/engine.go`、`core/connect/strategy.go`、`core/connect/ice.go`
- 问题: `UpdateCoreConfig` 直接 `return "尚未实现"`；`SetICEConfig` 仅打日志；
  WebRTC 工厂 ICE 无法更新。
- 修复:
  - 新增 `CoreConfigUpdate` 结构（STUN/TURN/启用层）。
  - `UpdateCoreConfig` 真实生效：下发 ICE 配置 + 设置传输层优先级顺序。
  - `engine.SetICEConfig` 真正调用 `scheduler.UpdateWebRTCICE`。
  - `scheduler` 新增 `UpdateWebRTCICE`；`WebRTCFactory` 新增 `UpdateICE`；
    `ICEClient` 新增 `UpdateConfig`。

### P1-1 后续清理：routeIDStore 死状态
- 文件: `core/engine.go`、`internal/ctr/ctr.go`、`core/engine_test.go`
- 问题: 上轮删除了 `extractRouteID` 并把转发路由统一为 `peerKey`，但 `Engine` 仍保留
  `routeIDStore map[string]uint32`，`NotifyPeerInfo` 仍接收并存储恒为 0 的 `routeID`，
  `initiateConnection` 仍查 `routeIDStore[peerKey]` 作为冗余闸门。该 map 实际只是
  “是否收到过 NotifyPeerInfo”的布尔标记，存的 0 无任何用途，且对其他触发路径是陷阱。
- 修复: 彻底删除 `routeIDStore` 字段、其初始化与清理、`NotifyPeerInfo` 的 `routeID` 参数
  及存储逻辑；`initiateConnection` 删除 “获取 RouteID” 冗余检查。路由判定完全统一为
  `candidateStore` 非空 + `peerKey` 索引。同步清理 `ctr`、`engine_test` 的调用点。

### 测试
- `core/transport/relay_test.go`: peerKey 路由匹配转发 / 未知丢弃。
- `core/engine_test.go`: Bind 不再崩溃、无候选不盲目建连、注入候选后链路接通。
- `core/connect/scheduler_test.go`: 策略层选择、DirectFactory 缺候选必败、
  候选地址真实建连（本地 listener 验证）。
- 验证说明: 沙箱 App Control 拦截 `core` 包测试二进制的执行；`core/connect`、
  `core/transport` 测试已成功运行通过；全量 `go build ./...` 与 `go vet ./core/...` 通过。

## 2026-07-15 架构重构：多仓解耦 + 编译期注入 + 闭源 core + conn.Bind 接管

### 背景
原架构把闭源增强逻辑（`core`）以本地子目录方式内嵌于开源管理器主仓，存在两点硬伤：
1. **开源条约风险**：开源主仓直接包含闭源 `core` 源码，无法干净剥离。
2. **Endpoint 劫持原理缺陷**：旧方案把 peer Endpoint 改写为回环端口，企图让 WG 入站
   流量经本地端口旁路；但 WG 入站按真实源地址匹配 peer，改写后源地址与 peer 不一致，
   **入站接不通**，9 层传输大部分是空壳，信令层缺失，零端到端验证。

经架构决策（用户确认）：
- 底层用开源 `wireguard-go`，增强层 `meshray-core` 闭源，二者物理边界清晰、
  可分开授权；管理器开源；**无 meshray-core 时系统仍以标准 WG 直连运行**。
- 代码物理边界 = **多仓解耦**（独立 module）。
- 增强层接入方式 = **编译期注入**（build tag 触发空白导入 `init()` 注册）。

### 关键契约：开源契约层（叶子 module）
- 新增 `pkg/contract`（`module git.zkcoi.com/zkcoi/meshray-contract`，开源叶子模块），
  作为主仓与 core 之间**唯一的 module 边界契约**，解决 `meshray ↔ meshray-core`
  双向依赖导致的 module 级循环依赖：
  - 定义 `Candidate`、`Layer`（9 层常量 + `String` + `DefaultLayerOrder`）、`ICEConfig`、
    `PeerStatus`、`EngineStatus`、`EngineController` 接口
    （`Start`/`Stop`/`NotifyPeerCandidates`/`SetICEConfig`/`SetLayerOrder`/`GetStatus`）。
  - 定义 `BindFactory = func(listenPort int, networkID string, logger *zap.Logger) (conn.Bind, error)`；
    注册表 `RegisterEnhancedBindFactory`/`GetBindFactory(enhanced bool)`：
    enhanced=false 返回 wireguard-go 默认 Bind（**保证无 core 仍可直连**）；
    enhanced=true 未注册时返回 `errBindNotEnhanced`。

### core 独立 module（闭源）
- 新增 `core/go.mod`（`module git.zkcoi.com/zkcoi/meshray-core`，go 1.26.0），
  require 主仓全部依赖 + `meshray-contract`，末尾
  `replace git.zkcoi.com/zkcoi/meshray-contract => ../pkg/contract`。
- 新增 `core/bind.go`：`EnhancedBind` 实现 `conn.Bind`，包裹 `conn.NewDefaultBind()`；
  `Send` 当前透传并预留 9 层策略插入点；`NewEnhancedBind` 调用 `getOrCreateEngine`。
- 新增 `core/registry.go`：`engines` 单例 + `getOrCreateEngine`；`engineController`
  适配 `contract.EngineController`；`init()` 注册 `RegisterEnhancedBindFactory` +
  `RegisterEngineProvider`（**仅编译进 core 时执行**）。
- 新增 `core/adapter.go`：契约 ↔ core 类型转换（候选 / ICE / 层顺序 / 状态）。
- `core/engine.go`：import 路径改 `.../meshray/core/` → `.../meshray-core/`；
  删除废弃的 `Bind(peerKey, localPort)` / `Unbind(peerKey)` 旁路方法（根治 Endpoint 旁路缺陷）；
  删除不再使用的 `"fmt"` import。
- `core/transport/plugin.go`：删除死代码 `ExtractRouteID` 接口方法。
- `core/plugins/wg/wgparse.go`：删除 `ExtractRouteID` 实现及无用 import，仅保留
  `IsControlPacket`/`IsDataPacket`。
- `core/engine_test.go`：删除依赖已废弃 `engine.Bind`/`Unbind` 的用例。

### 管理器主仓（开源）
- `go.mod`：删除旧 `replace .../core => ./core`；新增
  `replace .../meshray-contract => ./pkg/contract`、
  `replace .../meshray-core => ./core`；require 两新 module（均 `v0.0.0`）。
- 新增 `internal/ctr/core_enable.go`：`//go:build meshray_core` + `_ "git.zkcoi.com/zkcoi/meshray-core"`
  空白导入，触发 core `init()` 注册。
- `internal/ctr/wg.go`：`CreateDevice` 增 `meshMode` 参数；
  `startUserModeWGProcessWithRefs` 用 `contract.GetBindFactory(meshMode == "enhanced")`
  替代硬编码 `conn.NewDefaultBind()`（**交由 core 经 conn.Bind 接管 WG 收发**）；
  增强模式获取失败返回错误；`UpdatePeerEndpoint` 注释强调增强模式不应改写回环端口。
- `internal/ctr/ctr.go`：去除 `core` 具名 import；`coreInst *core.Core` →
  `engines map[string]contract.EngineController`；`CreateNetwork` enhanced 模式走
  `contract.NewEngineController`；`AddPeer`/`RemovePeer` 不再改写 Endpoint 为回环端口；
  `SwitchMode` 经 `engineCtl.NotifyPeerCandidates` 注入候选；
  `CoreConfigUpdate.TURNServers` 改 `[]string`、`EnabledLayers` 改 `[]contract.Layer`；
  `NetworkStatus.CoreStatus` 改 `*contract.EngineStatus`。

### 构建验证
- `core` 独立：`go build ./... && go vet ./...` → 通过。
- 管理器默认（无 core）：`go build ./...` → 通过（标准 WG 直连）。
- 管理器增强（编译期注入）：`go build -tags meshray_core ./...` → 通过。

### 仍然待补（非本次范围）
- `EnhancedBind.Send` 实际插入 9 层策略（当前透传）。
- 信令层真正驱动 `NotifyPeerCandidates`；P3 传输层空壳补全。
- `docs/` 下仍描述 “relay 按 route_id 查表转发” 的旧文档校正（用户规则禁止新增说明文档，
  已通过代码注释反映新路由逻辑）。

## 2026-07-15 增强 Bind 虚拟桥接 + meshray-core 独立二进制（类 frp）

### 背景
用户希望像 frp 一样直接运行 core 在两台机器上验证收发，而非必须经由管理器。
核查发现旧 `EnhancedBind` 仅是**纯透传**（Send 直接转交默认 UDP bind），而 9 层
`relay` 的 `localPorts` 在增强模式下从未被注册，导致 9 层传输逻辑根本没接到 WG 收发路径上。
本次把 `EnhancedBind` 重写为真正桥接 WG ↔ 引擎 9 层传输，并新增独立可运行二进制。

### 关键发现（wireguard-go 匹配机制）
阅读 `device/receive.go` 确认：传输包由包内 **receiver index** 匹配 peer，Endpoint 仅用于
握手更新对端出向地址与 MAC2 cookie 计算。因此回灌入站时只要携带**对端真实 Endpoint**，
即可正确接通，无需让 WG 监听真实 UDP 端口——这正是增强 Bind 完全接管收发的依据。

### core/bind.go（重写）
- `EnhancedBind` 不再依赖默认 bind 的真实 UDP 监听：`Open` 返回从 `inboundCh` 读取引擎
  入站的接收函数；`Send` 按 `ep` 反查 peerKey，经引擎 9 层传输连接发送，连接未就绪时
  惰性触发拨号并走直连兜底。
- `deliverInbound`：引擎传输连接读到的入站密文投入 `inboundCh`，由接收函数拷贝进 WG
  缓冲并填入对端真实 Endpoint 回灌。
- `RegisterPeer(addr, peerKey)`：维护 endpoint→peerKey 与 peerKey→真实 Endpoint 映射。
- 接收函数严格遵守 WG 缓冲所有权（拷贝而非替换指针），`BatchSize`/`ParseEndpoint` 委托默认 bind。

### core/transport/relay.go
- 新增 `OnInbound func(peerKey string, packet []byte)` 钩子；`StartReadFromRemoteConn` 设置后
  入站改投该钩子（即 EnhancedBind），否则走原本地端口转发（向后兼容）。

### core/engine.go
- `Engine` 增加 `bind *EnhancedBind` 字段（由 `NewEnhancedBind` 回填）。
- `NotifyPeerInfo` 在收到候选时自动 `RegisterPeer`，并新增 `EnsureDial`（Send 惰性拨号用）。

### core/cmd/meshray-core（新增独立二进制）
- `main.go` + `config.go`：类 frp 风格，`-c config.json` 启动、`-genkey` 生成私钥。
- 直接以 `core.NewEnhancedBind` 接管 wireguard-go 用户态设备收发；按配置创建 TUN、
  注入对端候选触发 9 层拨号、`Up` 后分配隧道 IP（Linux `ip` / Windows `netsh`）。
- `mode: enhanced` 走 9 层接管；`mode: native` 退化为裸 WG 直连，便于对照测试。
- 新增 `meshray-core.example.json` 示例配置（占位符，用户替换公私钥/endpoint 即可两机对测）。
- `go.mod`：`go mod tidy` 引入 `wireguard/wgctrl` 与 Windows `wintun` 依赖。

### 构建验证
- `core` 独立：`go build ./... && go vet ./...` → 通过；`go build -o meshray-core.exe ./cmd/meshray-core` 通过；
  `./meshray-core.exe -genkey` 正常输出私钥。
- 管理器默认：`go build ./...` → 通过；增强：`go build -tags meshray_core ./...` → 通过。

### 两机对测步骤（摘要，详见二进制内注释与示例配置）
1. 两台机器各自 `meshray-core -genkey` 得私钥，用 `private_key` 对应公钥填对方 `public_key`。
2. A 的 `interface=10.0.0.1/24`、`peers[0].endpoint=B公网IP:51820`、`allowed_ips=10.0.0.2/32`；B 对称。
3. 两台均 `meshray-core -c meshray-core.json`，随后 `ping 10.0.0.x` 验证隧道收发。
4. Windows 需已安装 Wintun 驱动（随 WireGuard 客户端提供）。

## 2026-07-15 修复：初始直连缺少入站读取，公网/独立 IP 机器直连亦不通

### 背景（用户纠正）
用户指出 meshray-core 的核心机制必须**同时**满足「NAT 后的机器」与「有独立 IP 的机器」，
二者都要能直连（P2P）→ 失败再走中继/打洞兜底。这意味着即使两台公网机器直连，
当前实现也应能接通。实测与代码核查发现：**初始直连路径本身就没接通**，与是否 NAT 无关。

### 根因
- `StrategyScheduler.Dial`（`core/connect/strategy.go:284`）成功后**仅返回连接，从不调用 `OnConnectionUpdate`**。
- `OnConnectionUpdate` 回调（`core/engine.go:45`）才是唯一会调用
  `relay.StartReadFromRemoteConn` 启动入站读取的地方；而它只在 `reconnectToLayer`
  （降级重连）与 `probeHigherLayers`（恢复探测）内触发。
- `Engine.initiateConnection` 拿到 `Dial` 返回的直连后只做了 `connMgr.Add`，**未启动入站读取协程**。

### 断点后果
两侧都只发出握手、收不到对方回包 → 握手无法完成 → 隧道起不来。
该缺陷在「两台公网/独立 IP 机器直连」场景下同样存在，并非 NAT 专属问题。

### 修复
- `core/engine.go`：`initiateConnection` 在 `connMgr.Add` 之后显式调用
  `e.relay.StartReadFromRemoteConn(context.Background(), peerKey, conn)`，
  使初始直连的入站密文能经 `OnInbound` 回灌 WG（增强模式）或写入本地端口。
- 降级重连 / 恢复探测仍走 `OnConnectionUpdate` 原路径，无重复启动冲突
  （旧连接被 `connMgr.Add` 关闭后其读取协程自然退出）。

### 构建验证
- `core` 独立：`go build ./... && go vet ./...` → 通过。
- 注：沙箱无法运行 TUN/Wintun，端到端需用户在两台真实机器（或同机双进程）验证
  `ping 10.0.0.x` 收发。修复后公网 IP / 同局域网直连场景预期可正常握手建隧。
