From abab7cd0005c51d06d33c9993a166024fd1f0781 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 15 Jul 2026 02:36:15 +0200 Subject: [PATCH] fix(sub): stop the subscription from 500ing on valid-but-unusual stream settings The raw share-link generators used unchecked type assertions and unguarded array indexing: an empty Reality shortIds/serverNames array (random.Num(0) panics), a tcp-http header with no request block or an empty request.path, a grpc block missing its keys, empty stream settings, and a non-string Host header all panicked mid-generation. Because getSubs loops every client's link with no recover, one such client 500s the entire subscription for everyone. The sibling JSON, Clash and frontend generators already guard these; make the raw generators match with comma-ok assertions and length checks. --- internal/sub/service.go | 51 +++++++++++++++++++--------------- internal/sub/sub_panic_test.go | 43 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 internal/sub/sub_panic_test.go diff --git a/internal/sub/service.go b/internal/sub/service.go index b0ad4a108..2fded5d06 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -781,7 +781,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { } uuid := client.ID port := inbound.Port - streamNetwork := stream["network"].(string) + streamNetwork, _ := stream["network"].(string) params := make(map[string]string) params["type"] = streamNetwork @@ -840,7 +840,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string } password := encodeUserinfo(client.Password) port := inbound.Port - streamNetwork := stream["network"].(string) + streamNetwork, _ := stream["network"].(string) params := make(map[string]string) params["type"] = streamNetwork @@ -913,9 +913,9 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st } settings := s.linkSettings(inbound) - inboundPassword := settings["password"].(string) - method := settings["method"].(string) - streamNetwork := stream["network"].(string) + inboundPassword, _ := settings["password"].(string) + method, _ := settings["method"].(string) + streamNetwork, _ := stream["network"].(string) params := make(map[string]string) params["type"] = streamNetwork @@ -1206,9 +1206,11 @@ func applyShareNetworkParams(stream map[string]any, streamNetwork string, params header, _ := tcp["header"].(map[string]any) typeStr, _ := header["type"].(string) if typeStr == "http" { - request := header["request"].(map[string]any) + request, _ := header["request"].(map[string]any) requestPath, _ := request["path"].([]any) - params["path"] = requestPath[0].(string) + if len(requestPath) > 0 { + params["path"], _ = requestPath[0].(string) + } host := "" if response, ok := header["response"].(map[string]any); ok { if respHeaders, ok := response["headers"].(map[string]any); ok { @@ -1229,9 +1231,9 @@ func applyShareNetworkParams(stream map[string]any, streamNetwork string, params applyPathAndHostParams(ws, params) case "grpc": grpc, _ := stream["grpcSettings"].(map[string]any) - params["serviceName"] = grpc["serviceName"].(string) + params["serviceName"], _ = grpc["serviceName"].(string) params["authority"], _ = grpc["authority"].(string) - if grpc["multiMode"].(bool) { + if mm, _ := grpc["multiMode"].(bool); mm { params["mode"] = "multi" } case "httpupgrade": @@ -1262,9 +1264,11 @@ func applyVmessNetworkParams(stream map[string]any, network string, obj map[stri typeStr, _ := header["type"].(string) obj["type"] = typeStr if typeStr == "http" { - request := header["request"].(map[string]any) + request, _ := header["request"].(map[string]any) requestPath, _ := request["path"].([]any) - obj["path"] = requestPath[0].(string) + if len(requestPath) > 0 { + obj["path"], _ = requestPath[0].(string) + } host := "" if response, ok := header["response"].(map[string]any); ok { if respHeaders, ok := response["headers"].(map[string]any); ok { @@ -1284,9 +1288,9 @@ func applyVmessNetworkParams(stream map[string]any, network string, obj map[stri applyPathAndHostObj(ws, obj) case "grpc": grpc, _ := stream["grpcSettings"].(map[string]any) - obj["path"] = grpc["serviceName"].(string) - obj["authority"] = grpc["authority"].(string) - if grpc["multiMode"].(bool) { + obj["path"], _ = grpc["serviceName"].(string) + obj["authority"], _ = grpc["authority"].(string) + if mm, _ := grpc["multiMode"].(bool); mm { obj["type"] = "multi" } case "httpupgrade": @@ -1451,15 +1455,17 @@ func applyShareRealityParams(stream map[string]any, params map[string]string, cl realitySettings, _ := searchKey(realitySetting, "settings") if realitySetting != nil { if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { - sNames, _ := sniValue.([]any) - params["sni"] = sNames[random.Num(len(sNames))].(string) + if sNames, _ := sniValue.([]any); len(sNames) > 0 { + params["sni"], _ = sNames[random.Num(len(sNames))].(string) + } } if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { params["pbk"], _ = pbkValue.(string) } if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { - shortIds, _ := sidValue.([]any) - params["sid"] = shortIds[random.Num(len(shortIds))].(string) + if shortIds, _ := sidValue.([]any); len(shortIds) > 0 { + params["sid"], _ = shortIds[random.Num(len(shortIds))].(string) + } } if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { if fp, ok := fpValue.(string); ok && len(fp) > 0 { @@ -2422,12 +2428,13 @@ func searchHost(headers any) string { case []any: hosts, _ := v.([]any) if len(hosts) > 0 { - return hosts[0].(string) - } else { - return "" + h, _ := hosts[0].(string) + return h } + return "" case any: - return v.(string) + h, _ := v.(string) + return h } } } diff --git a/internal/sub/sub_panic_test.go b/internal/sub/sub_panic_test.go new file mode 100644 index 000000000..10e8bf3ee --- /dev/null +++ b/internal/sub/sub_panic_test.go @@ -0,0 +1,43 @@ +package sub + +import ( + "fmt" + "testing" + + "github.com/gin-gonic/gin" +) + +// A subscription is built by iterating every client's share link with no +// recover(), so any panic in the link generators 500s the whole subscription +// for every client. Valid-but-unusual stream settings (an empty Reality +// shortIds/serverNames array, a tcp-http header with no request, a grpc block +// missing its keys) must therefore produce a link, not a panic. +func TestGetSubsToleratesUnusualStreamSettings(t *testing.T) { + gin.SetMode(gin.TestMode) + cases := []struct { + name string + stream string + }{ + {"reality empty arrays", `{"network":"tcp","security":"reality","realitySettings":{"serverNames":[],"shortIds":[],"settings":{"publicKey":"pk"}}}`}, + {"tcp http missing request", `{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"http","response":{"headers":{}}}}}`}, + {"tcp http empty path", `{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"http","request":{"path":[]}}}}`}, + {"grpc missing keys", `{"network":"grpc","security":"none","grpcSettings":{}}`}, + {"empty stream settings", `{}`}, + } + + for i, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + seedSubDB(t) + subId := fmt.Sprintf("s%d", i) + seedSubInbound(t, subId, fmt.Sprintf("t%d", i), 46000+i, 1, tc.stream) + + links, _, _, _, err := NewSubService("").GetSubs(subId, "req.example.com") + if err != nil { + t.Fatalf("GetSubs errored: %v", err) + } + if len(links) != 1 { + t.Fatalf("expected 1 share link, got %d", len(links)) + } + }) + } +}