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 {
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)
@@ -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 (
@@ -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;
}
+41 -9
View File
@@ -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
}