Skip to content

端口管理

状态:Current

最后核对:2026-05-15

路由:/nodes/ports

端口管理用于展示 Nexus Agent 已知的 L4 监听端口、请求每个端口的 L4/L3 服务,以及 Agent 当前上报的运行时 listener 状态。

它不是创建上游目标的地方。上游目标仍由 L4 Upstreams 管理。它也不是创建转发链的地方。真实业务流量规则仍由 L4 Chains 和 L3 服务生成。本页管理的是 listener namespace:节点、transport、绑定地址、端口、关联服务、意图路由和运行时 listener 状态。

核心模型

区域负责内容示例
L4 上游可复用目标端点10.0.0.10:443service.internal:8443
L4 链可部署转发规则TCP TLS 链、UDP Noise 链、QUIC REALITY hop
端口管理listener 归属、服务列表、路由可视化和运行时状态node-a tcp :::443,L4/L3 服务

一个端口不是一个服务。一个端口是一个 listener namespace。服务来自 L4 链或 L3 路由能力。端口管理会把同一个 namespace 上的服务全部展示出来,并帮助操作员对比控制平面意图和 Agent 当前运行时 listener 表。

后端仍保留 listener priority policy,用于 API 兼容和自动化部署,但当前端口管理 UI 不再提供保存该策略的按钮。Agent 不会简单地把所有“非 winner”服务丢掉;它会先判断协议是否具备可靠早期分流信号,能合并就构建同端口 route table,只有无法分流的重复项才会被丢弃,例如同一个 listener 上第二个 RAW fallback。

同协议内部能否复用同一个端口,取决于协议是否有可靠的早期分流信号:

  • TCP REALITY 在线路上是 TLS-like:先按 TLS SNI 分流,同一个 SNI 下还可以通过不同 short ID 保留多个 REALITY route。
  • TCP TLS 可以通过 TLS SNI 分流。
  • UDP Noise 可以通过能解开首个 datagram 的本地 X25519 key 分流。
  • QUIC REALITY 可以和 UDP RAW 或 UDP Noise 共用 UDP socket;Agent 只把看起来像 QUIC 的包送给嵌入的 QUIC endpoint,普通 UDP 流量继续走 UDP router。
  • RAW 没有分流信号,无法可靠地让多个独立 RAW 服务共用同一个 listener。

控制平面流程

  1. 操作员创建或修改 L4 链。
  2. 后端把链定义保存到 tunnel_chains
  3. 操作员部署链或服务变更。
  4. 后端仍可把可选 listener 策略保存到 node_port_policies,用于兼容和 API 管理的部署。
  5. 部署链或更新策略时,后端对该节点 replay 所有启用的 L4 链。
  6. 后端下发一个 PingoraConfig,其中包含 upstream、TCP rules、QUIC rules、UDP rules、TLS config 和 listener_policies
  7. Agent 检查本机端口可绑定性,合并同端口 route table,只丢弃不可分流冲突,并 reconcile 运行时 listener。
  8. Agent heartbeat 把最终运行时 listener 表上报给后端。

Agent 做最终裁决,因为只有 Agent 知道本机 socket 状态。后端保存意图,Agent 负责安全地应用。

Listener 标识

相同数字端口可以出现多次,因为 TCP 和 UDP 是不同的内核 namespace。

字段含义
node_id拥有该 listener 的 Agent 节点。
transporttcpudp;QUIC 属于 UDP namespace。
listen_addr绑定地址。当前 L4 链部署通常下发 ::
listen_port数字端口,范围 1 到 65535。

示例:

场景是否同一个 listener说明
tcp :::443udp :::443不是TCP TLS 和 UDP Noise 可以使用同一个数字端口,因为 TCP 和 UDP 分开绑定。
udp :::443 UDP RAW 和 udp :::443 QUIC REALITYQUIC 使用 UDP,但 QUIC REALITY 可以嵌入选中的 UDP socket,RAW fallback 仍接收非 QUIC 流量。
tcp :::443 TCP TLS 和 tcp :::443 TCP REALITY二者都使用 TCP。Agent 会构建 TLS/SNI-family listener,并按握手信号分流 TLS、REALITY 和 RAW fallback。

