Skip to content

Port Management

Status: Current

Last reviewed: 2026-05-15

Route: /nodes/ports

Port Management shows Nexus-owned listener ports, the L4/L3 services that request each listener, and the agent-reported runtime state for those listeners.

It does not create upstream targets. Targets remain managed by L4 Upstreams. It does not create forwarding chains. Traffic rules still come from L4 Chains and L3 services. This page manages the listener namespace: node, transport, bind address, port, associated services, intended routes, and runtime listener state.

Core Model

AreaOwnsExample
L4 UpstreamsReusable target endpoints and inbound SNI match names10.0.0.10:443, service.internal:8443
L4 ChainsDeployable forwarding rulesTCP TLS chain, UDP Noise chain, QUIC REALITY hop
Port ManagementListener ownership, service list, route visibility, and runtime statenode-a tcp :::443, L4/L3 services

A port is not a service. A port is a listener namespace. Services come from L4 Chains or L3 routing features. Port Management makes every service on the same namespace visible and lets operators compare control-plane intent with the agent's current runtime listener table.

The backend still stores listener priority policies for API compatibility, but the current Port Management UI does not expose a save action for those policies. The agent does not blindly drop every non-winning service. It first builds a same-port route table whenever the protocols have reliable early demultiplexing signals, and only drops impossible duplicates such as a second RAW fallback on the same listener.

Same-protocol port reuse is only supported when the protocol has a reliable early demultiplexing signal:

  • TCP REALITY is TLS-like on the wire: it routes by TLS SNI first and can keep multiple REALITY routes with the same SNI when their short IDs differ.
  • TCP TLS can route by TLS SNI.
  • UDP Noise can route by the local X25519 key that decrypts the first datagram.
  • QUIC REALITY can share a UDP socket with UDP RAW or UDP Noise; the agent dispatches QUIC-looking packets to the embedded QUIC endpoint and lets normal UDP traffic continue through the UDP router.
  • RAW has no discriminator and cannot safely host multiple independent RAW services on the same listener.

Control-Plane Flow

  1. An operator creates or updates L4 Chains.
  2. The backend stores chain definitions in tunnel_chains.
  3. An operator deploys chain or service changes.
  4. The backend stores optional listener policy in node_port_policies for compatibility and API-managed deployments.
  5. On chain deploy or policy update, the backend replays all enabled L4 chains for the node.
  6. The backend sends one PingoraConfig containing upstreams, TCP rules, QUIC rules, UDP rules, TLS config, and listener_policies.
  7. The agent validates local bind availability, merges same-port route tables, drops only non-demultiplexable conflicts, and reconciles runtime listeners.
  8. Agent heartbeats report the resulting runtime listener table back to the backend.

The agent makes the final decision because only the agent can know local socket state. The backend stores intent; the agent applies it safely.

Listener Identity

The same numeric port can appear more than once because TCP and UDP are separate kernel namespaces.

FieldMeaning
node_idAgent node that owns the listener.
transporttcp or udp; QUIC belongs to the UDP namespace.
listen_addrBind address. L4 chain deployment currently emits ::.
listen_portNumeric port, 1 to 65535.

Examples:

ScenarioSame listener?Notes
tcp :::443 and udp :::443NoTCP TLS and UDP Noise can use the same numeric port because TCP and UDP bind separately.
udp :::443 UDP RAW and udp :::443 QUIC REALITYYesQUIC uses UDP, but QUIC REALITY can be embedded into the selected UDP socket so RAW fallback still receives non-QUIC traffic.
tcp :::443 TCP TLS and tcp :::443 TCP REALITYYesBoth use TCP. The agent builds a TLS/SNI-family listener and routes TLS, REALITY, and RAW fallback traffic by the available handshake signals.

Compatibility Policy

Default order:

text
REALITY > TLS > Noise > RAW

API and database values use lowercase labels:

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

The priority order is a compatibility policy for cases that still cannot be merged into one route table. Only protocol classes actually present on that listener participate. If a saved policy omits a valid label, the backend and agent append missing labels in default order.

