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
+19 -2
View File
@@ -404,8 +404,10 @@ func buildXhttpClashOpts(xhttp map[string]any) map[string]any {
stringFields := []xhttpStringField{
{"xPaddingBytes", "x-padding-bytes", ""},
{"uplinkHTTPMethod", "uplink-http-method", ""},
{"sessionPlacement", "session-placement", ""},
{"sessionKey", "session-key", ""},
{"sessionIDPlacement", "session-id-placement", ""},
{"sessionIDKey", "session-id-key", ""},
{"sessionIDTable", "session-id-table", ""},
{"sessionIDLength", "session-id-length", ""},
{"seqPlacement", "seq-placement", ""},
{"seqKey", "seq-key", ""},
{"uplinkDataPlacement", "uplink-data-placement", ""},
@@ -420,6 +422,21 @@ func buildXhttpClashOpts(xhttp map[string]any) map[string]any {
}
}
// Legacy inbounds (pre xray-core #6258) stored sessionPlacement/sessionKey.
// Fall back to them so not-yet-resaved configs still map. Mirrors the
// frontend migration.
for _, f := range []xhttpStringField{
{"sessionPlacement", "session-id-placement", ""},
{"sessionKey", "session-id-key", ""},
} {
if _, exists := opts[f.dst]; exists {
continue
}
if v, ok := xhttp[f.src].(string); ok && v != "" {
opts[f.dst] = v
}
}
// Bool fields (truthy only)
if v, ok := xhttp["noGRPCHeader"].(bool); ok && v {
opts["no-grpc-header"] = true
+14 -6
View File
@@ -330,8 +330,10 @@ func TestBuildXhttpClashOpts_FullFieldMapping(t *testing.T) {
"xPaddingPlacement": "queryInHeader",
"xPaddingMethod": "tokenish",
"uplinkHTTPMethod": "POST",
"sessionPlacement": "query",
"sessionKey": "sess",
"sessionIDPlacement": "query",
"sessionIDKey": "sess",
"sessionIDTable": "Base62",
"sessionIDLength": "16-32",
"seqPlacement": "header",
"seqKey": "seq",
"uplinkDataPlacement": "body",
@@ -377,11 +379,17 @@ func TestBuildXhttpClashOpts_FullFieldMapping(t *testing.T) {
if opts["uplink-http-method"] != "POST" {
t.Errorf("uplink-http-method = %v", opts["uplink-http-method"])
}
if opts["session-placement"] != "query" {
t.Errorf("session-placement = %v", opts["session-placement"])
if opts["session-id-placement"] != "query" {
t.Errorf("session-id-placement = %v", opts["session-id-placement"])
}
if opts["session-key"] != "sess" {
t.Errorf("session-key = %v", opts["session-key"])
if opts["session-id-key"] != "sess" {
t.Errorf("session-id-key = %v", opts["session-id-key"])
}
if opts["session-id-table"] != "Base62" {
t.Errorf("session-id-table = %v", opts["session-id-table"])
}
if opts["session-id-length"] != "16-32" {
t.Errorf("session-id-length = %v", opts["session-id-length"])
}
if opts["seq-placement"] != "header" {
t.Errorf("seq-placement = %v", opts["seq-placement"])
+15 -1
View File
@@ -1731,7 +1731,7 @@ func buildXhttpExtra(xhttp map[string]any) map[string]any {
stringFields := []string{
"uplinkHTTPMethod",
"sessionPlacement", "sessionKey",
"sessionIDPlacement", "sessionIDKey", "sessionIDTable", "sessionIDLength",
"seqPlacement", "seqKey",
"uplinkDataPlacement", "uplinkDataKey",
"scMaxEachPostBytes", "scMinPostsIntervalMs",
@@ -1750,6 +1750,20 @@ func buildXhttpExtra(xhttp map[string]any) map[string]any {
}
}
// Legacy inbounds (pre xray-core #6258) stored sessionPlacement/sessionKey.
// Lift them onto the renamed keys so links from not-yet-resaved configs
// still carry the session settings. Mirrors the frontend migration.
for legacy, renamed := range map[string]string{
"sessionPlacement": "sessionIDPlacement",
"sessionKey": "sessionIDKey",
} {
if _, exists := extra[renamed]; !exists {
if v, ok := xhttp[legacy].(string); ok && len(v) > 0 {
extra[renamed] = v
}
}
}
for _, field := range []string{"uplinkChunkSize"} {
if v, ok := nonZeroShareValue(xhttp[field]); ok {
extra[field] = v
+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
}
@@ -0,0 +1,81 @@
package service
import (
"encoding/json"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
)
// xray-core v26.6.22 (#6258) renamed the XHTTP session keys with no fallback.
// The lift must rewrite stored configs at config-generation time so pre-upgrade
// inbounds/outbounds keep working without a manual re-save.
func TestLiftXhttpSessionIDKeys(t *testing.T) {
t.Run("lifts legacy keys and drops them", func(t *testing.T) {
stream := map[string]any{
"xhttpSettings": map[string]any{
"sessionPlacement": "cookie",
"sessionKey": "x_session",
},
}
if !liftXhttpSessionIDKeys(stream) {
t.Fatal("expected changed=true")
}
xhttp := stream["xhttpSettings"].(map[string]any)
if xhttp["sessionIDPlacement"] != "cookie" || xhttp["sessionIDKey"] != "x_session" {
t.Fatalf("renamed keys missing: %#v", xhttp)
}
if _, ok := xhttp["sessionPlacement"]; ok {
t.Fatal("legacy sessionPlacement still present")
}
if _, ok := xhttp["sessionKey"]; ok {
t.Fatal("legacy sessionKey still present")
}
})
t.Run("keeps an explicit new key over the legacy one", func(t *testing.T) {
stream := map[string]any{
"xhttpSettings": map[string]any{
"sessionPlacement": "cookie",
"sessionIDPlacement": "header",
},
}
liftXhttpSessionIDKeys(stream)
xhttp := stream["xhttpSettings"].(map[string]any)
if xhttp["sessionIDPlacement"] != "header" {
t.Fatalf("explicit new key was overwritten: %v", xhttp["sessionIDPlacement"])
}
})
t.Run("no-op without xhttpSettings or legacy keys", func(t *testing.T) {
if liftXhttpSessionIDKeys(map[string]any{"wsSettings": map[string]any{}}) {
t.Fatal("expected no change for non-xhttp stream")
}
if liftXhttpSessionIDKeys(map[string]any{"xhttpSettings": map[string]any{"path": "/"}}) {
t.Fatal("expected no change when no legacy keys present")
}
})
}
func TestLiftOutboundsXhttpSessionIDKeys(t *testing.T) {
raw := json_util.RawMessage(`[{"protocol":"vless","streamSettings":{"network":"xhttp","xhttpSettings":{"sessionKey":"x_session","sessionPlacement":"query"}}}]`)
out := liftOutboundsXhttpSessionIDKeys(raw)
var parsed []map[string]any
if err := json.Unmarshal(out, &parsed); err != nil {
t.Fatalf("unmarshal rewritten outbounds: %v", err)
}
xhttp := parsed[0]["streamSettings"].(map[string]any)["xhttpSettings"].(map[string]any)
if xhttp["sessionIDKey"] != "x_session" || xhttp["sessionIDPlacement"] != "query" {
t.Fatalf("outbound keys not lifted: %#v", xhttp)
}
if _, ok := xhttp["sessionKey"]; ok {
t.Fatal("legacy sessionKey survived in outbound")
}
// Unchanged input must return byte-identical output (no spurious hot-reload).
clean := json_util.RawMessage(`[{"protocol":"freedom"}]`)
if got := liftOutboundsXhttpSessionIDKeys(clean); string(got) != string(clean) {
t.Fatalf("clean outbounds were rewritten: %s", got)
}
}
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "طريقة Padding",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sessionIDTable": "جدول معرّف الجلسة",
"sessionIDTableHint": "مجموعة الأحرف لتوليد معرّف الجلسة: اسم معرّف مسبقًا (ALPHABET، Base62، hex، number، …) أو سلسلة ASCII. اتركه فارغًا لاستخدام الإعداد الافتراضي لـ xray-core.",
"sessionIDLength": "طول معرّف الجلسة",
"sessionIDLengthHint": "طول أو نطاق (مثل 8-16) لمعرّف الجلسة المُولَّد. يُستخدم فقط عند تعيين جدول معرّف الجلسة؛ يجب أن يكون الحد الأدنى أكبر من 0.",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Padding Method",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sessionIDTable": "Session ID Table",
"sessionIDTableHint": "Charset for generated session IDs: a predefined name (ALPHABET, Base62, hex, number, …) or a literal ASCII string. Leave empty for xray-core's default.",
"sessionIDLength": "Session ID Length",
"sessionIDLengthHint": "Length or range (e.g. 8-16) of generated session IDs. Only used when a Session ID Table is set; minimum must be greater than 0.",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Método de Padding",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sessionIDTable": "Tabla de Session ID",
"sessionIDTableHint": "Conjunto de caracteres para generar los session ID: un nombre predefinido (ALPHABET, Base62, hex, number, …) o una cadena ASCII literal. Déjalo vacío para el valor por defecto de xray-core.",
"sessionIDLength": "Longitud de Session ID",
"sessionIDLengthHint": "Longitud o rango (p. ej. 8-16) del session ID generado. Solo se usa cuando hay una Tabla de Session ID definida; el mínimo debe ser mayor que 0.",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "روش Padding",
"sessionPlacement": "محل نشست",
"sessionKey": "کلید نشست",
"sessionIDTable": "جدول شناسه نشست",
"sessionIDTableHint": "مجموعه نویسه‌ها برای تولید شناسه نشست: یک نام از پیش‌تعریف‌شده (ALPHABET، Base62، hex، number، …) یا یک رشته ASCII. برای مقدار پیش‌فرض xray-core خالی بگذارید.",
"sessionIDLength": "طول شناسه نشست",
"sessionIDLengthHint": "طول یا بازه (مثلاً 8-16) شناسه نشست تولیدشده. فقط وقتی جدول شناسه نشست تنظیم شده باشد استفاده می‌شود؛ کمینه باید بزرگ‌تر از 0 باشد.",
"sequencePlacement": "محل Sequence",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "محل داده Uplink",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Metode Padding",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sessionIDTable": "Tabel Session ID",
"sessionIDTableHint": "Kumpulan karakter untuk membuat session ID: nama yang telah ditentukan (ALPHABET, Base62, hex, number, …) atau string ASCII literal. Kosongkan untuk default xray-core.",
"sessionIDLength": "Panjang Session ID",
"sessionIDLengthHint": "Panjang atau rentang (mis. 8-16) session ID yang dibuat. Hanya digunakan saat Tabel Session ID disetel; nilai minimum harus lebih besar dari 0.",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -555,6 +555,10 @@
"paddingMethod": "Padding 方法",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sessionIDTable": "セッション ID テーブル",
"sessionIDTableHint": "セッション ID 生成に使う文字セット:定義済みの名前(ALPHABET、Base62、hex、number など)またはリテラル ASCII 文字列。空欄で xray-core の既定値を使用します。",
"sessionIDLength": "セッション ID の長さ",
"sessionIDLengthHint": "生成するセッション ID の長さまたは範囲(例: 8-16)。セッション ID テーブルを設定したときのみ有効です。最小値は 0 より大きい必要があります。",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -555,6 +555,10 @@
"paddingMethod": "Método de Padding",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sessionIDTable": "Tabela de Session ID",
"sessionIDTableHint": "Conjunto de caracteres para gerar session IDs: um nome predefinido (ALPHABET, Base62, hex, number, …) ou uma string ASCII literal. Deixe vazio para o padrão do xray-core.",
"sessionIDLength": "Comprimento do Session ID",
"sessionIDLengthHint": "Comprimento ou intervalo (ex.: 8-16) do session ID gerado. Usado apenas quando uma Tabela de Session ID está definida; o mínimo deve ser maior que 0.",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -555,6 +555,10 @@
"paddingMethod": "Padding Method",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sessionIDTable": "Таблица Session ID",
"sessionIDTableHint": "Набор символов для генерации session ID: предопределённое имя (ALPHABET, Base62, hex, number, …) или строка ASCII. Оставьте пустым для значения xray-core по умолчанию.",
"sessionIDLength": "Длина Session ID",
"sessionIDLengthHint": "Длина или диапазон (например, 8-16) генерируемого session ID. Используется только когда задана таблица; минимум должен быть больше 0.",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Padding Yöntemi",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sessionIDTable": "Oturum Kimliği Tablosu",
"sessionIDTableHint": "Oturum kimliği üretmek için karakter kümesi: önceden tanımlı bir ad (ALPHABET, Base62, hex, number, …) veya düz ASCII dizesi. xray-core varsayılanı için boş bırakın.",
"sessionIDLength": "Oturum Kimliği Uzunluğu",
"sessionIDLengthHint": "Üretilen oturum kimliğinin uzunluğu veya aralığı (örn. 8-16). Yalnızca bir Oturum Kimliği Tablosu ayarlandığında kullanılır; en küçük değer 0'dan büyük olmalıdır.",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Padding Method",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sessionIDTable": "Таблиця Session ID",
"sessionIDTableHint": "Набір символів для генерації session ID: попередньо визначене ім'я (ALPHABET, Base62, hex, number, …) або рядок ASCII. Залиште порожнім для значення xray-core за замовчуванням.",
"sessionIDLength": "Довжина Session ID",
"sessionIDLengthHint": "Довжина або діапазон (напр., 8-16) згенерованого session ID. Використовується лише коли задано таблицю; мінімум має бути більший за 0.",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -555,6 +555,10 @@
"paddingMethod": "Phương thức Padding",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sessionIDTable": "Bảng Session ID",
"sessionIDTableHint": "Tập ký tự để tạo session ID: một tên định sẵn (ALPHABET, Base62, hex, number, …) hoặc chuỗi ASCII. Để trống để dùng mặc định của xray-core.",
"sessionIDLength": "Độ dài Session ID",
"sessionIDLengthHint": "Độ dài hoặc khoảng (ví dụ 8-16) của session ID được tạo. Chỉ dùng khi đã đặt Bảng Session ID; giá trị nhỏ nhất phải lớn hơn 0.",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -554,6 +554,10 @@
"paddingMethod": "Padding 方法",
"sessionPlacement": "Session 位置",
"sessionKey": "Session Key",
"sessionIDTable": "会话 ID 字符表",
"sessionIDTableHint": "生成会话 ID 使用的字符集:预定义名称(ALPHABET、Base62、hex、number 等)或字面 ASCII 字符串。留空则使用 xray-core 默认值。",
"sessionIDLength": "会话 ID 长度",
"sessionIDLengthHint": "生成会话 ID 的长度或范围(如 8-16)。仅在设置了会话 ID 字符表时生效;最小值必须大于 0。",
"sequencePlacement": "Sequence 位置",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink 数据位置",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Padding 方法",
"sessionPlacement": "Session 位置",
"sessionKey": "Session Key",
"sessionIDTable": "工作階段 ID 字元表",
"sessionIDTableHint": "產生工作階段 ID 使用的字元集:預定義名稱(ALPHABET、Base62、hex、number 等)或字面 ASCII 字串。留空則使用 xray-core 預設值。",
"sessionIDLength": "工作階段 ID 長度",
"sessionIDLengthHint": "產生工作階段 ID 的長度或範圍(如 8-16)。僅在設定了工作階段 ID 字元表時生效;最小值必須大於 0。",
"sequencePlacement": "Sequence 位置",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink 資料位置",