兼容策略

默认顺序:

text
REALITY > TLS > Noise > RAW

API 和数据库中使用小写标签:

json
["reality", "tls", "noise", "raw"]

优先级顺序是兼容策略,只用于仍无法合并到一个 route table 的场景。只有当前 listener 上实际存在的协议类型会参与选择。如果保存的策略漏掉某个合法标签,后端和 Agent 会按默认顺序补齐缺失标签。

当前前端不再暴露策略保存 icon。API client 仍可保存策略;保存策略会触发该节点的 L4 配置 replay。策略本身不会创建业务规则。没有匹配的 L4 链规则时,它只是一个 policy-only listener。

冲突解析

Agent 行为可简化为:

text
for each listener namespace:
  collect TCP rules by tcp listen address
  collect UDP rules and QUIC rules by udp listen address
  if TCP has REALITY, TLS, or SSLPREREAD routes:
    build one TLS/SNI-family listener
    add all matching REALITY routes, including same-SNI routes with different short IDs
    add TLS/SSLPREREAD routes by SNI
    keep at most one TCP RAW fallback for non-TLS or unmatched-SNI traffic
  else:
    keep the single selected TCP route class
  if UDP has RAW or Noise routes:
    build one UDP listener
    add Noise routes by local X25519 key
    keep at most one UDP RAW fallback
    embed same-address QUIC REALITY routes into the same UDP socket
  else:
    build a standalone QUIC listener when only QUIC routes request the namespace

详细行为:

获胜类型复用行为
TCP REALITY支持多个 REALITY route 共用一个 TCP listener。REALITY 被当成 TLS-like:listener 先按 SNI 分流,再对所有 SNI 命中的 REALITY route 逐个尝试 short ID。
TCP TLS支持多个 TLS route 共用一个 TCP listener,通过预读 TLS ClientHello 并按 SNI 分流。也可以保留一个 TCP RAW route,作为非 TLS 或 SNI 未命中流量的默认 fallback。
UDP Noise支持多个 UDP Noise route 共用一个 UDP listener,通过首个 datagram 尝试候选本地 X25519 key,然后把 UDP session 固定到选中的 route。
QUIC REALITYQUIC 属于 UDP namespace。QUIC REALITY route 可以嵌入 UDP RAW 或 UDP Noise 使用的同一个 UDP socket,同时 QUIC 自己的 REALITY 路由仍可按 server name 和 short ID 分流。
RAW没有分流信号。同一个 selected listener 最多只能有一个 RAW route。多个 RAW route 共用同一 listener 没有可靠语义。

TCP RAW Fallback

TCP 的 TLS/SNI-family listener 可以保留一个 RAW route 作为明确的默认 fallback。TLS/SNI router 会先读取 TLS ClientHello:

  • 如果 SNI 命中已配置的 TLS 或 SSLPREREAD route,流量走该 route;
  • 如果 SNI 命中 REALITY route,Agent 会逐个尝试候选 REALITY route,直到 short ID 鉴权成功;
  • 如果首包不是 TLS,或 SNI 没命中,流量落到 RAW route;
  • 如果同一个 listener 上有多个 RAW rule,只能保留一个,因为 RAW 没有分流信号。

这就是 TCP TLS/SNI 控制 listener 和 SSH RAW listener 共用同一个 TCP 端口的语义。REALITY 能共存于同一个 listener,是因为它在 REALITY short ID 检查前就能从 TLS-like ClientHello 中识别出来。

UDP 共享 Socket

