diff --git a/sub/subJsonService.go b/sub/subJsonService.go index 72886051..fe4bfdd5 100644 --- a/sub/subJsonService.go +++ b/sub/subJsonService.go @@ -22,6 +22,8 @@ type SubJsonService struct { configJson map[string]any defaultOutbounds []json_util.RawMessage fragmentOrNoises bool + fragment string + noises string mux string inboundService service.InboundService @@ -79,6 +81,8 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string, configJson: configJson, defaultOutbounds: defaultOutbounds, fragmentOrNoises: fragmentOrNoises, + fragment: fragment, + noises: noises, mux: mux, SubService: subService, } @@ -232,6 +236,7 @@ func (s *SubJsonService) streamData(stream string) map[string]any { if s.fragmentOrNoises { streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "direct_out", "tcpKeepAliveIdle": 100}`) + s.applySubJsonFinalMask(streamSettings) } // remove proxy protocol @@ -255,6 +260,104 @@ func (s *SubJsonService) streamData(stream string) map[string]any { return streamSettings } +func (s *SubJsonService) applySubJsonFinalMask(streamSettings map[string]any) { + finalmask, _ := streamSettings["finalmask"].(map[string]any) + if finalmask == nil { + finalmask = map[string]any{} + } + + changed := false + if tcpMask, ok := buildSubJsonFragmentFinalMask(s.fragment); ok { + tcpMasks, _ := finalmask["tcp"].([]any) + finalmask["tcp"] = append(tcpMasks, tcpMask) + changed = true + } + if udpMask, ok := buildSubJsonNoisesFinalMask(s.noises); ok { + udpMasks, _ := finalmask["udp"].([]any) + finalmask["udp"] = append(udpMasks, udpMask) + changed = true + } + + if changed { + streamSettings["finalmask"] = finalmask + } +} + +func buildSubJsonFragmentFinalMask(fragment string) (map[string]any, bool) { + if fragment == "" { + return nil, false + } + + var settings map[string]any + if err := json.Unmarshal([]byte(fragment), &settings); err != nil || len(settings) == 0 { + return nil, false + } + + if interval, ok := settings["interval"]; ok { + if _, hasDelay := settings["delay"]; !hasDelay { + settings["delay"] = interval + } + delete(settings, "interval") + } + + return map[string]any{ + "type": "fragment", + "settings": settings, + }, true +} + +func buildSubJsonNoisesFinalMask(noises string) (map[string]any, bool) { + if noises == "" { + return nil, false + } + + var rawNoises []map[string]any + if err := json.Unmarshal([]byte(noises), &rawNoises); err != nil || len(rawNoises) == 0 { + return nil, false + } + + noiseItems := make([]any, 0, len(rawNoises)) + for _, rawNoise := range rawNoises { + item := map[string]any{} + noiseType, _ := rawNoise["type"].(string) + packet, hasPacket := rawNoise["packet"] + + if noiseType == "rand" { + if !hasPacket { + continue + } + item["rand"] = packet + } else if hasPacket { + if noiseType != "" { + item["type"] = noiseType + } + item["packet"] = packet + } else { + continue + } + + if delay, ok := rawNoise["delay"]; ok { + item["delay"] = delay + } + if randRange, ok := rawNoise["randRange"]; ok { + item["randRange"] = randRange + } + + noiseItems = append(noiseItems, item) + } + + if len(noiseItems) == 0 { + return nil, false + } + + return map[string]any{ + "type": "noise", + "settings": map[string]any{ + "noise": noiseItems, + }, + }, true +} + func (s *SubJsonService) removeAcceptProxy(setting any) map[string]any { netSettings, ok := setting.(map[string]any) if ok { @@ -442,7 +545,7 @@ func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, newStream["hysteriaSettings"] = outHyStream if finalmask, ok := hyStream["finalmask"].(map[string]any); ok { - newStream["finalmask"] = finalmask + newStream["finalmask"] = mergeFinalMask(newStream["finalmask"], finalmask) } newStream["network"] = "hysteria" @@ -454,6 +557,41 @@ func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, return result } +func mergeFinalMask(base any, extra map[string]any) map[string]any { + merged := map[string]any{} + if baseMap, ok := base.(map[string]any); ok { + for key, value := range baseMap { + switch key { + case "tcp", "udp": + if masks, ok := value.([]any); ok { + merged[key] = append([]any(nil), masks...) + } + default: + merged[key] = value + } + } + } + + for key, value := range extra { + switch key { + case "tcp", "udp": + baseMasks, _ := merged[key].([]any) + extraMasks, _ := value.([]any) + if len(extraMasks) > 0 { + merged[key] = append(baseMasks, extraMasks...) + } + case "quicParams": + if _, exists := merged[key]; !exists { + merged[key] = value + } + default: + merged[key] = value + } + } + + return merged +} + type Outbound struct { Protocol string `json:"protocol"` Tag string `json:"tag"` diff --git a/sub/subJsonService_test.go b/sub/subJsonService_test.go new file mode 100644 index 00000000..47653ce7 --- /dev/null +++ b/sub/subJsonService_test.go @@ -0,0 +1,106 @@ +package sub + +import ( + "encoding/json" + "testing" +) + +func TestSubJsonServiceKeepsDirectOutAndAddsFinalMask(t *testing.T) { + fragment := `{"packets":"1-3","length":"100-200","interval":"10-20","maxSplit":"100-200"}` + noises := `[{"type":"rand","packet":"10-20","delay":"10-16","applyTo":"ip"},{"type":"base64","packet":"SGVsbG8=","delay":"5"}]` + svc := NewSubJsonService(fragment, noises, "", "", nil) + + var directOut map[string]any + if err := json.Unmarshal(svc.defaultOutbounds[len(svc.defaultOutbounds)-1], &directOut); err != nil { + t.Fatalf("failed to unmarshal compatibility direct_out: %v", err) + } + if directOut["tag"] != "direct_out" { + t.Fatalf("direct_out tag = %v, want direct_out", directOut["tag"]) + } + directSettings, _ := directOut["settings"].(map[string]any) + if _, ok := directSettings["fragment"]; !ok { + t.Fatal("compatibility direct_out is missing freedom fragment") + } + if _, ok := directSettings["noises"]; !ok { + t.Fatal("compatibility direct_out is missing freedom noises") + } + + stream := svc.streamData(`{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}}}`) + if _, ok := stream["sockopt"]; !ok { + t.Fatal("streamSettings is missing direct_out sockopt compatibility path") + } + + finalmask, _ := stream["finalmask"].(map[string]any) + if finalmask == nil { + t.Fatal("streamSettings is missing finalmask") + } + + tcpMasks, _ := finalmask["tcp"].([]any) + if len(tcpMasks) != 1 { + t.Fatalf("finalmask tcp masks len = %d, want 1", len(tcpMasks)) + } + fragmentMask, _ := tcpMasks[0].(map[string]any) + if fragmentMask["type"] != "fragment" { + t.Fatalf("tcp mask type = %v, want fragment", fragmentMask["type"]) + } + fragmentSettings, _ := fragmentMask["settings"].(map[string]any) + if fragmentSettings["delay"] != "10-20" { + t.Fatalf("fragment delay = %v, want 10-20", fragmentSettings["delay"]) + } + if _, ok := fragmentSettings["interval"]; ok { + t.Fatal("finalmask fragment should use delay, not interval") + } + + udpMasks, _ := finalmask["udp"].([]any) + if len(udpMasks) != 1 { + t.Fatalf("finalmask udp masks len = %d, want 1", len(udpMasks)) + } + noiseMask, _ := udpMasks[0].(map[string]any) + if noiseMask["type"] != "noise" { + t.Fatalf("udp mask type = %v, want noise", noiseMask["type"]) + } + noiseSettings, _ := noiseMask["settings"].(map[string]any) + noiseItems, _ := noiseSettings["noise"].([]any) + if len(noiseItems) != 2 { + t.Fatalf("noise items len = %d, want 2", len(noiseItems)) + } + randItem, _ := noiseItems[0].(map[string]any) + if randItem["rand"] != "10-20" { + t.Fatalf("rand noise item rand = %v, want 10-20", randItem["rand"]) + } + if _, ok := randItem["applyTo"]; ok { + t.Fatal("finalmask noise should not carry freedom noises applyTo") + } + packetItem, _ := noiseItems[1].(map[string]any) + if packetItem["type"] != "base64" || packetItem["packet"] != "SGVsbG8=" { + t.Fatalf("packet noise item = %#v, want base64 packet", packetItem) + } +} + +func TestSubJsonServiceAppendsFinalMaskToExistingMasks(t *testing.T) { + fragment := `{"packets":"tlshello","length":"100-200","interval":"0"}` + svc := NewSubJsonService(fragment, "", "", "", nil) + + stream := svc.streamData(`{ + "network":"tcp", + "security":"none", + "tcpSettings":{"header":{"type":"none"}}, + "finalmask":{"tcp":[{"type":"sudoku"}],"udp":[{"type":"salamander","settings":{"password":"secret"}}]} + }`) + + finalmask, _ := stream["finalmask"].(map[string]any) + tcpMasks, _ := finalmask["tcp"].([]any) + if len(tcpMasks) != 2 { + t.Fatalf("finalmask tcp masks len = %d, want 2", len(tcpMasks)) + } + firstTCP, _ := tcpMasks[0].(map[string]any) + secondTCP, _ := tcpMasks[1].(map[string]any) + if firstTCP["type"] != "sudoku" || secondTCP["type"] != "fragment" { + t.Fatalf("tcp masks = %#v, want existing mask followed by subscription fragment", tcpMasks) + } + + udpMasks, _ := finalmask["udp"].([]any) + if len(udpMasks) != 1 { + t.Fatalf("finalmask udp masks len = %d, want existing udp mask preserved", len(udpMasks)) + } +}