根据我的分析，以下是隧道链路构建时下一跳连接地址选择流程的完整报告：

---

## 隧道链路构建流程分析报告

### 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") ──► 发送到节点
```