UDP 下,同一个 socket 可以同时承载 UDP RAW fallback、UDP Noise route 和嵌入式 QUIC REALITY route,只要它们请求的是同一个 udp address:port

分发规则:

  • 已知 QUIC peer 的 QUIC 包继续交给 Quinn;
  • 未知 peer 只有在包看起来是 QUIC long-header Initial 时才交给 Quinn;
  • Noise 候选 route 使用配置的本地 key 尝试解密首个 datagram;
  • 如果没有 QUIC 或 Noise route 接受该 datagram,则交给唯一 UDP RAW fallback。

这样普通 UDP RAW 流量不会被 QUIC endpoint 吞掉,同时 QUIC REALITY 仍能与普通 UDP listener 共用一个端口。

协议分流矩阵

Listener namespace协议类型分流键同端口 route说明
TCPREALITYTLS-like ClientHello SNI,然后 REALITY short ID多个同 SNI 下 short ID 不同的多个 REALITY route 是合法的。
TCPTLSTLS ClientHello SNI多个,每个 SNI 一个cert_domain 不能用于 SNI 分流。可保留一个 RAW fallback。
TCPRAW一个无协议层分流信号。可作为 TLS/SNI listener 下的默认 fallback。
UDPQUIC REALITYQUIC Initial 检测,然后 REALITY route 材料内部多个可以嵌入共享 UDP socket,与 UDP RAW 和 UDP Noise 共存。
UDPNoise首包用本地 X25519 key 解密匹配多个,每个本地 key 一个首包后 session 固定到 route。
UDPRAW一个可作为共享 UDP socket 的 fallback;两个 RAW UDP 服务仍不能共用同一个 listener。

Noise 和 TLS 能否共用端口

可以共用同一个数字端口,但前提是一个在 UDP namespace,一个在 TCP namespace。

例如 tcp :::443 的 TCP TLS 和 udp :::443 的 UDP Noise 可以同时存在。它们不是同一个 listener,也不会互相参与优先级裁剪。

QUIC REALITY 和 UDP Noise 都在 UDP namespace,但 QUIC REALITY 可以嵌入选中的 UDP socket executor,所以 QUIC REALITY、UDP Noise 和一个 UDP RAW fallback 可以共用同一个 udp address:port。TCP TLS 和 TCP REALITY 都在 TCP namespace;当它们的 SNI 和 REALITY short ID 材料明确时,可以共存于同一个 TLS/SNI-family listener。

Validator 风险

这里的 validator 风险指控制平面校验、前端展示、Agent 实际运行三者之间的语义漂移。

典型情况:

  • 后端允许保存一组规则,但 Agent 因系统占用或不可分流的重复项丢弃其中一部分,例如多个 RAW fallback。
  • 前端展示的是控制平面意图,但 Agent runtime 还没有上报最终 reconcile 后的 listener 表。
  • 默认优先级只存在于隐藏的 Agent 逻辑里,后端和前端状态里没有表达。

当前实现的缓解措施:

缓解措施说明
统一策略存储node_port_policies 仍是 API 管理的每节点 listener 优先级兼容存储。
Proto 下发PingoraConfig.listener_policies 把策略下发给 Agent。
Agent 最终裁决Agent 仍做最终绑定和冲突裁决,因为只有它知道本机 socket 状态。
Runtime heartbeat 状态HeartbeatRequest.runtime_listeners 上报 Agent 实际应用后的 listener 状态。
Runtime preview前端预览按钮加载 /api/nodes/ports/runtime,不再依赖本地草稿计算。
人类可读日志Agent 日志包含 selected priority、dropped priority、listener address、cluster 和 rule fingerprint。
聚焦测试测试覆盖 TCP SNI-family 共存、UDP Noise key 分流、UDP RAW fallback,以及 QUIC REALITY 和普通 UDP route 共用 UDP socket。

系统已占用端口

如果某个 address:port 已经被非 Nexus 进程监听,Agent 不会自动换端口,也不会静默跳过 listener。该次配置应用会被拒绝,并通过 ConfigApplyReport 回报。

