根据我的分析,以下是隧道链路构建时下一跳连接地址选择流程的完整报告: --- ## 隧道链路构建流程分析报告 ### 1. 隧道链路配置发送到节点的代码位置 **主函数**: `applyTunnelRuntime()` - 文件: `go-backend/internal/http/handler/mutations.go` - 行号: 2454-2521 **核心流程**: ``` applyTunnelRuntime (L2454) ├── 入口节点: 发送 AddChains 命令 (L2474) ├── 跳转节点: 发送 AddChains (L2496) + AddService (L2502) 命令 └── 出口节点: 发送 AddService 命令 (L2514) ``` **配置构建函数**: | 函数 | 位置 | 作用 | |------|------|------| | `buildTunnelChainConfig()` | L2567-2624 | 构建转发链配置 | | `buildTunnelChainServiceConfig()` | L2626-2652 | 构建服务配置 | | `selectTunnelDialHost()` | L2654-2699 | **选择下一跳连接地址** | --- ### 2. 下一跳连接地址是如何确定的 **地址选择核心函数**: `selectTunnelDialHost()` (L2654-2699) ```go // 关键逻辑 (简化版) func selectTunnelDialHost(fromNode, toNode *nodeRecord, ipPreference string) (string, error) { // 1. 检测源节点和目标节点的 IPv4/IPv6 支持 fromV4 := nodeSupportsV4(fromNode) fromV6 := nodeSupportsV6(fromNode) toV4 := nodeSupportsV4(toNode) toV6 := nodeSupportsV6(toNode) // 2. 根据偏好选择地址 switch ipPreference { case "v6": // 优先 IPv6 if fromV6 && toV6 { return pickNodeAddressV6(toNode) } if fromV4 && toV4 { return pickNodeAddressV4(toNode) } case "v4": // 优先 IPv4 if fromV4 && toV4 { return pickNodeAddressV4(toNode) } if fromV6 && toV6 { return pickNodeAddressV6(toNode) } default: // 默认优先 IPv4 if fromV4 && toV4 { return pickNodeAddressV4(toNode) } if fromV6 && toV6 { return pickNodeAddressV6(toNode) } } // 3. 不兼容时报错 return "", fmt.Errorf("节点链路不兼容") } ``` **地址选择优先级** (在 `pickNodeAddressV4/V6` 中, L2755-2773): - **IPv4**: `ServerIPv4` > `ServerIP` (旧字段) - **IPv6**: `ServerIPv6` > `ServerIP` (旧字段) **端口来源** (在 `buildTunnelChainConfig` 中, L2585-2588): ```go port := target.Port // 来自 ChainTunnel.Port 字段 if port <= 0 { return nil, errors.New("节点端口不能为空") } ``` --- ### 3. ChainTunnel.Port 字段的使用流程 **数据模型** (`model.go` L127-138): ```go type ChainTunnel struct { ID int64 TunnelID int64 ChainType string // "1"=入口, "2"=跳转, "3"=出口 NodeID int64 Port sql.NullInt64 // ★ 连接端口 Strategy sql.NullString Inx sql.NullInt64 // 跳转序号 Protocol sql.NullString } ``` **Port 字段流转**: ``` 创建隧道时 │ ▼ prepareTunnelCreateState (L1970) ├── 出口节点: 从请求读取 port,若为空则调用 PickNodePortTx 自动分配 ├── 跳转节点: 从请求读取 port,若为空则调用 PickNodePortTx 自动分配 │ ▼ replaceTunnelChainsTx (L2775) └── CreateChainTunnelTx 写入数据库 (L2809, L2837) │ ▼ 读取/重建状态时 │ ▼ ListChainNodesForTunnel (repository_control.go L280) └── 从 chain_tunnel 表读取 Port 字段 (L295) │ ▼ reconstructTunnelState (L845) └── 将 Port 复制到 tunnelRuntimeNode.Port (L887, L901) │ ▼ buildTunnelChainConfig (L2567) └── 使用 target.Port 构建连接地址 (L2585) ``` --- ### 4. 修改连接地址需要改动的关键位置 如果要修改下一跳连接地址的选择逻辑,需要关注以下位置: | 修改目标 | 文件 | 行号 | 函数 | |---------|------|------|------| | **IP地址选择策略** | `mutations.go` | L2654-2699 | `selectTunnelDialHost()` | | **IPv4地址提取** | `mutations.go` | L2755-2763 | `pickNodeAddressV4()` | | **IPv6地址提取** | `mutations.go` | L2765-2773 | `pickNodeAddressV6()` | | **端口获取** | `mutations.go` | L2585-2588 | `buildTunnelChainConfig()` | | **端口分配逻辑** | `repository_mutations.go` | ~L459 | `PickNodePortTx()` | | **端口写入数据库** | `repository_mutations.go` | L398-406 | `CreateChainTunnelTx()` | | **端口从数据库读取** | `repository_control.go` | L280-343 | `ListChainNodesForTunnel()` | **关键配置结构**: ```go // mutations.go L1950-1957 type tunnelRuntimeNode struct { NodeID int64 Protocol string // 连接协议 Strategy string // 选择策略 Inx int // 跳转序号 ChainType int // 1=入口, 2=跳转, 3=出口 Port int // ★ 连接端口 } ``` --- ### 5. 调用链总结 ``` 用户创建隧道请求 │ ▼ tunnelAdd (mutations.go L450) │ ├── prepareTunnelCreateState() ──► 分配端口,构建 state │ ├── replaceTunnelChainsTx() ──► 写入 chain_tunnel 表 │ └── applyTunnelRuntime() │ ├── buildTunnelChainConfig() │ │ │ ├── selectTunnelDialHost() ──► 选择 IP 地址 │ │ │ └── 使用 target.Port ──► 端口 │ └── sendNodeCommand("AddChains/AddService") ──► 发送到节点 ```