Fix fragment exports for older Xray clients (#6702)

* Fix fragment exports for older Xray clients

* fix(link): tolerate a finalmask without tcp in panel share links

withLegacyFragmentRanges called finalmask.tcp.map unguarded, but stored
rows reach the link generator unparsed and dropEmptyFinalMask deletes an
empty tcp list on save. Any VMess/VLESS/Trojan/SS inbound with only UDP
masks or quicParams threw a TypeError in the QR, info and export-links
views. The Go counterpart already skipped a missing tcp.

Also trims the Go helper's comment to the two-line cap and drops a
[]string branch no JSON-decoded finalmask can reach.

---------

Co-authored-by: Artem K <a.kush@vkteam.ru>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Artem K
2026-10-02 18:38:44 +03:00
committed by GitHub
parent a716122ef2
commit 721de5adde
6 changed files with 413 additions and 7 deletions
+97
View File
@@ -3,6 +3,7 @@ package sub
import (
"encoding/base64"
"encoding/json"
"reflect"
"strings"
"testing"
@@ -1055,6 +1056,102 @@ func TestMarshalFinalMask_WithContent(t *testing.T) {
}
}
func TestMarshalFinalMaskAddsLegacyFragmentRanges(t *testing.T) {
lengths := []any{"5-10", "10-15", "15-20", "20-25", "25-30"}
delays := []any{"10-20", "5-20", "5-25", "15-25", "10-30"}
screenshotSettings := map[string]any{
"packets": "tlshello",
"lengths": lengths,
"delays": delays,
"maxSplit": "10-15",
}
explicitSettings := map[string]any{
"length": "1-2",
"lengths": []any{"8-9"},
"delay": "3-4",
"delays": []any{"6-7"},
}
emptySettings := map[string]any{
"lengths": []any{},
"delays": []any{},
}
legacyOnlySettings := map[string]any{"length": "40-50", "delay": "10-20"}
otherSettings := map[string]any{"lengths": []any{"30-40"}}
fm := map[string]any{
"tcp": []any{
map[string]any{"type": "fragment", "settings": screenshotSettings},
map[string]any{"type": "fragment", "settings": explicitSettings},
map[string]any{"type": "fragment", "settings": emptySettings},
map[string]any{"type": "fragment", "settings": legacyOnlySettings},
map[string]any{"type": "sudoku", "settings": otherSettings},
},
}
original, err := json.Marshal(fm)
if err != nil {
t.Fatalf("marshal input finalmask: %v", err)
}
encoded, ok := marshalFinalMask(fm)
if !ok {
t.Fatal("expected finalmask with fragment masks to be marshaled")
}
var got map[string]any
if err := json.Unmarshal([]byte(encoded), &got); err != nil {
t.Fatalf("unmarshal marshaled finalmask: %v", err)
}
masks, _ := got["tcp"].([]any)
if len(masks) != 5 {
t.Fatalf("tcp mask count = %d, want 5", len(masks))
}
settingsAt := func(index int) map[string]any {
t.Helper()
mask, _ := masks[index].(map[string]any)
settings, _ := mask["settings"].(map[string]any)
if settings == nil {
t.Fatalf("tcp[%d] settings missing: %#v", index, mask)
}
return settings
}
gotScreenshot := settingsAt(0)
if gotScreenshot["length"] != "25-30" || gotScreenshot["delay"] != "10-30" {
t.Fatalf("legacy ranges = (%v, %v), want last array entries", gotScreenshot["length"], gotScreenshot["delay"])
}
if !reflect.DeepEqual(gotScreenshot["lengths"], lengths) || !reflect.DeepEqual(gotScreenshot["delays"], delays) {
t.Fatalf("per-segment ranges changed: lengths=%#v delays=%#v", gotScreenshot["lengths"], gotScreenshot["delays"])
}
if gotScreenshot["packets"] != "tlshello" || gotScreenshot["maxSplit"] != "10-15" {
t.Fatalf("other fragment settings changed: %#v", gotScreenshot)
}
gotExplicit := settingsAt(1)
if gotExplicit["length"] != "1-2" || gotExplicit["delay"] != "3-4" {
t.Fatalf("explicit legacy ranges were overwritten: %#v", gotExplicit)
}
gotEmpty := settingsAt(2)
if _, exists := gotEmpty["length"]; exists {
t.Fatalf("empty lengths must not emit a fallback: %#v", gotEmpty)
}
if _, exists := gotEmpty["delay"]; exists {
t.Fatalf("empty delays must not emit a fallback: %#v", gotEmpty)
}
gotLegacyOnly := settingsAt(3)
if gotLegacyOnly["length"] != "40-50" || gotLegacyOnly["delay"] != "10-20" {
t.Fatalf("legacy-only ranges changed: %#v", gotLegacyOnly)
}
if _, exists := settingsAt(4)["length"]; exists {
t.Fatalf("non-fragment mask received a fallback: %#v", settingsAt(4))
}
after, err := json.Marshal(fm)
if err != nil {
t.Fatalf("marshal input finalmask after export: %v", err)
}
if string(after) != string(original) {
t.Fatalf("marshalFinalMask mutated its input:\nbefore: %s\nafter: %s", original, after)
}
}
func TestMarshalFinalMask_UnknownTypeIsDropped(t *testing.T) {
fm := map[string]any{
"tcp": []any{