mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat(sub): add finalmask support to JSON subscriptions
This commit is contained in:
parent
5c1d64b841
commit
f4a07121a9
2 changed files with 245 additions and 1 deletions
|
|
@ -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"`
|
||||
|
|
|
|||
106
sub/subJsonService_test.go
Normal file
106
sub/subJsonService_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue