From e51b3c362198519370962576f6735418a6c84b10 Mon Sep 17 00:00:00 2001 From: Cluvex <125141320+CluvexStudio@users.noreply.github.com> Date: Wed, 30 Sep 2026 06:41:12 +0330 Subject: [PATCH] Noise finalmask: `type` supports "exp" (#6862) https://github.com/XTLS/Xray-core/pull/6844#issuecomment-5859092964 https://github.com/XTLS/Xray-core/pull/6844#issuecomment-5859855674 https://github.com/XTLS/Xray-core/pull/6862#issuecomment-5897471145 --- infra/conf/transport_finalmask.go | 103 ++++++++- infra/conf/transport_finalmask_noise_test.go | 136 ++++++++++++ .../internet/finalmask/noise/config.pb.go | 196 ++++++++++++++++-- .../internet/finalmask/noise/config.proto | 17 ++ transport/internet/finalmask/noise/conn.go | 77 ++++++- .../internet/finalmask/noise/conn_test.go | 137 ++++++++++++ 6 files changed, 633 insertions(+), 33 deletions(-) create mode 100644 infra/conf/transport_finalmask_noise_test.go create mode 100644 transport/internet/finalmask/noise/conn_test.go diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index 784af98a0..69a69b5f2 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -310,14 +310,27 @@ type NoiseMask struct { } func (c *NoiseMask) Build() (proto.Message, error) { + noiseSlice := make([]*noise.Item, 0, len(c.Noise)) for _, item := range c.Noise { if len(item.Packet) > 0 && item.Rand.To > 0 { return nil, errors.New("len(item.Packet) > 0 && item.Rand.To > 0") } - } - - noiseSlice := make([]*noise.Item, 0, len(c.Noise)) - for _, item := range c.Noise { + if strings.ToLower(item.Type) == "exp" { + var exp string + if err := json.Unmarshal(item.Packet, &exp); err != nil { + return nil, errors.New(`"packet" of noise "type": "exp" must be a string`).Base(err) + } + segments, err := parseNoiseExp(exp) + if err != nil { + return nil, err + } + noiseSlice = append(noiseSlice, &noise.Item{ + Segments: segments, + DelayMin: int64(item.Delay.From), + DelayMax: int64(item.Delay.To), + }) + continue + } if item.RandRange == nil { item.RandRange = &Int32Range{From: 0, To: 255} } @@ -346,6 +359,88 @@ func (c *NoiseMask) Build() (proto.Message, error) { }, nil } +var noiseExpPattern = regexp.MustCompile(`<\s*([a-z]+)(?:\s+([^>]*?))?\s*>`) + +func parseNoiseExp(exp string) ([]*noise.Segment, error) { + var segments []*noise.Segment + matches := noiseExpPattern.FindAllStringSubmatchIndex(exp, -1) + last := 0 + for _, m := range matches { + if strings.TrimSpace(exp[last:m[0]]) != "" { + return nil, errors.New("invalid noise exp near ", exp[last:m[0]]) + } + last = m[1] + key := exp[m[2]:m[3]] + arg := "" + if m[4] >= 0 { + arg = exp[m[4]:m[5]] + } + segment, err := buildNoiseSegment(key, arg) + if err != nil { + return nil, err + } + segments = append(segments, segment) + } + if strings.TrimSpace(exp[last:]) != "" { + return nil, errors.New("invalid noise exp near ", exp[last:]) + } + if len(segments) == 0 { + return nil, errors.New("empty noise exp: ", exp) + } + return segments, nil +} + +func buildNoiseSegment(key, arg string) (*noise.Segment, error) { + sizeSegment := func(kind noise.Segment_Kind) (*noise.Segment, error) { + if arg == "" { + return nil, errors.New("<", key, "> in noise exp needs a size") + } + lo, hi, err := ParseRangeString(arg) + if err != nil { + return nil, err + } + if lo < 0 || hi < lo || hi > 65535 { + return nil, errors.New("invalid size in noise exp: ", arg) + } + return &noise.Segment{Kind: kind, MinSize: int64(lo), MaxSize: int64(hi)}, nil + } + switch key { + case "b": + hexStr := strings.TrimPrefix(strings.TrimPrefix(strings.Join(strings.Fields(arg), ""), "0x"), "0X") + if len(hexStr) == 0 { + return nil, errors.New("empty bytes in noise exp") + } + raw, err := hex.DecodeString(hexStr) + if err != nil { + return nil, errors.New("invalid hex in noise exp: ", arg).Base(err) + } + return &noise.Segment{Kind: noise.Segment_BYTES, Bytes: raw}, nil + case "r": + return sizeSegment(noise.Segment_RANDOM) + case "rc": + return sizeSegment(noise.Segment_RANDOM_ASCII) + case "rd": + return sizeSegment(noise.Segment_RANDOM_DIGIT) + case "t": + if arg != "" { + return nil, errors.New(" in noise exp takes no argument") + } + return &noise.Segment{Kind: noise.Segment_TIMESTAMP}, nil + case "c": + if arg != "" { + return nil, errors.New(" in noise exp takes no argument") + } + return &noise.Segment{Kind: noise.Segment_COUNTER}, nil + case "n": + if arg != "" { + return nil, errors.New(" in noise exp takes no argument") + } + return &noise.Segment{Kind: noise.Segment_NONCE}, nil + default: + return nil, errors.New("unknown <", key, "> in noise exp") + } +} + type UDPItem struct { Rand int32 `json:"rand"` RandRange *Int32Range `json:"randRange"` diff --git a/infra/conf/transport_finalmask_noise_test.go b/infra/conf/transport_finalmask_noise_test.go new file mode 100644 index 000000000..cca8a8666 --- /dev/null +++ b/infra/conf/transport_finalmask_noise_test.go @@ -0,0 +1,136 @@ +package conf + +import ( + "encoding/json" + "testing" + + "github.com/xtls/xray-core/transport/internet/finalmask/noise" +) + +func expPacket(exp string) json.RawMessage { + b, _ := json.Marshal(exp) + return b +} + +func buildNoiseExp(exp string) (*noise.Config, error) { + msg, err := (&NoiseMask{Noise: []NoiseItem{{Type: "exp", Packet: expPacket(exp)}}}).Build() + if err != nil { + return nil, err + } + return msg.(*noise.Config), nil +} + +func TestNoiseExp(t *testing.T) { + cfg, err := buildNoiseExp("") + if err != nil { + t.Fatal(err) + } + segments := cfg.Items[0].Segments + if len(segments) != 7 { + t.Fatalf("got %d segments, want 7", len(segments)) + } + want := []struct { + kind noise.Segment_Kind + bytes []byte + min, max int64 + }{ + {noise.Segment_BYTES, []byte{0x0d, 0x0a, 0x0d, 0x0a}, 0, 0}, + {noise.Segment_TIMESTAMP, nil, 0, 0}, + {noise.Segment_RANDOM, nil, 24, 24}, + {noise.Segment_RANDOM_ASCII, nil, 20, 40}, + {noise.Segment_RANDOM_DIGIT, nil, 8, 8}, + {noise.Segment_COUNTER, nil, 0, 0}, + {noise.Segment_NONCE, nil, 0, 0}, + } + for i, w := range want { + s := segments[i] + if s.Kind != w.kind || s.MinSize != w.min || s.MaxSize != w.max || string(s.Bytes) != string(w.bytes) { + t.Errorf("segment %d = %+v, want %+v", i, s, w) + } + } +} + +func TestNoiseExpStripsHexPrefix(t *testing.T) { + cfg, err := buildNoiseExp("") + if err != nil { + t.Fatal(err) + } + if got := cfg.Items[0].Segments[0].Bytes; string(got) != string([]byte{0x16, 0x03, 0x01, 0x00}) { + t.Errorf("got %x", got) + } +} + +func TestNoiseExpWhitespace(t *testing.T) { + if _, err := buildNoiseExp(" "); err != nil { + t.Errorf("surrounding whitespace should be allowed: %v", err) + } + cfg, err := buildNoiseExp("") + if err != nil { + t.Fatal(err) + } + if got := cfg.Items[0].Segments[0].Bytes; string(got) != "\r\n\r\n" { + t.Errorf("got %x", got) + } +} + +func TestNoiseExpRejects(t *testing.T) { + for _, exp := range []string{ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "garbage", + " tail", + "", + } { + if _, err := buildNoiseExp(exp); err == nil { + t.Errorf("expected an error for %q", exp) + } + } +} + +func TestNoiseExpConflicts(t *testing.T) { + if _, err := (&NoiseMask{Noise: []NoiseItem{{Type: "exp", Packet: expPacket(""), Rand: Int32Range{From: 10, To: 20}}}}).Build(); err == nil { + t.Error("exp with rand should be rejected") + } + for _, packet := range []string{``, `[1, 2]`, `5`} { + if _, err := (&NoiseMask{Noise: []NoiseItem{{Type: "exp", Packet: json.RawMessage(packet)}}}).Build(); err == nil { + t.Errorf("expected an error for packet %q", packet) + } + } +} + +func TestNoiseExpFromJSON(t *testing.T) { + var mask NoiseMask + if err := json.Unmarshal([]byte(`{"noise": [ + {"type": "exp", "packet": "", "delay": "1-3"}, + {"type": "EXP", "packet": ""}, + {"type": "str", "packet": ""}, + {"rand": "10-20"} + ]}`), &mask); err != nil { + t.Fatal(err) + } + msg, err := mask.Build() + if err != nil { + t.Fatal(err) + } + items := msg.(*noise.Config).Items + if len(items[0].Segments) != 2 || items[0].DelayMin != 1 || items[0].DelayMax != 3 { + t.Errorf("item 0 = %+v", items[0]) + } + if len(items[1].Segments) != 1 || items[1].Segments[0].Kind != noise.Segment_TIMESTAMP { + t.Errorf("item 1 = %+v", items[1]) + } + if len(items[2].Segments) != 0 || string(items[2].Packet) != "" { + t.Errorf("item 2 = %+v", items[2]) + } + if len(items[3].Segments) != 0 || items[3].RandMin != 10 || items[3].RandMax != 20 { + t.Errorf("item 3 = %+v", items[3]) + } +} diff --git a/transport/internet/finalmask/noise/config.pb.go b/transport/internet/finalmask/noise/config.pb.go index 71ba461a6..6f3a5ef16 100644 --- a/transport/internet/finalmask/noise/config.pb.go +++ b/transport/internet/finalmask/noise/config.pb.go @@ -21,6 +21,135 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type Segment_Kind int32 + +const ( + Segment_BYTES Segment_Kind = 0 + Segment_RANDOM Segment_Kind = 1 + Segment_RANDOM_ASCII Segment_Kind = 2 + Segment_RANDOM_DIGIT Segment_Kind = 3 + Segment_TIMESTAMP Segment_Kind = 4 + Segment_COUNTER Segment_Kind = 5 + Segment_NONCE Segment_Kind = 6 +) + +// Enum value maps for Segment_Kind. +var ( + Segment_Kind_name = map[int32]string{ + 0: "BYTES", + 1: "RANDOM", + 2: "RANDOM_ASCII", + 3: "RANDOM_DIGIT", + 4: "TIMESTAMP", + 5: "COUNTER", + 6: "NONCE", + } + Segment_Kind_value = map[string]int32{ + "BYTES": 0, + "RANDOM": 1, + "RANDOM_ASCII": 2, + "RANDOM_DIGIT": 3, + "TIMESTAMP": 4, + "COUNTER": 5, + "NONCE": 6, + } +) + +func (x Segment_Kind) Enum() *Segment_Kind { + p := new(Segment_Kind) + *p = x + return p +} + +func (x Segment_Kind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Segment_Kind) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_finalmask_noise_config_proto_enumTypes[0].Descriptor() +} + +func (Segment_Kind) Type() protoreflect.EnumType { + return &file_transport_internet_finalmask_noise_config_proto_enumTypes[0] +} + +func (x Segment_Kind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Segment_Kind.Descriptor instead. +func (Segment_Kind) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_finalmask_noise_config_proto_rawDescGZIP(), []int{0, 0} +} + +type Segment struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind Segment_Kind `protobuf:"varint,1,opt,name=kind,proto3,enum=xray.transport.internet.finalmask.noise.Segment_Kind" json:"kind,omitempty"` + Bytes []byte `protobuf:"bytes,2,opt,name=bytes,proto3" json:"bytes,omitempty"` + MinSize int64 `protobuf:"varint,3,opt,name=min_size,json=minSize,proto3" json:"min_size,omitempty"` + MaxSize int64 `protobuf:"varint,4,opt,name=max_size,json=maxSize,proto3" json:"max_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Segment) Reset() { + *x = Segment{} + mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Segment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Segment) ProtoMessage() {} + +func (x *Segment) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Segment.ProtoReflect.Descriptor instead. +func (*Segment) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_noise_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Segment) GetKind() Segment_Kind { + if x != nil { + return x.Kind + } + return Segment_BYTES +} + +func (x *Segment) GetBytes() []byte { + if x != nil { + return x.Bytes + } + return nil +} + +func (x *Segment) GetMinSize() int64 { + if x != nil { + return x.MinSize + } + return 0 +} + +func (x *Segment) GetMaxSize() int64 { + if x != nil { + return x.MaxSize + } + return 0 +} + type Item struct { state protoimpl.MessageState `protogen:"open.v1"` RandMin int64 `protobuf:"varint,1,opt,name=rand_min,json=randMin,proto3" json:"rand_min,omitempty"` @@ -30,13 +159,14 @@ type Item struct { Packet []byte `protobuf:"bytes,5,opt,name=packet,proto3" json:"packet,omitempty"` DelayMin int64 `protobuf:"varint,6,opt,name=delay_min,json=delayMin,proto3" json:"delay_min,omitempty"` DelayMax int64 `protobuf:"varint,7,opt,name=delay_max,json=delayMax,proto3" json:"delay_max,omitempty"` + Segments []*Segment `protobuf:"bytes,8,rep,name=segments,proto3" json:"segments,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Item) Reset() { *x = Item{} - mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[0] + mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -48,7 +178,7 @@ func (x *Item) String() string { func (*Item) ProtoMessage() {} func (x *Item) ProtoReflect() protoreflect.Message { - mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[0] + mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -61,7 +191,7 @@ func (x *Item) ProtoReflect() protoreflect.Message { // Deprecated: Use Item.ProtoReflect.Descriptor instead. func (*Item) Descriptor() ([]byte, []int) { - return file_transport_internet_finalmask_noise_config_proto_rawDescGZIP(), []int{0} + return file_transport_internet_finalmask_noise_config_proto_rawDescGZIP(), []int{1} } func (x *Item) GetRandMin() int64 { @@ -113,6 +243,13 @@ func (x *Item) GetDelayMax() int64 { return 0 } +func (x *Item) GetSegments() []*Segment { + if x != nil { + return x.Segments + } + return nil +} + type Config struct { state protoimpl.MessageState `protogen:"open.v1"` ResetMin int64 `protobuf:"varint,1,opt,name=reset_min,json=resetMin,proto3" json:"reset_min,omitempty"` @@ -124,7 +261,7 @@ type Config struct { func (x *Config) Reset() { *x = Config{} - mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[1] + mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -136,7 +273,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[1] + mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -149,7 +286,7 @@ func (x *Config) ProtoReflect() protoreflect.Message { // Deprecated: Use Config.ProtoReflect.Descriptor instead. func (*Config) Descriptor() ([]byte, []int) { - return file_transport_internet_finalmask_noise_config_proto_rawDescGZIP(), []int{1} + return file_transport_internet_finalmask_noise_config_proto_rawDescGZIP(), []int{2} } func (x *Config) GetResetMin() int64 { @@ -177,7 +314,21 @@ var File_transport_internet_finalmask_noise_config_proto protoreflect.FileDescri const file_transport_internet_finalmask_noise_config_proto_rawDesc = "" + "\n" + - "/transport/internet/finalmask/noise/config.proto\x12'xray.transport.internet.finalmask.noise\"\xda\x01\n" + + "/transport/internet/finalmask/noise/config.proto\x12'xray.transport.internet.finalmask.noise\"\x8a\x02\n" + + "\aSegment\x12I\n" + + "\x04kind\x18\x01 \x01(\x0e25.xray.transport.internet.finalmask.noise.Segment.KindR\x04kind\x12\x14\n" + + "\x05bytes\x18\x02 \x01(\fR\x05bytes\x12\x19\n" + + "\bmin_size\x18\x03 \x01(\x03R\aminSize\x12\x19\n" + + "\bmax_size\x18\x04 \x01(\x03R\amaxSize\"h\n" + + "\x04Kind\x12\t\n" + + "\x05BYTES\x10\x00\x12\n" + + "\n" + + "\x06RANDOM\x10\x01\x12\x10\n" + + "\fRANDOM_ASCII\x10\x02\x12\x10\n" + + "\fRANDOM_DIGIT\x10\x03\x12\r\n" + + "\tTIMESTAMP\x10\x04\x12\v\n" + + "\aCOUNTER\x10\x05\x12\t\n" + + "\x05NONCE\x10\x06\"\xa8\x02\n" + "\x04Item\x12\x19\n" + "\brand_min\x18\x01 \x01(\x03R\arandMin\x12\x19\n" + "\brand_max\x18\x02 \x01(\x03R\arandMax\x12$\n" + @@ -185,7 +336,8 @@ const file_transport_internet_finalmask_noise_config_proto_rawDesc = "" + "\x0erand_range_max\x18\x04 \x01(\x05R\frandRangeMax\x12\x16\n" + "\x06packet\x18\x05 \x01(\fR\x06packet\x12\x1b\n" + "\tdelay_min\x18\x06 \x01(\x03R\bdelayMin\x12\x1b\n" + - "\tdelay_max\x18\a \x01(\x03R\bdelayMax\"\x87\x01\n" + + "\tdelay_max\x18\a \x01(\x03R\bdelayMax\x12L\n" + + "\bsegments\x18\b \x03(\v20.xray.transport.internet.finalmask.noise.SegmentR\bsegments\"\x87\x01\n" + "\x06Config\x12\x1b\n" + "\treset_min\x18\x01 \x01(\x03R\bresetMin\x12\x1b\n" + "\treset_max\x18\x02 \x01(\x03R\bresetMax\x12C\n" + @@ -204,18 +356,23 @@ func file_transport_internet_finalmask_noise_config_proto_rawDescGZIP() []byte { return file_transport_internet_finalmask_noise_config_proto_rawDescData } -var file_transport_internet_finalmask_noise_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_finalmask_noise_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_transport_internet_finalmask_noise_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_transport_internet_finalmask_noise_config_proto_goTypes = []any{ - (*Item)(nil), // 0: xray.transport.internet.finalmask.noise.Item - (*Config)(nil), // 1: xray.transport.internet.finalmask.noise.Config + (Segment_Kind)(0), // 0: xray.transport.internet.finalmask.noise.Segment.Kind + (*Segment)(nil), // 1: xray.transport.internet.finalmask.noise.Segment + (*Item)(nil), // 2: xray.transport.internet.finalmask.noise.Item + (*Config)(nil), // 3: xray.transport.internet.finalmask.noise.Config } var file_transport_internet_finalmask_noise_config_proto_depIdxs = []int32{ - 0, // 0: xray.transport.internet.finalmask.noise.Config.items:type_name -> xray.transport.internet.finalmask.noise.Item - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 0, // 0: xray.transport.internet.finalmask.noise.Segment.kind:type_name -> xray.transport.internet.finalmask.noise.Segment.Kind + 1, // 1: xray.transport.internet.finalmask.noise.Item.segments:type_name -> xray.transport.internet.finalmask.noise.Segment + 2, // 2: xray.transport.internet.finalmask.noise.Config.items:type_name -> xray.transport.internet.finalmask.noise.Item + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_transport_internet_finalmask_noise_config_proto_init() } @@ -228,13 +385,14 @@ func file_transport_internet_finalmask_noise_config_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_noise_config_proto_rawDesc), len(file_transport_internet_finalmask_noise_config_proto_rawDesc)), - NumEnums: 0, - NumMessages: 2, + NumEnums: 1, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, GoTypes: file_transport_internet_finalmask_noise_config_proto_goTypes, DependencyIndexes: file_transport_internet_finalmask_noise_config_proto_depIdxs, + EnumInfos: file_transport_internet_finalmask_noise_config_proto_enumTypes, MessageInfos: file_transport_internet_finalmask_noise_config_proto_msgTypes, }.Build() File_transport_internet_finalmask_noise_config_proto = out.File diff --git a/transport/internet/finalmask/noise/config.proto b/transport/internet/finalmask/noise/config.proto index d874b973f..319d8dee7 100644 --- a/transport/internet/finalmask/noise/config.proto +++ b/transport/internet/finalmask/noise/config.proto @@ -6,6 +6,22 @@ option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/nois option java_package = "com.xray.transport.internet.finalmask.noise"; option java_multiple_files = true; +message Segment { + enum Kind { + BYTES = 0; + RANDOM = 1; + RANDOM_ASCII = 2; + RANDOM_DIGIT = 3; + TIMESTAMP = 4; + COUNTER = 5; + NONCE = 6; + } + Kind kind = 1; + bytes bytes = 2; + int64 min_size = 3; + int64 max_size = 4; +} + message Item { int64 rand_min = 1; int64 rand_max = 2; @@ -14,6 +30,7 @@ message Item { bytes packet = 5; int64 delay_min = 6; int64 delay_max = 7; + repeated Segment segments = 8; } message Config { diff --git a/transport/internet/finalmask/noise/conn.go b/transport/internet/finalmask/noise/conn.go index 8bb115bb4..951071c0e 100644 --- a/transport/internet/finalmask/noise/conn.go +++ b/transport/internet/finalmask/noise/conn.go @@ -1,18 +1,25 @@ package noise import ( + "crypto/rand" + "encoding/binary" "net" "sync" + "sync/atomic" "time" + "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/crypto" ) +const asciiLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + type noiseConn struct { net.PacketConn - config *Config - m map[string]time.Time - mu sync.Mutex + config *Config + m map[string]time.Time + mu sync.Mutex + counter atomic.Uint32 } func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { @@ -27,6 +34,62 @@ func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { return NewConnClient(c, raw) } +func (c *noiseConn) buildPacket(item *Item) []byte { + if len(item.Segments) == 0 { + if item.RandMax > 0 { + buf := make([]byte, crypto.RandBetween(item.RandMin, item.RandMax)) + crypto.RandBytesBetween(buf, byte(item.RandRangeMin), byte(item.RandRangeMax)) + return buf + } + return item.Packet + } + var out []byte + for _, seg := range item.Segments { + out = append(out, c.buildSegment(seg)...) + } + return out +} + +func (c *noiseConn) buildSegment(seg *Segment) []byte { + switch seg.Kind { + case Segment_BYTES: + return seg.Bytes + case Segment_TIMESTAMP: + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, uint32(time.Now().Unix())) + return b + case Segment_COUNTER: + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, c.counter.Add(1)) + return b + case Segment_NONCE: + b := make([]byte, 8) + common.Must2(rand.Read(b)) + return b + default: + size := crypto.RandBetween(seg.MinSize, seg.MaxSize+1) + if size <= 0 { + return nil + } + buf := make([]byte, size) + switch seg.Kind { + case Segment_RANDOM_ASCII: + common.Must2(rand.Read(buf)) + for i := range buf { + buf[i] = asciiLetters[int(buf[i])%len(asciiLetters)] + } + case Segment_RANDOM_DIGIT: + common.Must2(rand.Read(buf)) + for i := range buf { + buf[i] = '0' + buf[i]%10 + } + default: + common.Must2(rand.Read(buf)) + } + return buf + } +} + func (c *noiseConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { c.mu.Lock() defer c.mu.Unlock() @@ -35,13 +98,7 @@ func (c *noiseConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { if t.IsZero() || (c.config.ResetMax > 0 && time.Now().After(t)) { for _, item := range c.config.Items { - if item.RandMax > 0 { - buf := make([]byte, crypto.RandBetween(item.RandMin, item.RandMax)) - crypto.RandBytesBetween(buf, byte(item.RandRangeMin), byte(item.RandRangeMax)) - c.PacketConn.WriteTo(buf, addr) - } else { - c.PacketConn.WriteTo(item.Packet, addr) - } + c.PacketConn.WriteTo(c.buildPacket(item), addr) time.Sleep(time.Duration(crypto.RandBetween(item.DelayMin, item.DelayMax)) * time.Millisecond) } } diff --git a/transport/internet/finalmask/noise/conn_test.go b/transport/internet/finalmask/noise/conn_test.go new file mode 100644 index 000000000..5dade71b0 --- /dev/null +++ b/transport/internet/finalmask/noise/conn_test.go @@ -0,0 +1,137 @@ +package noise + +import ( + "bytes" + "encoding/binary" + "net" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type fakePacketConn struct { + mu sync.Mutex + written [][]byte +} + +func (c *fakePacketConn) WriteTo(p []byte, _ net.Addr) (int, error) { + c.mu.Lock() + defer c.mu.Unlock() + c.written = append(c.written, bytes.Clone(p)) + return len(p), nil +} + +func (c *fakePacketConn) packets() [][]byte { + c.mu.Lock() + defer c.mu.Unlock() + return c.written +} + +func (c *fakePacketConn) ReadFrom(_ []byte) (int, net.Addr, error) { return 0, nil, nil } +func (c *fakePacketConn) Close() error { return nil } +func (c *fakePacketConn) LocalAddr() net.Addr { return &net.UDPAddr{} } +func (c *fakePacketConn) SetDeadline(time.Time) error { return nil } +func (c *fakePacketConn) SetReadDeadline(time.Time) error { return nil } +func (c *fakePacketConn) SetWriteDeadline(time.Time) error { return nil } + +func newConn() *noiseConn { + return &noiseConn{PacketConn: &fakePacketConn{}, config: &Config{}, m: make(map[string]time.Time)} +} + +func TestBuildSegmentBytes(t *testing.T) { + c := newConn() + got := c.buildSegment(&Segment{Kind: Segment_BYTES, Bytes: []byte{0x0d, 0x0a, 0x0d, 0x0a}}) + require.Equal(t, []byte{0x0d, 0x0a, 0x0d, 0x0a}, got) +} + +func TestBuildSegmentTimestamp(t *testing.T) { + c := newConn() + before := time.Now().Unix() + got := c.buildSegment(&Segment{Kind: Segment_TIMESTAMP}) + require.Len(t, got, 4) + ts := int64(binary.BigEndian.Uint32(got)) + require.GreaterOrEqual(t, ts, before) + require.LessOrEqual(t, ts, time.Now().Unix()) +} + +func TestBuildSegmentCounter(t *testing.T) { + c := newConn() + first := binary.BigEndian.Uint32(c.buildSegment(&Segment{Kind: Segment_COUNTER})) + second := binary.BigEndian.Uint32(c.buildSegment(&Segment{Kind: Segment_COUNTER})) + require.Equal(t, uint32(1), first) + require.Equal(t, uint32(2), second) +} + +func TestBuildSegmentNonce(t *testing.T) { + c := newConn() + a := c.buildSegment(&Segment{Kind: Segment_NONCE}) + b := c.buildSegment(&Segment{Kind: Segment_NONCE}) + require.Len(t, a, 8) + require.Len(t, b, 8) + require.NotEqual(t, a, b) +} + +func TestBuildSegmentRandomSizes(t *testing.T) { + c := newConn() + for range 200 { + require.Len(t, c.buildSegment(&Segment{Kind: Segment_RANDOM, MinSize: 24, MaxSize: 24}), 24) + + n := len(c.buildSegment(&Segment{Kind: Segment_RANDOM, MinSize: 20, MaxSize: 32})) + require.GreaterOrEqual(t, n, 20) + require.LessOrEqual(t, n, 32) + + for _, b := range c.buildSegment(&Segment{Kind: Segment_RANDOM_ASCII, MinSize: 40, MaxSize: 40}) { + require.True(t, (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z'), "not a letter: %q", b) + } + for _, b := range c.buildSegment(&Segment{Kind: Segment_RANDOM_DIGIT, MinSize: 40, MaxSize: 40}) { + require.True(t, b >= '0' && b <= '9', "not a digit: %q", b) + } + } +} + +func TestBuildPacketComposite(t *testing.T) { + c := newConn() + item := &Item{Segments: []*Segment{ + {Kind: Segment_BYTES, Bytes: []byte{0x0d, 0x0a, 0x0d, 0x0a}}, + {Kind: Segment_TIMESTAMP}, + {Kind: Segment_RANDOM, MinSize: 24, MaxSize: 24}, + }} + got := c.buildPacket(item) + require.Len(t, got, 4+4+24) + require.Equal(t, []byte{0x0d, 0x0a, 0x0d, 0x0a}, got[:4]) +} + +func TestBuildPacketLegacy(t *testing.T) { + c := newConn() + require.Equal(t, []byte{1, 2, 3}, c.buildPacket(&Item{Packet: []byte{1, 2, 3}})) + require.Len(t, c.buildPacket(&Item{RandMin: 16, RandMax: 17}), 16) +} + +func TestWriteToSendsNoiseThenPayload(t *testing.T) { + raw := &fakePacketConn{} + c := &noiseConn{ + PacketConn: raw, + m: make(map[string]time.Time), + config: &Config{Items: []*Item{ + {Segments: []*Segment{{Kind: Segment_BYTES, Bytes: []byte{0x0d, 0x0a, 0x0d, 0x0a}}, {Kind: Segment_RANDOM, MinSize: 8, MaxSize: 8}}}, + {RandMin: 40, RandMax: 41}, + }}, + } + addr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 51820} + payload := []byte("real-handshake") + _, err := c.WriteTo(payload, addr) + require.NoError(t, err) + + sent := raw.packets() + require.Len(t, sent, 3) + require.Len(t, sent[0], 12) + require.Equal(t, []byte{0x0d, 0x0a, 0x0d, 0x0a}, sent[0][:4]) + require.Len(t, sent[1], 40) + require.Equal(t, payload, sent[2]) + + _, err = c.WriteTo(payload, addr) + require.NoError(t, err) + require.Len(t, raw.packets(), 4) +}