feat(sub): add finalmask support to JSON subscriptions

This commit is contained in:
biohazardous-man 2026-06-04 15:20:28 +03:00
parent 5c1d64b841
commit f4a07121a9
2 changed files with 245 additions and 1 deletions

View file

@ -22,6 +22,8 @@ type SubJsonService struct {
configJson map[string]any configJson map[string]any
defaultOutbounds []json_util.RawMessage defaultOutbounds []json_util.RawMessage
fragmentOrNoises bool fragmentOrNoises bool
fragment string
noises string
mux string mux string
inboundService service.InboundService inboundService service.InboundService
@ -79,6 +81,8 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
configJson: configJson, configJson: configJson,
defaultOutbounds: defaultOutbounds, defaultOutbounds: defaultOutbounds,
fragmentOrNoises: fragmentOrNoises, fragmentOrNoises: fragmentOrNoises,
fragment: fragment,
noises: noises,
mux: mux, mux: mux,
SubService: subService, SubService: subService,
} }
@ -232,6 +236,7 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
if s.fragmentOrNoises { if s.fragmentOrNoises {
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "direct_out", "tcpKeepAliveIdle": 100}`) streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "direct_out", "tcpKeepAliveIdle": 100}`)
s.applySubJsonFinalMask(streamSettings)
} }
// remove proxy protocol // remove proxy protocol
@ -255,6 +260,104 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
return streamSettings 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 { func (s *SubJsonService) removeAcceptProxy(setting any) map[string]any {
netSettings, ok := setting.(map[string]any) netSettings, ok := setting.(map[string]any)
if ok { if ok {
@ -442,7 +545,7 @@ func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any,
newStream["hysteriaSettings"] = outHyStream newStream["hysteriaSettings"] = outHyStream
if finalmask, ok := hyStream["finalmask"].(map[string]any); ok { if finalmask, ok := hyStream["finalmask"].(map[string]any); ok {
newStream["finalmask"] = finalmask newStream["finalmask"] = mergeFinalMask(newStream["finalmask"], finalmask)
} }
newStream["network"] = "hysteria" newStream["network"] = "hysteria"
@ -454,6 +557,41 @@ func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any,
return result 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 { type Outbound struct {
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Tag string `json:"tag"` Tag string `json:"tag"`

106
sub/subJsonService_test.go Normal file
View file

@ -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))
}
}