The current frontend no longer exposes the policy save icon. API clients can still save a policy; saving a policy triggers L4 config replay for that node. The policy itself does not create traffic rules. Without matching L4 Chain rules, the row is only a policy-only listener.

Conflict Resolution

Agent behavior, simplified:

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

Detailed behavior:

Winning classMultiplexing behavior
TCP REALITYSupports multiple REALITY routes on one TCP listener. REALITY is treated as TLS-like: the listener uses SNI as the first split key and then tries all SNI-matching REALITY routes by short ID.
TCP TLSSupports multiple TLS routes on one TCP listener by pre-reading TLS ClientHello and routing by SNI. Can also keep one TCP RAW route as the default fallback for non-TLS or unmatched-SNI traffic.
UDP NoiseSupports multiple UDP Noise routes on one UDP listener by trying candidate local X25519 keys on the first datagram, then pinning the UDP session to the selected route.
QUIC REALITYQUIC is in the UDP namespace. QUIC REALITY routes can be embedded into the same UDP socket used by UDP RAW or UDP Noise, while QUIC's own REALITY routing can still split by server name and short ID.
RAWNo discriminator. At most one RAW route can own a selected listener. Duplicate RAW routes on the same listener are not meaningful.

TCP RAW Fallback

For TCP, a TLS/SNI-family listener can retain one RAW route as an explicit default fallback. The TLS/SNI router first tries to read a TLS ClientHello:

  • if SNI matches a configured TLS or SSLPREREAD route, traffic uses that route;
  • if SNI matches REALITY routes, the agent tries each candidate REALITY route until short ID authentication succeeds;
  • if the first bytes are not TLS, or no SNI route matches, traffic falls back to the RAW route;
  • if more than one RAW rule exists on the same listener, only one can be kept because RAW has no discriminator.

This is what allows a TCP TLS/SNI control listener and an SSH RAW listener to share the same TCP port. REALITY can coexist in the same listener because it is recognized from the TLS-like ClientHello before REALITY short ID checks run.

UDP Shared Socket

For UDP, one socket can host UDP RAW fallback, UDP Noise routes, and embedded QUIC REALITY routes when they request the same udp address:port.

Dispatch rules:

  • existing QUIC peer state continues to Quinn for QUIC packets;
  • an unknown peer is dispatched to Quinn only when the packet looks like a QUIC long-header Initial;
  • Noise candidates try to decrypt the first datagram with configured local keys;
  • if no QUIC or Noise route accepts the datagram, the single UDP RAW fallback receives it.

This keeps ordinary UDP RAW traffic from being swallowed by the QUIC endpoint while still allowing QUIC REALITY to live on the same UDP listener.

Protocol Split Matrix

Listener namespaceProtocol classSplit keySame-port routesNotes
TCPREALITYTLS-like ClientHello SNI, then REALITY short IDManyMultiple same-SNI REALITY routes are valid when short IDs differ.
TCPTLSTLS ClientHello SNIMany, one per SNIEmpty cert_domain cannot be used for SNI routing. Can retain one RAW fallback.
TCPRAWNoneOneNo protocol-level discriminator. Can act as the default fallback under a TLS/SNI listener.
UDPQUIC REALITYQUIC Initial detection, then REALITY route materialMany internallyCan be embedded into the shared UDP socket with UDP RAW and UDP Noise.
UDPNoiseFirst datagram decrypt with local X25519 keyMany, one per local keySession is pinned after first datagram.
UDPRAWNoneOneCan act as fallback in the shared UDP socket; two RAW UDP services still cannot share one listener.

Noise And TLS On The Same Port

Noise and TLS can share the same numeric port if one is in the UDP namespace and the other is in the TCP namespace.

For example, TCP TLS on tcp :::443 and UDP Noise on udp :::443 can coexist. They are not the same listener and do not participate in the same priority decision.

QUIC REALITY and UDP Noise both live in the UDP namespace, but QUIC REALITY can be embedded into the selected UDP socket executor, so QUIC REALITY, UDP Noise, and one UDP RAW fallback can coexist on the same udp address:port. TCP TLS and TCP REALITY both live in the TCP namespace and can coexist inside the same TLS/SNI-family listener when their SNI and REALITY short ID material are unambiguous.

