mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-19 17:40:59 +00:00
fix(inbound): strip XHTTP client-only fields from xray config, keep for subscriptions (#5349)
Inbound XMUX and other client-side xHTTP knobs were written into bin/config.json even though xray-core's server listener ignores them. Strip them in GenXrayInboundConfig while leaving the DB row intact so buildXhttpExtra still pushes defaults to clients via share links.
This commit is contained in:
@@ -66,7 +66,7 @@ describe('normalizeXhttpForWire stream-one', () => {
|
|||||||
expect(out).not.toHaveProperty('scMaxEachPostBytes');
|
expect(out).not.toHaveProperty('scMaxEachPostBytes');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps inbound xmux when enableXmux is on (for the share-link extra)', () => {
|
it('keeps inbound xmux when enableXmux is on (stored for subscription extra; stripped from xray config on Go side)', () => {
|
||||||
const out = normalizeXhttpForWire({
|
const out = normalizeXhttpForWire({
|
||||||
path: '/app',
|
path: '/app',
|
||||||
mode: 'auto',
|
mode: 'auto',
|
||||||
|
|||||||
@@ -225,6 +225,49 @@ func jsonStringFieldFromRaw(r json.RawMessage) string {
|
|||||||
return string(trimmed)
|
return string(trimmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StripInboundXhttpClientFields removes xHTTP knobs that belong on the
|
||||||
|
// client dialer and subscription share-link extras only. xray-core's XHTTP
|
||||||
|
// inbound listener does not consume them; the panel still stores them on
|
||||||
|
// the inbound row so buildXhttpExtra can push defaults to clients.
|
||||||
|
func StripInboundXhttpClientFields(streamSettings string) (string, bool) {
|
||||||
|
if streamSettings == "" {
|
||||||
|
return streamSettings, false
|
||||||
|
}
|
||||||
|
var stream map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
|
||||||
|
return streamSettings, false
|
||||||
|
}
|
||||||
|
if stream["network"] != "xhttp" {
|
||||||
|
return streamSettings, false
|
||||||
|
}
|
||||||
|
xhttp, ok := stream["xhttpSettings"].(map[string]any)
|
||||||
|
if !ok || len(xhttp) == 0 {
|
||||||
|
return streamSettings, false
|
||||||
|
}
|
||||||
|
clientOnly := []string{
|
||||||
|
"xmux",
|
||||||
|
"downloadSettings",
|
||||||
|
"scMinPostsIntervalMs",
|
||||||
|
"uplinkChunkSize",
|
||||||
|
"noGRPCHeader",
|
||||||
|
}
|
||||||
|
changed := false
|
||||||
|
for _, key := range clientOnly {
|
||||||
|
if _, has := xhttp[key]; has {
|
||||||
|
delete(xhttp, key)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
return streamSettings, false
|
||||||
|
}
|
||||||
|
out, err := json.MarshalIndent(stream, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return streamSettings, false
|
||||||
|
}
|
||||||
|
return string(out), true
|
||||||
|
}
|
||||||
|
|
||||||
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
|
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
|
||||||
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||||
listen := i.Listen
|
listen := i.Listen
|
||||||
@@ -248,12 +291,16 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
|||||||
settings = stripped
|
settings = stripped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
streamSettings := i.StreamSettings
|
||||||
|
if stripped, ok := StripInboundXhttpClientFields(streamSettings); ok {
|
||||||
|
streamSettings = stripped
|
||||||
|
}
|
||||||
return &xray.InboundConfig{
|
return &xray.InboundConfig{
|
||||||
Listen: json_util.RawMessage(listen),
|
Listen: json_util.RawMessage(listen),
|
||||||
Port: i.Port,
|
Port: i.Port,
|
||||||
Protocol: protocol,
|
Protocol: protocol,
|
||||||
Settings: json_util.RawMessage(settings),
|
Settings: json_util.RawMessage(settings),
|
||||||
StreamSettings: json_util.RawMessage(i.StreamSettings),
|
StreamSettings: json_util.RawMessage(streamSettings),
|
||||||
Tag: i.Tag,
|
Tag: i.Tag,
|
||||||
Sniffing: json_util.RawMessage(i.Sniffing),
|
Sniffing: json_util.RawMessage(i.Sniffing),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,3 +188,85 @@ func TestInboundClientIpsUnmarshalJSONAcceptsBothShapes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStripInboundXhttpClientFields_RemovesClientOnlyKnobs(t *testing.T) {
|
||||||
|
stream := `{
|
||||||
|
"network": "xhttp",
|
||||||
|
"security": "reality",
|
||||||
|
"xhttpSettings": {
|
||||||
|
"path": "/app",
|
||||||
|
"host": "example.com",
|
||||||
|
"mode": "stream-one",
|
||||||
|
"xmux": { "maxConcurrency": "16-32" },
|
||||||
|
"downloadSettings": { "network": "xhttp" },
|
||||||
|
"scMinPostsIntervalMs": "20-40",
|
||||||
|
"uplinkChunkSize": 4096,
|
||||||
|
"noGRPCHeader": true
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
out, changed := StripInboundXhttpClientFields(stream)
|
||||||
|
if !changed {
|
||||||
|
t.Fatal("expected client-only xhttp fields to be stripped")
|
||||||
|
}
|
||||||
|
if strings.Contains(out, `"xmux"`) {
|
||||||
|
t.Fatalf("xmux should be removed from xray config stream: %s", out)
|
||||||
|
}
|
||||||
|
for _, key := range []string{"downloadSettings", "scMinPostsIntervalMs", "uplinkChunkSize", "noGRPCHeader"} {
|
||||||
|
if strings.Contains(out, `"`+key+`"`) {
|
||||||
|
t.Fatalf("%s should be removed from xray config stream: %s", key, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var parsed map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
||||||
|
t.Fatalf("invalid JSON: %v", err)
|
||||||
|
}
|
||||||
|
xhttp := parsed["xhttpSettings"].(map[string]any)
|
||||||
|
if xhttp["path"] != "/app" || xhttp["host"] != "example.com" {
|
||||||
|
t.Fatalf("server fields must survive: %#v", xhttp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStripInboundXhttpClientFields_UnchangedWithoutClientFields(t *testing.T) {
|
||||||
|
stream := `{"network":"xhttp","xhttpSettings":{"path":"/app","mode":"stream-one"}}`
|
||||||
|
out, changed := StripInboundXhttpClientFields(stream)
|
||||||
|
if changed {
|
||||||
|
t.Fatalf("expected no change, got: %s", out)
|
||||||
|
}
|
||||||
|
if out != stream {
|
||||||
|
t.Fatalf("unchanged stream must be returned verbatim")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStripInboundXhttpClientFields_NonXhttpPassthrough(t *testing.T) {
|
||||||
|
stream := `{"network":"ws","wsSettings":{"path":"/"}}`
|
||||||
|
out, changed := StripInboundXhttpClientFields(stream)
|
||||||
|
if changed || out != stream {
|
||||||
|
t.Fatalf("non-xhttp stream must pass through unchanged, got changed=%v out=%s", changed, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenXrayInboundConfig_OmitsInboundXmuxButDbRowUnchanged(t *testing.T) {
|
||||||
|
stream := `{
|
||||||
|
"network": "xhttp",
|
||||||
|
"xhttpSettings": {
|
||||||
|
"path": "/app",
|
||||||
|
"mode": "stream-one",
|
||||||
|
"xmux": { "maxConcurrency": "16-32", "hMaxRequestTimes": "600-900" }
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
in := Inbound{
|
||||||
|
Protocol: VLESS,
|
||||||
|
Port: 443,
|
||||||
|
Listen: "0.0.0.0",
|
||||||
|
Tag: "in-xhttp",
|
||||||
|
Settings: `{"clients":[],"decryption":"none"}`,
|
||||||
|
StreamSettings: stream,
|
||||||
|
}
|
||||||
|
cfg := in.GenXrayInboundConfig()
|
||||||
|
if strings.Contains(string(cfg.StreamSettings), `"xmux"`) {
|
||||||
|
t.Fatalf("GenXrayInboundConfig must not emit xmux: %s", cfg.StreamSettings)
|
||||||
|
}
|
||||||
|
if strings.Contains(in.StreamSettings, `"xmux"`) == false {
|
||||||
|
t.Fatal("inbound row streamSettings must still carry xmux for subscriptions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user