fix(handler): encode AddOutbound payload as proto to avoid xray nil-deref #1

Merged
kinsei merged 1 commit from fix/addoutbound-proto-encoding into main 2026-05-20 17:36:39 +02:00
Owner

Problem

xray 26.3.27 panics with nil-pointer dereference at app/proxyman/outbound/handler.go:104 ((*TypedMessage).GetInstance(0x0)) on every AddOutbound call from panel hot-reload. 29 panic occurrences observed on prod RU nodes in 24h; cascade outbounds never materialize → cascade TCP=0 across all 16 RU nodes.

Root cause: handler.service.ts:addOutbound was passing raw xray JSON ({tag, protocol, settings, streamSettings}) straight to xtlsApi.handler.rawClient.addOutbound via outbound: obj as never. nice-grpc serializes by proto field name; xray JSON's fields don't match the OutboundHandlerConfig proto (which expects senderSettings: TypedMessage, proxySettings: TypedMessage). Only tag ended up on the wire → server-side ProxySettings was nil → panic.

Fix

New src/modules/handler/converters/outbound-json-to-proto.ts converts xray JSON → OutboundHandlerConfig proto:

  • Uses xtls-sdk's compiled proto types where shipped (vless/trojan outbound Config, ServerEndpoint, User, IPOrDomain, SenderConfig, StreamConfig, TransportConfig, TypedMessage).
  • Hand-rolls wire encoding via @bufbuild/protobuf/wire BinaryWriter for transports xtls-sdk does NOT ship: grpc/reality/tcp/tls/splithttp/websocket. Reality client logic mirrors xray-core infra/conf v26.3.27.
  • handler.service.ts: both addOutbound and syncOutbounds now convert before grpc; conversion errors in sync become per-outbound error strings instead of whole-batch aborts.

Scope

Covered now (everything cascade + existing inbound types need):

  • vless+grpc+Reality (the cascade fixture)
  • vless+tcp+TLS
  • trojan+xhttp+TLS
  • trojan+grpc+Reality
  • vless+ws+TLS

Not implemented (rejects with clear error): kcp/mkcp, httpupgrade, hysteria. TCP header_settings (HTTP/1.1 obfuscation) silently ignored — none of our inbounds use it. Pinned to xray-core v26.3.27 field numbers (matches Dockerfile XRAY_CORE_VERSION).

Verification

  • npm install clean, no lockfile drift
  • npm run build exits 0, no TS errors
  • npm test — 21/21 pass via node:test through ts-node
  • Cascade fixture verbatim from prod is in the spec — Reality wire bytes decode back to publicKey (32 bytes from b64url), shortId (zero-padded), serverName, fingerprint, spiderX

Test plan post-merge

  1. CI image build on main
  2. Bump remnanode image tag across 16 RU nodes (rolling, per ADR-052 procedure)
  3. Verify cascade TCP > 0 on o3wl2 (87.228.61.179):
ss -tn state established | grep -cE '65.109.178.75|188.227.84.155|90.156.223.184'

Pre-fix: 0. Expected post-fix: ≥1 per RU node.
4. Check xray.err.log on RU nodes for new AddOutbound panics — should be zero.

Backup tag: pre-merge-addoutbound-proto-{TS} on origin/main.

Co-Authored-By: Пятница 🤖 pyatnitsa@openclaw