Validator Risk

Validator risk here means semantic drift between control-plane validation, frontend display, and actual agent runtime behavior.

It can happen when:

  • the backend accepts a rule set, but the agent drops part of it due to system bind occupancy or non-demultiplexable duplicates such as multiple RAW fallbacks;
  • the frontend displays control-plane intent while the agent runtime has not yet reported the reconciled listener table;
  • default priority exists only as hidden agent logic and is not represented in backend or frontend state.

Current mitigations:

MitigationDescription
Shared policy storagenode_port_policies remains the compatibility store for API-managed per-node listener priority.
Proto propagationPingoraConfig.listener_policies carries policy to the agent.
Agent-side final decisionThe agent still makes the final bind and conflict decision because only it knows local socket state.
Runtime heartbeat stateHeartbeatRequest.runtime_listeners reports the listener state that the agent actually applied.
Runtime previewThe frontend preview action loads /api/nodes/ports/runtime instead of relying on local draft computation.
Human-readable logsAgent logs selected priority, dropped priority, listener address, cluster, and rule fingerprint.
Focused testsTests cover TCP SNI-family coexistence, UDP Noise key routing, UDP RAW fallback, and QUIC REALITY sharing a UDP socket with ordinary UDP routes.

System-Bound Ports

If an address:port is already bound by a non-Nexus process, the agent does not auto-allocate another port. The config apply is rejected and reported through ConfigApplyReport instead of silently skipping the listener.

For TCP, the agent checks the TCP address:port. For UDP and QUIC, the agent checks the UDP address:port namespace. An existing Nexus-managed listener can be hot-reloaded and is not treated as system occupancy.

Frontend Workflow

  1. Open Nodes > Port Management.
  2. Select a node from the dropdown, or keep All nodes.
  3. Review each row:
    • node name and id prefix;
    • transport, such as tcp or udp;
    • listener namespace, such as :::443;
    • L4 or L3 service sources requesting that listener;
    • intended route count and runtime status when available.
  4. Click the service/order action to open the draggable service list. The dialog lists each L4 or L3 service that must be considered for that listener; the current UI does not save a priority policy from this dialog.
  5. Click the runtime preview action to fetch the agent-reported listener table. The dialog shows a loading state while it calls /api/nodes/ports/runtime.
  6. Compare the runtime route list with the intended service list. Missing runtime data means the agent has not connected, has not heartbeated with runtime listener state, or the latest config apply was rejected.
  7. Watch agent logs if the port is already occupied or a non-demultiplexable duplicate was dropped.

The listener table keeps one row per node_id + transport + listen_addr + listen_port. TCP and UDP can share the same numeric port and still appear as separate rows.

REST API

List Ports

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

Returns all node options and listener items. node_id filters listener items but the node dropdown still receives all nodes.

Response shape:

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
}

The /api/nodes/ports response is control-plane intent. It is built from chains, upstreams, policies, and service definitions. Use the runtime endpoint below when the question is "what is the agent actually listening on right now?"

List Runtime L4 Ports

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

Returns the runtime listener table most recently reported by agents through heartbeat. Query parameters are optional filters.

Response shape:

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
}

The runtime preview dialog uses this endpoint. It must show loading and error states because the data depends on agent connectivity and heartbeat freshness.

Save Listener Policy

http
PUT /api/nodes/ports/policies

Required role: admin or operator.

This endpoint remains for API compatibility and operator automation. The current frontend Port Management page does not expose a save icon for this policy.

Request:

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

Behavior:

FieldRule
listenPortMust be between 1 and 65535.
transportMust be tcp or udp.
priorityOrderCan only contain reality, tls, noise, and raw.

The backend deduplicates, normalizes, and appends missing protocol labels. After saving, it replays L4 chain config for that node.

Runtime preview no longer applies a frontend-only draft ordering. It displays the agent-reported route table from /api/nodes/ports/runtime.

Database

The policy table is node_port_policies.

