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>
This commit is contained in:
patterniha
2026-06-22 19:57:23 +03:30
committed by RPRX
parent 5aefcb41fb
commit 567500c4af
4 changed files with 116 additions and 64 deletions
+27 -10
View File
@@ -1409,10 +1409,12 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
} }
type FragmentMask struct { type FragmentMask struct {
Packets string `json:"packets"` Packets string `json:"packets"`
Length Int32Range `json:"length"` Length Int32Range `json:"length"`
Delay Int32Range `json:"delay"` Delay Int32Range `json:"delay"`
MaxSplit Int32Range `json:"maxSplit"` Lengths []Int32Range `json:"lengths"`
Delays []Int32Range `json:"delays"`
MaxSplit Int32Range `json:"maxSplit"`
} }
func (c *FragmentMask) Build() (proto.Message, error) { func (c *FragmentMask) Build() (proto.Message, error) {
@@ -1437,14 +1439,29 @@ func (c *FragmentMask) Build() (proto.Message, error) {
} }
} }
config.LengthMin = int64(c.Length.From) if len(c.Lengths) > 0 {
config.LengthMax = int64(c.Length.To) for _, r := range c.Lengths {
if config.LengthMin == 0 { config.LengthsMin = append(config.LengthsMin, int64(r.From))
return nil, errors.New("LengthMin can't be 0") 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) if config.LengthsMin[len(config.LengthsMin)-1] == 0 {
config.DelayMax = int64(c.Delay.To) 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.MaxSplitMin = int64(c.MaxSplit.From)
config.MaxSplitMax = int64(c.MaxSplit.To) config.MaxSplitMax = int64(c.MaxSplit.To)
@@ -25,12 +25,12 @@ type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
PacketsFrom int64 `protobuf:"varint,1,opt,name=packets_from,json=packetsFrom,proto3" json:"packets_from,omitempty"` 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"` 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"` 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -79,34 +79,6 @@ func (x *Config) GetPacketsTo() int64 {
return 0 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 { func (x *Config) GetMaxSplitMin() int64 {
if x != nil { if x != nil {
return x.MaxSplitMin return x.MaxSplitMin
@@ -121,23 +93,54 @@ func (x *Config) GetMaxSplitMax() int64 {
return 0 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 var File_transport_internet_finalmask_fragment_config_proto protoreflect.FileDescriptor
const file_transport_internet_finalmask_fragment_config_proto_rawDesc = "" + const file_transport_internet_finalmask_fragment_config_proto_rawDesc = "" +
"\n" + "\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" + "\x06Config\x12!\n" +
"\fpackets_from\x18\x01 \x01(\x03R\vpacketsFrom\x12\x1d\n" + "\fpackets_from\x18\x01 \x01(\x03R\vpacketsFrom\x12\x1d\n" +
"\n" + "\n" +
"packets_to\x18\x02 \x01(\x03R\tpacketsTo\x12\x1d\n" + "packets_to\x18\x02 \x01(\x03R\tpacketsTo\x12\"\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" +
"\rmax_split_min\x18\a \x01(\x03R\vmaxSplitMin\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" ".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 ( var (
@@ -9,10 +9,10 @@ option java_multiple_files = true;
message Config { message Config {
int64 packets_from = 1; int64 packets_from = 1;
int64 packets_to = 2; 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_min = 7;
int64 max_split_max = 8; 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;
} }
+41 -9
View File
@@ -43,6 +43,29 @@ func (c *fragmentConn) Splice() bool {
return true 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) { func (c *fragmentConn) Write(p []byte) (n int, err error) {
c.count++ c.count++
@@ -57,12 +80,13 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) {
data := p[5:recordLen] data := p[5:recordLen]
buff := make([]byte, 2048) buff := make([]byte, 2048)
var hello []byte var hello []byte
mergeHello := c.mergeTlsHelloSegments()
maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax) maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax)
var splitNum int64 var splitNum int64
for from := 0; ; { for from := 0; ; {
to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax)) lengthMin, lengthMax := c.lengthForSegment(int(splitNum))
splitNum++ to := from + int(crypto.RandBetween(lengthMin, lengthMax))
if to > len(data) || (maxSplit > 0 && splitNum >= maxSplit) { if to > len(data) || (maxSplit > 0 && splitNum+1 >= maxSplit) {
to = len(data) to = len(data)
} }
l := to - from l := to - from
@@ -74,15 +98,19 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) {
from = to from = to
buff[3] = byte(l >> 8) buff[3] = byte(l >> 8)
buff[4] = byte(l) buff[4] = byte(l)
if c.config.DelayMax == 0 { if mergeHello {
hello = append(hello, buff[:5+l]...) hello = append(hello, buff[:5+l]...)
} else { } else {
delayMin, delayMax := c.delayForSegment(int(splitNum))
_, err := c.Conn.Write(buff[:5+l]) _, 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 { if err != nil {
return 0, err return 0, err
} }
} }
splitNum++
if from == len(data) { if from == len(data) {
if len(hello) > 0 { if len(hello) > 0 {
_, err := c.Conn.Write(hello) _, 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) maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax)
var splitNum int64 var splitNum int64
for from := 0; ; { for from := 0; ; {
to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax)) lengthMin, lengthMax := c.lengthForSegment(int(splitNum))
splitNum++ to := from + int(crypto.RandBetween(lengthMin, lengthMax))
if to > len(p) || (maxSplit > 0 && splitNum >= maxSplit) { if to > len(p) || (maxSplit > 0 && splitNum+1 >= maxSplit) {
to = len(p) to = len(p)
} }
n, err := c.Conn.Write(p[from:to]) 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 { if err != nil {
return from, err 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) { if from >= len(p) {
return from, nil return from, nil
} }