فهرست منبع

fix(sub): emit Shadowsocks http-header links as SIP002 obfs-local plugin

v2rayN's SS parser only reads the SIP002 `plugin` query param; it ignores the
xray-native type/headerType/host/path, so an SS link with a TCP http header
imported as plain SS and failed to connect. Re-encode the http header as
`plugin=obfs-local;obfs=http;obfs-host=<host>`, which v2rayN maps to an
xray tcp/http-header outbound. Mirrored in the frontend link generator.

Note: v2rayN carries only the host and forces request path "/", so this matches
an inbound whose header path is "/" (the default); xray validates path, not host.
MHSanaei 1 روز پیش
والد
کامیت
21e9b94bb4
3فایلهای تغییر یافته به همراه52 افزوده شده و 0 حذف شده
  1. 12 0
      frontend/src/lib/xray/inbound-link.ts
  2. 28 0
      internal/sub/characterization_test.go
  3. 12 0
      internal/sub/service.go

+ 12 - 0
frontend/src/lib/xray/inbound-link.ts

@@ -595,6 +595,18 @@ export function genShadowsocksLink(input: GenShadowsocksLinkInput): string {
     applyExternalProxyTLSParams(externalProxy, params, security);
   }
 
+  // SIP002 clients (v2rayN) ignore type/headerType/host/path and only read
+  // `plugin`. Re-encode a TCP http header as obfs-local so they build a
+  // matching tcp/http outbound (v2rayN forces request path "/").
+  if ((stream.network ?? 'tcp') === 'tcp' && params.get('headerType') === 'http') {
+    const host = params.get('host') ?? '';
+    params.delete('type');
+    params.delete('headerType');
+    params.delete('host');
+    params.delete('path');
+    params.set('plugin', `obfs-local;obfs=http;obfs-host=${host}`);
+  }
+
   const isSS2022 = settings.method.substring(0, 4) === '2022';
   const isSSMultiUser = settings.method !== '2022-blake3-chacha20-poly1305';
   const passwords: string[] = [];

+ 28 - 0
internal/sub/characterization_test.go

@@ -174,6 +174,34 @@ func TestChar_C3_ShadowsocksExternalProxy(t *testing.T) {
 	}
 }
 
+// A TCP http header on Shadowsocks must be emitted as a SIP002 obfs-local
+// plugin (what v2rayN parses), not the xray-native type/headerType/host/path
+// params (which SIP002 clients silently ignore).
+func TestShadowsocksTcpHttpHeaderUsesObfsLocalPlugin(t *testing.T) {
+	stream := `{
+		"network":"tcp","security":"none",
+		"tcpSettings":{"header":{"type":"http","request":{"path":["/"],"headers":{"Host":["test"]}}}}
+	}`
+	in := &model.Inbound{
+		Listen:         "203.0.113.1",
+		Port:           38143,
+		Protocol:       model.Shadowsocks,
+		Remark:         "ss",
+		Settings:       `{"method":"2022-blake3-aes-256-gcm","password":"inboundpw","clients":[{"password":"clientpw","email":"user"}]}`,
+		StreamSettings: stream,
+	}
+	s := &SubService{}
+	got := s.genShadowsocksLink(in, "user")
+	if !strings.Contains(got, "plugin=obfs-local%3Bobfs%3Dhttp%3Bobfs-host%3Dtest") {
+		t.Fatalf("expected obfs-local plugin param, got: %q", got)
+	}
+	for _, leak := range []string{"headerType=", "type=tcp", "host=test", "path="} {
+		if strings.Contains(got, leak) {
+			t.Fatalf("xray-native param %q must not leak into SS link: %q", leak, got)
+		}
+	}
+}
+
 // C6 — Hysteria2, TLS, 1 externalProxy entry with a cert pin. Guards that the
 // Hysteria generator stays on its own path (hex pinSHA256, not pcs) and is NOT
 // folded into the unified builder. Pin hex is derived, so Contains is used.

+ 12 - 0
internal/sub/service.go

@@ -704,6 +704,18 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
 		applyShareTLSParams(stream, params)
 	}
 
+	// SIP002 clients (v2rayN) ignore the xray-native type/headerType/host/path
+	// params and only read `plugin`. Re-encode a TCP http header as obfs-local so
+	// they build a matching tcp/http outbound (v2rayN forces request path "/").
+	if streamNetwork == "tcp" && params["headerType"] == "http" {
+		host := params["host"]
+		delete(params, "type")
+		delete(params, "headerType")
+		delete(params, "host")
+		delete(params, "path")
+		params["plugin"] = "obfs-local;obfs=http;obfs-host=" + host
+	}
+
 	encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password)
 	if method[0] == '2' {
 		encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)