ColumnMeaning
idPolicy id.
node_idOwning node, references nodes(id).
transporttcp or udp.
listen_addrListener bind address.
listen_portListener port, 1 to 65535.
priority_orderJSONB array of protocol labels.
enabledWhether the policy is active.
created_at, updated_atTimestamps.

Uniqueness:

text
UNIQUE (node_id, transport, listen_addr, listen_port)

This means one node can have separate policies for tcp :::443 and udp :::443.

Agent Control Protocol

PingoraConfig carries listener policies:

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;
}

The backend and agent keep separate copies of common/proto/agent.proto; both copies must stay identical.

Agent heartbeat also carries the current runtime listener table:

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;
}

Logging And Troubleshooting

Useful agent log patterns:

Log meaningWhat to check
Gateway config validatedCompare requested vs selected TCP/QUIC/UDP rule counts.
TCP SNI-family port reuse enabledTCP REALITY, TLS/SSLPREREAD, and RAW fallback were merged into one listener.
UDP same-port route table enabledUDP RAW, UDP Noise, or embedded QUIC REALITY routes were merged into one UDP listener.
Embedding QUIC REALITY route into the selected UDP socket executorQUIC REALITY is sharing the selected UDP socket instead of opening a standalone QUIC socket.
Dropping duplicate TCP RAW fallback because another RAW route already owns this listener fallbackOnly one TCP RAW fallback can exist under one TCP SNI-family listener.
Dropping duplicate UDP RAW fallback because another RAW route already owns this listener fallbackOnly one UDP RAW fallback can exist under one UDP listener.
already bound outside this managerAnother process owns the port; the snapshot is rejected. Stop that process or choose another port.

Troubleshooting checklist:

  1. Confirm the L4 chain is enabled.
  2. Confirm the chain was deployed after the latest edit.
  3. Open Port Management and check the listener row exists.
  4. Open runtime preview and check whether the agent reports the listener and the expected routes.
  5. Check whether the port is occupied by a non-Nexus process on the agent host.
  6. Check whether a credential is missing or mismatched:
    • TLS requires a TLS certificate with an explicit domain.
    • REALITY requires server names and short IDs.
    • UDP Noise requires valid X25519 key material.
  7. Inspect agent logs for dropped rules.

Performance Notes

Most listener-merge cost happens during config apply: group by listener, validate whether routes can be demultiplexed, keep a single fallback where needed, and build the runtime route table. Runtime overhead depends on the listener composition:

  • TCP TLS SNI routing pre-reads the first TLS ClientHello record, then feeds that record back into rustls for normal accept.
  • TCP REALITY selection uses SNI first and may try multiple SNI-matching REALITY routes until short ID authentication succeeds.
  • UDP Noise routing tries candidate keys only on the first datagram of a new UDP session. After selection, the session is pinned to the route.
  • QUIC sharing on a UDP socket dispatches unknown peers to Quinn only for QUIC long-header Initial packets; known QUIC peers can continue with fixed-bit QUIC packets. Non-QUIC UDP falls through to Noise or RAW.
  • Runtime preview freshness is bounded by the agent heartbeat cadence.
  • REALITY, TLS, Noise, and QUIC same-port reuse are not literally free, but the overhead is small for normal route counts.

If one port carries many SNI, REALITY, or Noise key routes, watch first-packet/handshake latency and route counts in logs.

Design Rules

DoAvoid
Use TCP TLS SNI when many TLS services need one TCP port.Do not put two RAW services on the same listener and expect routing.
Use UDP Noise key routing when many UDP Noise services need one UDP port.Do not assume TCP and UDP priorities interact. They are separate namespaces.
Use QUIC REALITY shared UDP dispatch when QUIC and ordinary UDP must use one UDP port.Do not assume every QUIC mode can share a UDP socket; this behavior is for QUIC REALITY route material handled by the shared listener.
Compare control-plane intent with runtime preview after deployment.Do not rely on automatic fallback port allocation. It is intentionally disabled.
Keep listener policy and chain deployment together in change review when using the policy API.Do not treat policy-only listeners as active traffic listeners.

NexusNet documentation