## Problem xray 26.3.27 panics with nil-pointer dereference at `app/proxyman/outbound/handler.go:104` (`(*TypedMessage).GetInstance(0x0)`) on every `AddOutbound` call from panel hot-reload. 29 panic occurrences observed on prod RU nodes in 24h; cascade outbounds never materialize → cascade TCP=0 across all 16 RU nodes. Root cause: `handler.service.ts:addOutbound` was passing raw xray JSON (`{tag, protocol, settings, streamSettings}`) straight to `xtlsApi.handler.rawClient.addOutbound` via `outbound: obj as never`. nice-grpc serializes by proto field name; xray JSON's fields don't match the `OutboundHandlerConfig` proto (which expects `senderSettings: TypedMessage`, `proxySettings: TypedMessage`). Only `tag` ended up on the wire → server-side `ProxySettings` was nil → panic. ## Fix New `src/modules/handler/converters/outbound-json-to-proto.ts` converts xray JSON → `OutboundHandlerConfig` proto: - Uses xtls-sdk's compiled proto types where shipped (vless/trojan outbound Config, ServerEndpoint, User, IPOrDomain, SenderConfig, StreamConfig, TransportConfig, TypedMessage). - Hand-rolls wire encoding via `@bufbuild/protobuf/wire` `BinaryWriter` for transports xtls-sdk does NOT ship: grpc/reality/tcp/tls/splithttp/websocket. Reality client logic mirrors xray-core `infra/conf` v26.3.27. - `handler.service.ts`: both `addOutbound` and `syncOutbounds` now convert before grpc; conversion errors in sync become per-outbound error strings instead of whole-batch aborts. ## Scope Covered now (everything cascade + existing inbound types need): - vless+grpc+Reality (the cascade fixture) - vless+tcp+TLS - trojan+xhttp+TLS - trojan+grpc+Reality - vless+ws+TLS Not implemented (rejects with clear error): kcp/mkcp, httpupgrade, hysteria. TCP `header_settings` (HTTP/1.1 obfuscation) silently ignored — none of our inbounds use it. Pinned to xray-core v26.3.27 field numbers (matches Dockerfile `XRAY_CORE_VERSION`). ## Verification - `npm install` clean, no lockfile drift - `npm run build` exits 0, no TS errors - `npm test` — 21/21 pass via `node:test` through ts-node - Cascade fixture verbatim from prod is in the spec — Reality wire bytes decode back to publicKey (32 bytes from b64url), shortId (zero-padded), serverName, fingerprint, spiderX ## Test plan post-merge 1. CI image build on main 2. Bump remnanode image tag across 16 RU nodes (rolling, per ADR-052 procedure) 3. Verify cascade TCP > 0 on o3wl2 (87.228.61.179): ``` ss -tn state established | grep -cE '65.109.178.75|188.227.84.155|90.156.223.184' ``` Pre-fix: 0. Expected post-fix: ≥1 per RU node. 4. Check xray.err.log on RU nodes for new `AddOutbound` panics — should be zero. Backup tag: `pre-merge-addoutbound-proto-{TS}` on `origin/main`. Co-Authored-By: Пятница 🤖 <pyatnitsa@openclaw>
Panel hot-reload sends raw xray JSON outbounds via /handler/sync-outbounds.
The previous addOutbound passthrough relied on grpc serializing JSON field
names, which dropped senderSettings/proxySettings and crashed xray-core
26.3.27 at app/proxyman/outbound/handler.go:104 (nil TypedMessage receiver).

Add converters/outbound-json-to-proto.ts that builds a proper
OutboundHandlerConfig: vless/trojan proxySettings via the xtls-sdk-generated
protos, plus hand-rolled wire encoding for transport configs absent from the
SDK (grpc, reality, tcp, tls, splithttp/xhttp, websocket). Reality client
side mirrors xray-core's infra/conf logic: base64url publicKey, hex shortId
padded to 8 bytes, spiderX defaulting to "/", spiderY derived from query
params.

handler.service.ts addOutbound / syncOutbounds now run conversions before
calling the raw grpc client; OutboundJsonError surfaces per-outbound
problems without aborting the whole sync.

Tests use node:test via ts-node; covers the production cascade fixture
(vless+grpc+Reality), vless+tcp+TLS, trojan+xhttp+TLS, and error cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kinsei merged commit 836de55c47 into main 2026-05-20 17:36:39 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kinsei/remnawave-node!1
No description provided.