mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-04 13:12:07 +03:00
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:
@@ -144,9 +144,36 @@ function hasShareableFinalMaskValue(value: unknown): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function withLegacyFragmentRanges(finalmask: FinalMaskStreamSettings): FinalMaskStreamSettings {
|
||||
// Stored rows reach here unparsed: dropEmptyFinalMask deletes an empty `tcp` on save.
|
||||
if (!Array.isArray(finalmask.tcp)) return finalmask;
|
||||
let changed = false;
|
||||
const tcp = finalmask.tcp.map((mask) => {
|
||||
if (mask.type !== 'fragment' || !mask.settings) return mask;
|
||||
|
||||
const settings = mask.settings;
|
||||
const legacy: Record<string, unknown> = {};
|
||||
if (settings.length === undefined && Array.isArray(settings.lengths)) {
|
||||
const length = settings.lengths.at(-1);
|
||||
if (typeof length === 'string' && length.trim().length > 0) legacy.length = length;
|
||||
}
|
||||
if (settings.delay === undefined && Array.isArray(settings.delays)) {
|
||||
const delay = settings.delays.at(-1);
|
||||
if (typeof delay === 'string' && delay.trim().length > 0) legacy.delay = delay;
|
||||
}
|
||||
if (Object.keys(legacy).length === 0) return mask;
|
||||
|
||||
changed = true;
|
||||
return { ...mask, settings: { ...settings, ...legacy } };
|
||||
});
|
||||
|
||||
return changed ? { ...finalmask, tcp } : finalmask;
|
||||
}
|
||||
|
||||
function serializeFinalMask(finalmask: FinalMaskStreamSettings | undefined): string {
|
||||
if (!finalmask) return '';
|
||||
return hasShareableFinalMaskValue(finalmask) ? JSON.stringify(finalmask) : '';
|
||||
const shareable = withLegacyFragmentRanges(finalmask);
|
||||
return hasShareableFinalMaskValue(shareable) ? JSON.stringify(shareable) : '';
|
||||
}
|
||||
|
||||
function applyFinalMaskToObj(
|
||||
|
||||
@@ -19,9 +19,11 @@ import {
|
||||
preferPublicHost,
|
||||
resolveAddr,
|
||||
} from '@/lib/xray/inbound-link';
|
||||
import { type DbInboundLike, inboundFromDb } from '@/lib/xray/inbound-from-db';
|
||||
import { InboundSchema } from '@/schemas/api/inbound';
|
||||
import type { AmneziawgInboundSettings } from '@/schemas/protocols/inbound/amneziawg';
|
||||
import type { WireguardInboundSettings } from '@/schemas/protocols/inbound/wireguard';
|
||||
import type { FinalMaskStreamSettings } from '@/schemas/protocols/stream/finalmask';
|
||||
|
||||
// reverse of inbound-link.ts's own toBase64Url, for asserting on the
|
||||
// decoded vpn:// payload without depending on that helper being exported.
|
||||
@@ -179,6 +181,129 @@ describe('genVlessLink vlessRoute', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('genVlessLink TCP fragment finalmask compatibility', () => {
|
||||
const [, raw] = fixturesForProtocol('vless')[0];
|
||||
const baseInbound = InboundSchema.parse(raw);
|
||||
const clientId = (raw as { settings: { clients: Array<{ id: string }> } }).settings.clients[0].id;
|
||||
|
||||
function linkFor(finalmask: FinalMaskStreamSettings): string {
|
||||
if (!baseInbound.streamSettings) throw new Error('fixture needs stream settings');
|
||||
baseInbound.streamSettings.finalmask = finalmask;
|
||||
|
||||
return genVlessLink({
|
||||
inbound: baseInbound,
|
||||
address: 'example.test',
|
||||
port: baseInbound.port,
|
||||
clientId,
|
||||
});
|
||||
}
|
||||
|
||||
function finalmaskFrom(link: string): Record<string, unknown> {
|
||||
const encoded = new URL(link).searchParams.get('fm');
|
||||
if (!encoded) throw new Error('link needs an fm parameter');
|
||||
return JSON.parse(encoded) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
it('emits the final configured length and delay for legacy clients', () => {
|
||||
const finalmask: FinalMaskStreamSettings = {
|
||||
tcp: [
|
||||
{
|
||||
type: 'fragment',
|
||||
settings: {
|
||||
packets: 'tlshello',
|
||||
lengths: ['5-10', '10-15', '15-20', '20-25', '25-30'],
|
||||
delays: ['10-20', '5-20', '5-25', '15-25', '10-30'],
|
||||
maxSplit: '10-15',
|
||||
},
|
||||
},
|
||||
],
|
||||
udp: [],
|
||||
};
|
||||
const tcpMasks = finalmask.tcp;
|
||||
const fragmentSettings = finalmask.tcp[0].settings;
|
||||
const lengths = fragmentSettings?.lengths;
|
||||
const delays = fragmentSettings?.delays;
|
||||
const before = structuredClone(finalmask);
|
||||
|
||||
const exported = finalmaskFrom(linkFor(finalmask));
|
||||
const fragment = (exported.tcp as Array<{ settings: Record<string, unknown> }>)[0].settings;
|
||||
|
||||
expect(fragment).toEqual({
|
||||
packets: 'tlshello',
|
||||
lengths: ['5-10', '10-15', '15-20', '20-25', '25-30'],
|
||||
delays: ['10-20', '5-20', '5-25', '15-25', '10-30'],
|
||||
maxSplit: '10-15',
|
||||
length: '25-30',
|
||||
delay: '10-30',
|
||||
});
|
||||
expect(finalmask).toEqual(before);
|
||||
expect(finalmask.tcp).toBe(tcpMasks);
|
||||
expect(finalmask.tcp[0].settings).toBe(fragmentSettings);
|
||||
expect(fragmentSettings?.lengths).toBe(lengths);
|
||||
expect(fragmentSettings?.delays).toBe(delays);
|
||||
});
|
||||
|
||||
it('preserves explicit legacy fields and does not add them to other masks', () => {
|
||||
const finalmask: FinalMaskStreamSettings = {
|
||||
tcp: [
|
||||
{
|
||||
type: 'fragment',
|
||||
settings: {
|
||||
length: '40-50',
|
||||
delay: '3-4',
|
||||
lengths: ['5-10', '25-30'],
|
||||
delays: ['10-20', '10-30'],
|
||||
},
|
||||
},
|
||||
{ type: 'sudoku', settings: { lengths: ['5-10'], delays: ['10-20'] } },
|
||||
],
|
||||
udp: [{ type: 'noise', settings: { lengths: ['5-10'], delays: ['10-20'] } }],
|
||||
};
|
||||
|
||||
const exported = finalmaskFrom(linkFor(finalmask));
|
||||
|
||||
expect(exported).toEqual(finalmask);
|
||||
});
|
||||
|
||||
it('exports a stored UDP-only finalmask whose empty tcp list was dropped on save', () => {
|
||||
const udpOnly = { udp: [{ type: 'salamander', settings: { password: 'p' } }] };
|
||||
const inbound = inboundFromDb({
|
||||
...(raw as unknown as DbInboundLike),
|
||||
streamSettings: { ...(raw.streamSettings as Record<string, unknown>), finalmask: udpOnly },
|
||||
});
|
||||
|
||||
const link = genVlessLink({ inbound, address: 'example.test', port: inbound.port, clientId });
|
||||
|
||||
expect(finalmaskFrom(link)).toEqual(udpOnly);
|
||||
});
|
||||
|
||||
it('does not create empty legacy values or search before a mixed-type last entry', () => {
|
||||
const finalmask: FinalMaskStreamSettings = {
|
||||
tcp: [
|
||||
{ type: 'fragment', settings: { packets: 'tlshello', lengths: [], delays: [] } },
|
||||
{
|
||||
type: 'fragment',
|
||||
settings: { packets: 'tlshello', lengths: ['5-10', 25], delays: ['10-20', null] },
|
||||
},
|
||||
{ type: 'fragment', settings: { lengths: [' '], delays: ['\t'] } },
|
||||
],
|
||||
udp: [],
|
||||
};
|
||||
|
||||
const exported = finalmaskFrom(linkFor(finalmask));
|
||||
const [emptyRanges, mixedRanges, blankRanges] = exported.tcp as Array<{
|
||||
settings: Record<string, unknown>;
|
||||
}>;
|
||||
|
||||
expect(emptyRanges.settings).not.toHaveProperty('length');
|
||||
expect(emptyRanges.settings).not.toHaveProperty('delay');
|
||||
expect(mixedRanges.settings).not.toHaveProperty('length');
|
||||
expect(mixedRanges.settings).not.toHaveProperty('delay');
|
||||
expect(blankRanges.settings).not.toHaveProperty('length');
|
||||
expect(blankRanges.settings).not.toHaveProperty('delay');
|
||||
});
|
||||
});
|
||||
|
||||
describe('genTrojanLink', () => {
|
||||
const fixtures = fixturesForProtocol('trojan');
|
||||
expect(fixtures.length, 'need at least one trojan full-inbound fixture').toBeGreaterThan(0);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
@@ -308,19 +309,107 @@ func TestSub_HostFinalMask_RawLink(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
ib := seedSubInbound(t, "s1", "fmh", 4455, 1,
|
||||
`{"network":"tcp","security":"tls","tlsSettings":{"serverName":"base.sni"},"finalmask":{"tcp":[{"type":"sudoku"}]}}`)
|
||||
finalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello","lengths":["5-10","10-15","15-20","20-25","25-30"],"delays":["10-20","5-20","5-25","15-25","10-30"],"maxSplit":"10-15"}}]}`
|
||||
seedHost(t, &model.Host{
|
||||
InboundId: ib.Id, SortOrder: 0, Remark: "FM", Address: "fm.cdn.com", Port: 8443, Security: "tls",
|
||||
FinalMask: `{"tcp":[{"type":"fragment"}]}`,
|
||||
FinalMask: finalMask,
|
||||
})
|
||||
|
||||
links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetSubs: %v", err)
|
||||
}
|
||||
joined := strings.Join(links, "\n")
|
||||
wantFm := "fm=" + url.QueryEscape(`{"tcp":[{"type":"sudoku"},{"type":"fragment"}]}`)
|
||||
if !strings.Contains(joined, wantFm) {
|
||||
t.Fatalf("raw link should merge the host Final Mask into fm.\n got: %s\nwant substring: %s", joined, wantFm)
|
||||
if len(links) == 0 {
|
||||
t.Fatal("GetSubs returned no links")
|
||||
}
|
||||
link, err := url.Parse(strings.Split(links[0], "\n")[0])
|
||||
if err != nil {
|
||||
t.Fatalf("parse raw link: %v", err)
|
||||
}
|
||||
var finalmask map[string]any
|
||||
if err := json.Unmarshal([]byte(link.Query().Get("fm")), &finalmask); err != nil {
|
||||
t.Fatalf("unmarshal fm query param: %v", err)
|
||||
}
|
||||
tcp, _ := finalmask["tcp"].([]any)
|
||||
if len(tcp) != 2 {
|
||||
t.Fatalf("tcp mask count = %d, want existing + host mask: %#v", len(tcp), finalmask)
|
||||
}
|
||||
fragment, _ := tcp[1].(map[string]any)
|
||||
settings, _ := fragment["settings"].(map[string]any)
|
||||
if settings["length"] != "25-30" || settings["delay"] != "10-30" {
|
||||
t.Fatalf("legacy ranges = (%v, %v), want last per-segment values", settings["length"], settings["delay"])
|
||||
}
|
||||
if len(settings["lengths"].([]any)) != 5 || len(settings["delays"].([]any)) != 5 {
|
||||
t.Fatalf("per-segment ranges changed: %#v", settings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSub_HostFinalMaskJSONAddsLegacyFragmentRanges(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
baseStream := `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"base.sni"},"finalmask":{"tcp":[{"type":"sudoku","settings":{"password":"p"}}]}}`
|
||||
ib := seedSubInbound(t, "s1", "fmj", 4456, 1, baseStream)
|
||||
finalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello","lengths":["5-10","10-15","15-20","20-25","25-30"],"delays":["10-20","5-20","5-25","15-25","10-30"],"maxSplit":"10-15"}}]}`
|
||||
host := seedHost(t, &model.Host{
|
||||
InboundId: ib.Id, SortOrder: 0, Remark: "FM", Address: "fm-json.cdn.com", Port: 8444, Security: "tls",
|
||||
FinalMask: finalMask,
|
||||
})
|
||||
|
||||
globalFinalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello","lengths":["31-40"],"delays":[]}}]}`
|
||||
out, _, err := NewSubJsonService("", "", globalFinalMask, "", NewSubService("")).GetJson("s1", "req.example.com", false)
|
||||
if err != nil {
|
||||
t.Fatalf("GetJson: %v", err)
|
||||
}
|
||||
var config map[string]any
|
||||
if err := json.Unmarshal([]byte(out), &config); err != nil {
|
||||
t.Fatalf("unmarshal JSON subscription: %v", err)
|
||||
}
|
||||
outbounds, _ := config["outbounds"].([]any)
|
||||
if len(outbounds) == 0 {
|
||||
t.Fatalf("JSON subscription has no outbounds: %s", out)
|
||||
}
|
||||
outbound, _ := outbounds[0].(map[string]any)
|
||||
stream, _ := outbound["streamSettings"].(map[string]any)
|
||||
finalmask, _ := stream["finalmask"].(map[string]any)
|
||||
tcp, _ := finalmask["tcp"].([]any)
|
||||
if len(tcp) != 3 {
|
||||
t.Fatalf("tcp mask count = %d, want base + global + host masks: %#v", len(tcp), finalmask)
|
||||
}
|
||||
globalFragment, _ := tcp[1].(map[string]any)
|
||||
globalSettings, _ := globalFragment["settings"].(map[string]any)
|
||||
if globalSettings["length"] != "31-40" {
|
||||
t.Fatalf("global legacy length = %v, want 31-40", globalSettings["length"])
|
||||
}
|
||||
if _, exists := globalSettings["delay"]; exists {
|
||||
t.Fatalf("empty global delays must not emit a fallback: %#v", globalSettings)
|
||||
}
|
||||
fragment, _ := tcp[2].(map[string]any)
|
||||
settings, _ := fragment["settings"].(map[string]any)
|
||||
if settings["length"] != "25-30" || settings["delay"] != "10-30" {
|
||||
t.Fatalf("legacy ranges = (%v, %v), want the final per-segment ranges", settings["length"], settings["delay"])
|
||||
}
|
||||
if got := settings["lengths"].([]any); len(got) != 5 || got[4] != "25-30" {
|
||||
t.Fatalf("per-segment lengths changed: %#v", settings["lengths"])
|
||||
}
|
||||
if got := settings["delays"].([]any); len(got) != 5 || got[4] != "10-30" {
|
||||
t.Fatalf("per-segment delays changed: %#v", settings["delays"])
|
||||
}
|
||||
if got := settings["maxSplit"]; got != "10-15" {
|
||||
t.Fatalf("maxSplit = %v, want 10-15", got)
|
||||
}
|
||||
|
||||
var storedHost model.Host
|
||||
if err := database.GetDB().First(&storedHost, host.Id).Error; err != nil {
|
||||
t.Fatalf("reload host: %v", err)
|
||||
}
|
||||
if storedHost.FinalMask != finalMask {
|
||||
t.Fatalf("stored host FinalMask changed: %s", storedHost.FinalMask)
|
||||
}
|
||||
var storedInbound model.Inbound
|
||||
if err := database.GetDB().First(&storedInbound, ib.Id).Error; err != nil {
|
||||
t.Fatalf("reload inbound: %v", err)
|
||||
}
|
||||
if storedInbound.StreamSettings != baseStream {
|
||||
t.Fatalf("stored inbound StreamSettings changed: %s", storedInbound.StreamSettings)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -636,6 +636,9 @@ func (s *SubJsonService) getConfig(subReq *SubService, inbound *model.Inbound, c
|
||||
applyExternalProxyTLSToStream(extPrxy, newStream, security)
|
||||
}
|
||||
applyHostStreamOverrides(extPrxy, newStream)
|
||||
if finalmask, ok := newStream["finalmask"].(map[string]any); ok {
|
||||
newStream["finalmask"] = withLegacyFragmentRanges(finalmask)
|
||||
}
|
||||
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
|
||||
hostMux := hostMuxOverride(extPrxy)
|
||||
|
||||
|
||||
+66
-1
@@ -2844,7 +2844,7 @@ func applyFinalMaskObj(finalmask map[string]any, obj map[string]any) {
|
||||
}
|
||||
|
||||
func marshalFinalMask(finalmask map[string]any) (string, bool) {
|
||||
normalized := normalizeFinalMask(finalmask)
|
||||
normalized := withLegacyFragmentRanges(normalizeFinalMask(finalmask))
|
||||
if !hasFinalMaskContent(normalized) {
|
||||
return "", false
|
||||
}
|
||||
@@ -2855,6 +2855,71 @@ func marshalFinalMask(finalmask map[string]any) (string, bool) {
|
||||
return string(b), true
|
||||
}
|
||||
|
||||
// withLegacyFragmentRanges copies the last lengths/delays entry into the singular fields older
|
||||
// cores require; newer xray-core prefers the arrays whenever they are non-empty.
|
||||
func withLegacyFragmentRanges(finalmask map[string]any) map[string]any {
|
||||
tcpMasks, ok := finalmask["tcp"].([]any)
|
||||
if !ok {
|
||||
return finalmask
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
var resultMasks []any
|
||||
for i, rawMask := range tcpMasks {
|
||||
mask, ok := rawMask.(map[string]any)
|
||||
if !ok || mask["type"] != "fragment" {
|
||||
continue
|
||||
}
|
||||
settings, ok := mask["settings"].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
legacySettings := maps.Clone(settings)
|
||||
changed := false
|
||||
if _, exists := settings["length"]; !exists {
|
||||
if value, ok := lastFragmentRange(settings["lengths"]); ok {
|
||||
legacySettings["length"] = value
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if _, exists := settings["delay"]; !exists {
|
||||
if value, ok := lastFragmentRange(settings["delays"]); ok {
|
||||
legacySettings["delay"] = value
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
result = maps.Clone(finalmask)
|
||||
resultMasks = slices.Clone(tcpMasks)
|
||||
result["tcp"] = resultMasks
|
||||
}
|
||||
legacyMask := maps.Clone(mask)
|
||||
legacyMask["settings"] = legacySettings
|
||||
resultMasks[i] = legacyMask
|
||||
}
|
||||
if result == nil {
|
||||
return finalmask
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func lastFragmentRange(value any) (string, bool) {
|
||||
ranges, _ := value.([]any)
|
||||
if len(ranges) == 0 {
|
||||
return "", false
|
||||
}
|
||||
rangeValue, ok := ranges[len(ranges)-1].(string)
|
||||
if !ok || strings.TrimSpace(rangeValue) == "" {
|
||||
return "", false
|
||||
}
|
||||
return rangeValue, true
|
||||
}
|
||||
|
||||
func normalizeFinalMask(finalmask map[string]any) map[string]any {
|
||||
tcpMasks := normalizedFinalMaskTCPMasks(finalmask)
|
||||
udpMasks := normalizedFinalMaskUDPMasks(finalmask)
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user