对 TCP,Agent 检查 TCP address:port。对 UDP 和 QUIC,Agent 检查 UDP address:port namespace。已由 Nexus 管理的现有 listener 可以热更新,不算系统占用。

前端操作流程

  1. 打开 节点 > 端口管理
  2. 从下拉框选择节点,或保持 All nodes
  3. 查看每一行:
    • 节点名和 id 前缀;
    • transport,例如 tcpudp
    • listener namespace,例如 :::443
    • 请求该 listener 的 L4 或 L3 服务来源;
    • 意图 route 数量,以及可用时的运行时状态。
  4. 点击 service/order 按钮打开可拖拽服务列表。弹窗会逐个列出必须参与处理的 L4 或 L3 服务;当前 UI 不会从这个弹窗保存优先级策略。
  5. 点击 runtime preview 按钮,从 Agent 上报表获取当前 listener。弹窗调用 /api/nodes/ports/runtime 时会显示 loading。
  6. 对比 runtime route list 和意图服务列表。缺少 runtime 数据通常表示 Agent 未连接、尚未 heartbeat 上报 runtime listener,或最近一次配置应用被拒绝。
  7. 如果端口被系统占用或不可分流重复项被丢弃,查看 Agent 日志。

REST API

列出 L4 端口

http
GET /api/nodes/ports
GET /api/nodes/ports?node_id=<uuid>

返回所有节点选项和 listener 项。node_id 只过滤 listener 项,不会让节点下拉框只剩当前节点。

响应结构:

json
{
  "success": true,
  "data": {
    "nodes": [
      { "id": "node-uuid", "name": "hk-edge-1", "status": "online" }
    ],
    "items": [
      {
        "nodeId": "node-uuid",
        "nodeName": "hk-edge-1",
        "transport": "tcp",
        "listenAddr": "::",
        "listenPort": 443,
        "priorityOrder": ["reality", "tls", "noise", "raw"],
        "hasPolicy": true,
        "policyId": "policy-uuid",
        "routes": [
          {
            "chainId": "chain-uuid",
            "chainName": "tls-edge",
            "source": "l4_chain",
            "sourceLabel": "L4 Chain",
            "payload": "tcp",
            "outer": "tcp",
            "security": "tls",
            "matchKind": "sni",
            "matchValue": "example.com",
            "credentialId": "credential-uuid",
            "nextHop": "target.example.com:443"
          },
          {
            "chainId": "chain-uuid",
            "chainName": "tls-edge",
            "source": "l4_upstream",
            "sourceLabel": "api-origin",
            "payload": "tcp",
            "outer": "tcp",
            "security": "tls",
            "matchKind": "sni",
            "matchValue": "api.example.com",
            "credentialId": null,
            "nextHop": "10.0.0.5:443"
          }
        ]
      }
    ],
    "defaultPriorityOrder": ["reality", "tls", "noise", "raw"]
  },
  "error": null
}

/api/nodes/ports 响应的是控制平面意图,由 chains、upstreams、policies 和 service definitions 计算得到。如果问题是“Agent 现在实际监听了什么”,使用下面的 runtime endpoint。

列出运行时 L4 端口

http
GET /api/nodes/ports/runtime
GET /api/nodes/ports/runtime?node_id=<uuid>&transport=tcp&listen_addr=::&listen_port=443

返回 Agent 最近一次通过 heartbeat 上报的运行时 listener 表。query 参数均为可选过滤条件。

响应结构:

json
{
  "success": true,
  "data": {
    "nodes": [
      {
        "nodeId": "node-uuid",
        "nodeName": "hk-edge-1",
        "status": "online",
        "reportedAt": "2026-05-15T10:20:30Z",
        "listeners": [
          {
            "transport": "tcp",
            "listenAddr": "::",
            "listenPort": 443,
            "state": "running",
            "routes": [
              {
                "service": "154.88.64.117:55443",
                "payload": "tcp",
                "outer": "tcp",
                "security": "reality",
                "matchKind": "reality",
                "matchValue": "SNI img.alicdn.com / short_id 0123456789abcdef",
                "nextHop": "154.88.64.117:55443"
              },
              {
                "service": "127.0.0.1:22",
                "payload": "tcp",
                "outer": "tcp",
                "security": "raw",
                "matchKind": "default",
                "matchValue": "default",
                "nextHop": "127.0.0.1:22"
              }
            ],
            "bytesIn": 0,
            "bytesOut": 0,
            "activeSessions": 0,
            "totalSessions": 0,
            "errorSessions": 0
          }
        ]
      }
    ]
  },
  "error": null
}

runtime preview 弹窗使用这个 endpoint。由于数据依赖 Agent 连接和 heartbeat 新鲜度,弹窗必须处理 loading 和 error 状态。

保存 Listener 策略

http
PUT /api/nodes/ports/policies

需要角色:adminoperator

该 endpoint 保留给 API 兼容和 operator automation。当前端口管理页不再提供该策略的保存 icon。

请求:

json
{
  "nodeId": "node-uuid",
  "transport": "udp",
  "listenAddr": "::",
  "listenPort": 443,
  "priorityOrder": ["raw", "reality", "noise", "tls"]
}

行为:

字段规则
listenPort必须在 1 到 65535。
transport必须是 tcpudp
priorityOrder只能包含 realitytlsnoiseraw

后端会去重、规范化并补齐缺失协议。保存后会 replay 该 node 的 L4 chain config。

runtime preview 不再应用前端本地 draft 顺序。它展示 /api/nodes/ports/runtime 返回的 Agent 运行时 route table。

数据库

策略表是 node_port_policies

字段含义
id策略 ID。
node_id所属节点,引用 nodes(id)
transporttcpudp
listen_addrlistener 绑定地址。
listen_portlistener 端口,1 到 65535。
priority_order协议标签 JSONB 数组。
enabled策略是否启用。
created_atupdated_at时间戳。

唯一约束:

text
UNIQUE (node_id, transport, listen_addr, listen_port)

这意味着同一节点可以分别拥有 tcp :::443udp :::443 的策略。

Agent 控制协议

PingoraConfig 携带 listener 策略:

protobuf
message PingoraConfig {
  repeated UpstreamCluster upstreams = 1;
  repeated TcpProxyRule tcp_rules = 2;
  repeated HttpProxyRule http_rules = 3;
  TlsConfig tls_config = 4;
  repeated QuicRule quic_rules = 5;
  repeated UdpRule udp_rules = 6;
  repeated ListenerPolicy listener_policies = 7;
}

message ListenerPolicy {
  string transport = 1;
  string listen_addr = 2;
  int32 listen_port = 3;
  repeated string priority_order = 4;
}

后端和 Agent 仓库各有一份 common/proto/agent.proto;两份必须保持一致。

Agent heartbeat 也会携带当前运行时 listener 表:

protobuf
message HeartbeatRequest {
  string node_id = 1;
  uint64 sequence = 2;
  google.protobuf.Timestamp timestamp = 3;
  double cpu_usage = 4;
  double memory_usage = 5;
  uint64 uptime_seconds = 6;
  repeated LinkMetric link_metrics = 7;
  repeated RuntimeListener runtime_listeners = 8;
}

message RuntimeListener {
  string transport = 1;
  string listen_addr = 2;
  int32 listen_port = 3;
  string state = 4;
  repeated RuntimeRoute routes = 5;
  uint64 bytes_in = 6;
  uint64 bytes_out = 7;
  uint64 active_sessions = 8;
  uint64 total_sessions = 9;
  uint64 error_sessions = 10;
}

message RuntimeRoute {
  string service = 1;
  string payload = 2;
  string outer = 3;
  string security = 4;
  string match_kind = 5;
  string match_value = 6;
  string next_hop = 7;
}

日志与排障

常见 Agent 日志模式:

日志含义检查点
Gateway config validated对比 requested 和 selected 的 TCP/QUIC/UDP 规则数量。
TCP SNI-family port reuse enabledTCP REALITY、TLS/SSLPREREAD 和 RAW fallback 已合并到同一个 listener。
UDP same-port route table enabledUDP RAW、UDP Noise 或嵌入式 QUIC REALITY route 已合并到同一个 UDP listener。
Embedding QUIC REALITY route into the selected UDP socket executorQUIC REALITY 正在共享选中的 UDP socket,而不是打开独立 QUIC socket。
Dropping duplicate TCP RAW fallback because another RAW route already owns this listener fallback同一个 TCP SNI-family listener 只能有一个 TCP RAW fallback。
Dropping duplicate UDP RAW fallback because another RAW route already owns this listener fallback同一个 UDP listener 只能有一个 UDP RAW fallback。
already bound outside this manager其他进程占用了端口;该快照会被拒绝。停止该进程或换端口。

排障清单:

  1. 确认 L4 链已启用。
  2. 确认最近修改后已重新 deploy。 3.打开端口管理,确认对应 listener 行存在。
  3. 打开 runtime preview,确认 Agent 上报了该 listener 和预期 route。
  4. 检查 Agent 主机上是否有非 Nexus 进程占用端口。
  5. 检查凭据是否缺失或不匹配:
    • TLS 需要带明确 domain 的 TLS certificate。
    • REALITY 需要 server names 和 short IDs。
    • UDP Noise 需要有效 X25519 key material。
  6. 查看 Agent 日志中的 dropped rules。

性能说明

listener 合并的主要成本发生在配置应用阶段:按 listener 分组、校验 route 是否可分流、在需要时保留单个 fallback,并构建运行时 route table。运行时成本取决于 listener 组成:

  • TCP TLS SNI 分流会预读 TLS ClientHello 的首个 TLS record,然后把该 record 回填给 rustls 继续正常握手。
  • TCP REALITY 选择先使用 SNI,再对多个 SNI 命中的 REALITY route 尝试 short ID 鉴权。
  • UDP Noise 分流只在新 UDP session 的首个 datagram 上尝试候选 key;选中后 session 固定到 route。
  • QUIC 共享 UDP socket 时,未知 peer 只有 QUIC long-header Initial 才会交给 Quinn;已知 QUIC peer 可以继续使用 fixed-bit QUIC 包。非 QUIC UDP 会继续落到 Noise 或 RAW。
  • runtime preview 的新鲜度受 Agent heartbeat 周期限制。
  • REALITY、TLS、Noise、QUIC 的同端口复用不是零成本,但正常 route 数量下成本很小。

如果在一个端口上放入大量 SNI、REALITY 或 Noise key,应关注首包/握手延迟和日志中的 route 数量。

设计规则

应该避免
多个 TLS 服务需要共用 TCP 端口时使用 TCP TLS SNI。不要把两个 RAW 服务放到同一 listener 并期待可分流。
多个 UDP Noise 服务需要共用 UDP 端口时使用 key 分流。不要假设 TCP 和 UDP 优先级会互相影响,它们是不同 namespace。
QUIC 和普通 UDP 必须共用 UDP 端口时,使用 QUIC REALITY 共享 UDP 分发。不要假设所有 QUIC 模式都能共享 UDP socket;当前语义针对共享 listener 处理的 QUIC REALITY route material。
部署后用 runtime preview 对比控制平面意图。不要依赖自动换端口,当前明确禁用。
使用 policy API 时,在变更评审中同时检查 listener policy 和 chain deployment。不要把 policy-only listener 当成已有流量 listener。

NexusNet documentation