From 567500c4af132f8bbe213c0098a5a9445b6465ed Mon Sep 17 00:00:00 2001 From: patterniha <71074308+patterniha@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:57:23 +0330 Subject: [PATCH] Fragment finalmask: Add `lengths` and `delays` (#6334) Usage: https://github.com/XTLS/Xray-core/pull/6334#issue-4685556394 Behavior: https://github.com/XTLS/Xray-core/pull/6334#issuecomment-4751547750 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- infra/conf/transport_internet.go | 37 +++++--- .../internet/finalmask/fragment/config.pb.go | 85 ++++++++++--------- .../internet/finalmask/fragment/config.proto | 8 +- transport/internet/finalmask/fragment/conn.go | 50 +++++++++-- 4 files changed, 116 insertions(+), 64 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 9b08f8c35..feb593b4b 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1409,10 +1409,12 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) { } type FragmentMask struct { - Packets string `json:"packets"` - Length Int32Range `json:"length"` - Delay Int32Range `json:"delay"` - MaxSplit Int32Range `json:"maxSplit"` + Packets string `json:"packets"` + Length Int32Range `json:"length"` + Delay Int32Range `json:"delay"` + Lengths []Int32Range `json:"lengths"` + Delays []Int32Range `json:"delays"` + MaxSplit Int32Range `json:"maxSplit"` } func (c *FragmentMask) Build() (proto.Message, error) { @@ -1437,14 +1439,29 @@ func (c *FragmentMask) Build() (proto.Message, error) { } } - config.LengthMin = int64(c.Length.From) - config.LengthMax = int64(c.Length.To) - if config.LengthMin == 0 { - return nil, errors.New("LengthMin can't be 0") + if len(c.Lengths) > 0 { + for _, r := range c.Lengths { + config.LengthsMin = append(config.LengthsMin, int64(r.From)) + config.LengthsMax = append(config.LengthsMax, int64(r.To)) + } + } else { + config.LengthsMin = append(config.LengthsMin, int64(c.Length.From)) + config.LengthsMax = append(config.LengthsMax, int64(c.Length.To)) } - config.DelayMin = int64(c.Delay.From) - config.DelayMax = int64(c.Delay.To) + if config.LengthsMin[len(config.LengthsMin)-1] == 0 { + return nil, errors.New("last lengths entry min can't be 0") + } + + if len(c.Delays) > 0 { + for _, r := range c.Delays { + config.DelaysMin = append(config.DelaysMin, int64(r.From)) + config.DelaysMax = append(config.DelaysMax, int64(r.To)) + } + } else { + config.DelaysMin = append(config.DelaysMin, int64(c.Delay.From)) + config.DelaysMax = append(config.DelaysMax, int64(c.Delay.To)) + } config.MaxSplitMin = int64(c.MaxSplit.From) config.MaxSplitMax = int64(c.MaxSplit.To) diff --git a/transport/internet/finalmask/fragment/config.pb.go b/transport/internet/finalmask/fragment/config.pb.go index c8660f5ec..3659c7ac3 100644 --- a/transport/internet/finalmask/fragment/config.pb.go +++ b/transport/internet/finalmask/fragment/config.pb.go @@ -25,12 +25,12 @@ type Config struct { state protoimpl.MessageState `protogen:"open.v1"` PacketsFrom int64 `protobuf:"varint,1,opt,name=packets_from,json=packetsFrom,proto3" json:"packets_from,omitempty"` PacketsTo int64 `protobuf:"varint,2,opt,name=packets_to,json=packetsTo,proto3" json:"packets_to,omitempty"` - LengthMin int64 `protobuf:"varint,3,opt,name=length_min,json=lengthMin,proto3" json:"length_min,omitempty"` - LengthMax int64 `protobuf:"varint,4,opt,name=length_max,json=lengthMax,proto3" json:"length_max,omitempty"` - DelayMin int64 `protobuf:"varint,5,opt,name=delay_min,json=delayMin,proto3" json:"delay_min,omitempty"` - DelayMax int64 `protobuf:"varint,6,opt,name=delay_max,json=delayMax,proto3" json:"delay_max,omitempty"` MaxSplitMin int64 `protobuf:"varint,7,opt,name=max_split_min,json=maxSplitMin,proto3" json:"max_split_min,omitempty"` MaxSplitMax int64 `protobuf:"varint,8,opt,name=max_split_max,json=maxSplitMax,proto3" json:"max_split_max,omitempty"` + LengthsMin []int64 `protobuf:"varint,9,rep,packed,name=lengths_min,json=lengthsMin,proto3" json:"lengths_min,omitempty"` + LengthsMax []int64 `protobuf:"varint,10,rep,packed,name=lengths_max,json=lengthsMax,proto3" json:"lengths_max,omitempty"` + DelaysMin []int64 `protobuf:"varint,11,rep,packed,name=delays_min,json=delaysMin,proto3" json:"delays_min,omitempty"` + DelaysMax []int64 `protobuf:"varint,12,rep,packed,name=delays_max,json=delaysMax,proto3" json:"delays_max,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -79,34 +79,6 @@ func (x *Config) GetPacketsTo() int64 { return 0 } -func (x *Config) GetLengthMin() int64 { - if x != nil { - return x.LengthMin - } - return 0 -} - -func (x *Config) GetLengthMax() int64 { - if x != nil { - return x.LengthMax - } - return 0 -} - -func (x *Config) GetDelayMin() int64 { - if x != nil { - return x.DelayMin - } - return 0 -} - -func (x *Config) GetDelayMax() int64 { - if x != nil { - return x.DelayMax - } - return 0 -} - func (x *Config) GetMaxSplitMin() int64 { if x != nil { return x.MaxSplitMin @@ -121,23 +93,54 @@ func (x *Config) GetMaxSplitMax() int64 { return 0 } +func (x *Config) GetLengthsMin() []int64 { + if x != nil { + return x.LengthsMin + } + return nil +} + +func (x *Config) GetLengthsMax() []int64 { + if x != nil { + return x.LengthsMax + } + return nil +} + +func (x *Config) GetDelaysMin() []int64 { + if x != nil { + return x.DelaysMin + } + return nil +} + +func (x *Config) GetDelaysMax() []int64 { + if x != nil { + return x.DelaysMax + } + return nil +} + var File_transport_internet_finalmask_fragment_config_proto protoreflect.FileDescriptor const file_transport_internet_finalmask_fragment_config_proto_rawDesc = "" + "\n" + - "2transport/internet/finalmask/fragment/config.proto\x12*xray.transport.internet.finalmask.fragment\"\x8a\x02\n" + + "2transport/internet/finalmask/fragment/config.proto\x12*xray.transport.internet.finalmask.fragment\"\x92\x02\n" + "\x06Config\x12!\n" + "\fpackets_from\x18\x01 \x01(\x03R\vpacketsFrom\x12\x1d\n" + "\n" + - "packets_to\x18\x02 \x01(\x03R\tpacketsTo\x12\x1d\n" + - "\n" + - "length_min\x18\x03 \x01(\x03R\tlengthMin\x12\x1d\n" + - "\n" + - "length_max\x18\x04 \x01(\x03R\tlengthMax\x12\x1b\n" + - "\tdelay_min\x18\x05 \x01(\x03R\bdelayMin\x12\x1b\n" + - "\tdelay_max\x18\x06 \x01(\x03R\bdelayMax\x12\"\n" + + "packets_to\x18\x02 \x01(\x03R\tpacketsTo\x12\"\n" + "\rmax_split_min\x18\a \x01(\x03R\vmaxSplitMin\x12\"\n" + - "\rmax_split_max\x18\b \x01(\x03R\vmaxSplitMaxB\xa0\x01\n" + + "\rmax_split_max\x18\b \x01(\x03R\vmaxSplitMax\x12\x1f\n" + + "\vlengths_min\x18\t \x03(\x03R\n" + + "lengthsMin\x12\x1f\n" + + "\vlengths_max\x18\n" + + " \x03(\x03R\n" + + "lengthsMax\x12\x1d\n" + + "\n" + + "delays_min\x18\v \x03(\x03R\tdelaysMin\x12\x1d\n" + + "\n" + + "delays_max\x18\f \x03(\x03R\tdelaysMaxB\xa0\x01\n" + ".com.xray.transport.internet.finalmask.fragmentP\x01Z?github.com/xtls/xray-core/transport/internet/finalmask/fragment\xaa\x02*Xray.Transport.Internet.Finalmask.Fragmentb\x06proto3" var ( diff --git a/transport/internet/finalmask/fragment/config.proto b/transport/internet/finalmask/fragment/config.proto index 62aaf1864..c83a962a1 100644 --- a/transport/internet/finalmask/fragment/config.proto +++ b/transport/internet/finalmask/fragment/config.proto @@ -9,10 +9,10 @@ option java_multiple_files = true; message Config { int64 packets_from = 1; int64 packets_to = 2; - int64 length_min = 3; - int64 length_max = 4; - int64 delay_min = 5; - int64 delay_max = 6; int64 max_split_min = 7; int64 max_split_max = 8; + repeated int64 lengths_min = 9; + repeated int64 lengths_max = 10; + repeated int64 delays_min = 11; + repeated int64 delays_max = 12; } \ No newline at end of file diff --git a/transport/internet/finalmask/fragment/conn.go b/transport/internet/finalmask/fragment/conn.go index 91822ace3..3c65a8bd5 100644 --- a/transport/internet/finalmask/fragment/conn.go +++ b/transport/internet/finalmask/fragment/conn.go @@ -43,6 +43,29 @@ func (c *fragmentConn) Splice() bool { return true } +// lengthForSegment returns the length range (min, max) for the given segment index (0-based). +// Clamps to the last entry when the index exceeds the list length. +func (c *fragmentConn) lengthForSegment(segIdx int) (int64, int64) { + if segIdx >= len(c.config.LengthsMin) { + segIdx = len(c.config.LengthsMin) - 1 + } + return c.config.LengthsMin[segIdx], c.config.LengthsMax[segIdx] +} + +// delayForSegment returns the delay range (min, max) for the given segment index (0-based). +// Clamps to the last entry when the index exceeds the list length. +func (c *fragmentConn) delayForSegment(segIdx int) (int64, int64) { + if segIdx >= len(c.config.DelaysMin) { + segIdx = len(c.config.DelaysMin) - 1 + } + return c.config.DelaysMin[segIdx], c.config.DelaysMax[segIdx] +} + +// mergeTlsHelloSegments returns true only when delays has exactly one zero entry. +func (c *fragmentConn) mergeTlsHelloSegments() bool { + return len(c.config.DelaysMax) == 1 && c.config.DelaysMax[0] == 0 +} + func (c *fragmentConn) Write(p []byte) (n int, err error) { c.count++ @@ -57,12 +80,13 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { data := p[5:recordLen] buff := make([]byte, 2048) var hello []byte + mergeHello := c.mergeTlsHelloSegments() maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax) var splitNum int64 for from := 0; ; { - to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax)) - splitNum++ - if to > len(data) || (maxSplit > 0 && splitNum >= maxSplit) { + lengthMin, lengthMax := c.lengthForSegment(int(splitNum)) + to := from + int(crypto.RandBetween(lengthMin, lengthMax)) + if to > len(data) || (maxSplit > 0 && splitNum+1 >= maxSplit) { to = len(data) } l := to - from @@ -74,15 +98,19 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { from = to buff[3] = byte(l >> 8) buff[4] = byte(l) - if c.config.DelayMax == 0 { + if mergeHello { hello = append(hello, buff[:5+l]...) } else { + delayMin, delayMax := c.delayForSegment(int(splitNum)) _, err := c.Conn.Write(buff[:5+l]) - time.Sleep(time.Duration(crypto.RandBetween(c.config.DelayMin, c.config.DelayMax)) * time.Millisecond) + if delayMax > 0 { + time.Sleep(time.Duration(crypto.RandBetween(delayMin, delayMax)) * time.Millisecond) + } if err != nil { return 0, err } } + splitNum++ if from == len(data) { if len(hello) > 0 { _, err := c.Conn.Write(hello) @@ -107,9 +135,9 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax) var splitNum int64 for from := 0; ; { - to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax)) - splitNum++ - if to > len(p) || (maxSplit > 0 && splitNum >= maxSplit) { + lengthMin, lengthMax := c.lengthForSegment(int(splitNum)) + to := from + int(crypto.RandBetween(lengthMin, lengthMax)) + if to > len(p) || (maxSplit > 0 && splitNum+1 >= maxSplit) { to = len(p) } n, err := c.Conn.Write(p[from:to]) @@ -117,7 +145,11 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { if err != nil { return from, err } - time.Sleep(time.Duration(crypto.RandBetween(c.config.DelayMin, c.config.DelayMax)) * time.Millisecond) + delayMin, delayMax := c.delayForSegment(int(splitNum)) + if delayMax > 0 { + time.Sleep(time.Duration(crypto.RandBetween(delayMin, delayMax)) * time.Millisecond) + } + splitNum++ if from >= len(p) { return from, nil }