feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22) (#5506)

* feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22)

xray-core v26.6.22 (PR #6258) renamed the XHTTP session config keys
sessionPlacement/sessionKey to sessionIDPlacement/sessionIDKey (no fallback
kept in core) and added sessionIDTable (predefined charset name or literal
ASCII) and sessionIDLength (range, e.g. 16-32, lower bound > 0).

Panel changes:
- Schema (xhttp.ts): rename the two keys, add sessionIDTable/sessionIDLength,
  and a z.preprocess that lifts legacy keys off stored configs so an upgraded
  panel never silently drops a saved session setting.
- Wire normalize + share-link build/parse: rename keys, emit the two new
  fields, and accept legacy sessionPlacement/sessionKey from old share links.
- Inbound + outbound XHTTP forms: rename field paths, add a sessionIDTable
  autocomplete (9 predefined tables + free ASCII) and a sessionIDLength range
  input shown only when a table is set, with light client validation (ASCII
  table, length min > 0; xray enforces the room-size minimum server-side).
- Subscription (service.go) and Clash (clash_service.go) builders: emit the
  renamed + new keys, with a legacy fallback for not-yet-resaved inbounds.
- Locales: add sessionIDTable/sessionIDLength labels + hints in all 13 files.

Two sibling v26.6.22 XHTTP commits need no panel change and are covered by the
core bump alone: #6332 (XHTTP/3 closes QUIC/UDP) and #6320 (udpHop honors the
existing dialerProxy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(xhttp): add Session ID Table to inbound form-blocks snapshot

The new sessionIDTable input renders by default in the inbound XHTTP form, so
its label joins the field-structure snapshot. sessionIDLength stays conditional
(only shown when a table is set), so it does not appear here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(xhttp): migrate legacy session keys in the running xray config

The Zod preprocess plus the subscription/Clash fallbacks only covered the
panel UI and share-link output. The config handed to the running xray-core
process is built from the raw stored streamSettings in GetXrayConfig, which
did not rewrite the renamed XHTTP session keys — so a pre-upgrade inbound (or
template outbound) stored with a non-default sessionPlacement was emitted
unchanged and dropped by xray-core v26.6.22, until the admin re-saved it.

Lift sessionPlacement/sessionKey onto sessionIDPlacement/sessionIDKey at
config-generation time, in the existing inbound stream-rewrite block (next to
the tls/reality/externalProxy handling) and across template outbounds. The
lift is idempotent and leaves unchanged configs byte-identical so the
hot-reload diff never sees a spurious change.

Also tighten validateSessionIDLength to reject an inverted range (e.g. 32-16)
in addition to the existing lower-bound > 0 check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(xray): avoid summed-capacity allocation in mergeSubscriptionOutbounds

CodeQL go/allocation-size-overflow flagged the pre-sized make() whose
capacity was a sum of three slice lengths. Grow the slice via append on
a nil slice instead; same result, no overflow-prone capacity expression.
This commit is contained in:
Rouzbeh†
2026-06-23 17:38:16 +02:00
committed by GitHub
parent b07fad0e69
commit fea3c94b11
31 changed files with 498 additions and 45 deletions
+68 -1
View File
@@ -121,6 +121,10 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
xrayConfig.API = ensureAPIServices(xrayConfig.API)
xrayConfig.Policy = ensureStatsPolicy(xrayConfig.Policy)
xrayConfig.RouterConfig = stripDisabledRules(xrayConfig.RouterConfig)
// Template outbounds authored before the xray-core #6258 XHTTP rename may
// still carry sessionPlacement/sessionKey; lift them too (same reason as
// the per-inbound lift below).
xrayConfig.OutboundConfigs = liftOutboundsXhttpSessionIDKeys(xrayConfig.OutboundConfigs)
_, _, _ = s.inboundService.AddTraffic(nil, nil)
@@ -251,6 +255,12 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
delete(stream, "externalProxy")
// xray-core v26.6.22 (#6258) renamed the XHTTP session keys and
// kept no fallback. Lift legacy sessionPlacement/sessionKey onto the
// new names here so inbounds stored before the rename keep working
// without the admin re-saving them.
liftXhttpSessionIDKeys(stream)
newStream, err := json.MarshalIndent(stream, "", " ")
if err != nil {
return nil, err
@@ -576,7 +586,7 @@ func mergeSubscriptionOutbounds(cfg *xray.Config, prepend, appendList []any) {
return
}
}
merged := make([]any, 0, len(prepend)+len(templateOutbounds)+len(appendList))
var merged []any
merged = append(merged, prepend...)
merged = append(merged, templateOutbounds...)
merged = append(merged, appendList...)
@@ -1078,3 +1088,60 @@ func (s *XrayService) IsNeedRestartAndSetFalse() bool {
func (s *XrayService) DidXrayCrash() bool {
return !s.IsXrayRunning() && !isManuallyStopped.Load()
}
// liftXhttpSessionIDKeys renames the legacy XHTTP session keys
// (sessionPlacement/sessionKey) to the v26.6.22 #6258 names
// (sessionIDPlacement/sessionIDKey) inside a streamSettings map. xray-core kept
// no fallback for the old names, so a config stored before the rename would be
// silently ignored by the engine. Returns true if it changed anything.
func liftXhttpSessionIDKeys(stream map[string]any) bool {
xhttp, ok := stream["xhttpSettings"].(map[string]any)
if !ok {
return false
}
changed := false
for legacy, renamed := range map[string]string{
"sessionPlacement": "sessionIDPlacement",
"sessionKey": "sessionIDKey",
} {
v, has := xhttp[legacy]
if !has {
continue
}
if _, exists := xhttp[renamed]; !exists {
xhttp[renamed] = v
}
delete(xhttp, legacy)
changed = true
}
return changed
}
// liftOutboundsXhttpSessionIDKeys applies liftXhttpSessionIDKeys to every
// outbound's streamSettings in the raw outbounds array. The original bytes are
// returned untouched when nothing needs lifting, so an unchanged config never
// looks modified to the hot-reload diff.
func liftOutboundsXhttpSessionIDKeys(raw json_util.RawMessage) json_util.RawMessage {
if len(raw) == 0 {
return raw
}
var outbounds []map[string]any
if err := json.Unmarshal(raw, &outbounds); err != nil {
return raw
}
changed := false
for _, ob := range outbounds {
if stream, ok := ob["streamSettings"].(map[string]any); ok {
if liftXhttpSessionIDKeys(stream) {
changed = true
}
}
}
if !changed {
return raw
}
if rewritten, err := json.Marshal(outbounds); err == nil {
return rewritten
}
return raw
}