diff --git a/common/geodata/consts.go b/common/geodata/consts.go new file mode 100644 index 000000000..3d03d14b7 --- /dev/null +++ b/common/geodata/consts.go @@ -0,0 +1,49 @@ +package geodata + +import ( + "sync" + + "github.com/xtls/xray-core/common" +) + +var privateIPMatcher = sync.OnceValue(func() IPMatcher { + return common.Must2(IPReg.BuildIPMatcher(common.Must2(ParseIPRules([]string{ + "0.0.0.0/8", + "10.0.0.0/8", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "172.16.0.0/12", + "192.0.0.0/24", + "192.0.2.0/24", + "192.88.99.0/24", + "192.168.0.0/16", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/3", + "::/127", + "fc00::/7", + "fe80::/10", + "ff00::/8", + })))) +}) + +func GetPrivateIPMatcher() IPMatcher { return privateIPMatcher() } + +var privateDomainMatcher = sync.OnceValue(func() DomainMatcher { + return common.Must2(DomainReg.BuildDomainMatcher(common.Must2(ParseDomainRules([]string{ + "lan", + "localdomain", + "example", + "invalid", + "localhost", + "test", + "local", + "home.arpa", + "internal", + "regexp:^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$", // Dotless domains + }, Domain_Domain)))) +}) + +func GetPrivateDomainMatcher() DomainMatcher { return privateDomainMatcher() } diff --git a/common/protocol/headers.pb.go b/common/protocol/headers.pb.go index 21ebe895c..70bec77a1 100644 --- a/common/protocol/headers.pb.go +++ b/common/protocol/headers.pb.go @@ -28,8 +28,6 @@ const ( SecurityType_AUTO SecurityType = 2 SecurityType_AES128_GCM SecurityType = 3 SecurityType_CHACHA20_POLY1305 SecurityType = 4 - SecurityType_NONE SecurityType = 5 // [DEPRECATED 2023-06] - SecurityType_ZERO SecurityType = 6 ) // Enum value maps for SecurityType. @@ -39,16 +37,12 @@ var ( 2: "AUTO", 3: "AES128_GCM", 4: "CHACHA20_POLY1305", - 5: "NONE", - 6: "ZERO", } SecurityType_value = map[string]int32{ "UNKNOWN": 0, "AUTO": 2, "AES128_GCM": 3, "CHACHA20_POLY1305": 4, - "NONE": 5, - "ZERO": 6, } ) @@ -129,15 +123,13 @@ const file_common_protocol_headers_proto_rawDesc = "" + "\n" + "\x1dcommon/protocol/headers.proto\x12\x14xray.common.protocol\"H\n" + "\x0eSecurityConfig\x126\n" + - "\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*`\n" + + "\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*L\n" + "\fSecurityType\x12\v\n" + "\aUNKNOWN\x10\x00\x12\b\n" + "\x04AUTO\x10\x02\x12\x0e\n" + "\n" + "AES128_GCM\x10\x03\x12\x15\n" + - "\x11CHACHA20_POLY1305\x10\x04\x12\b\n" + - "\x04NONE\x10\x05\x12\b\n" + - "\x04ZERO\x10\x06B^\n" + + "\x11CHACHA20_POLY1305\x10\x04B^\n" + "\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3" var ( diff --git a/common/protocol/headers.proto b/common/protocol/headers.proto index 1ae3537f5..3ac1eef4e 100644 --- a/common/protocol/headers.proto +++ b/common/protocol/headers.proto @@ -11,8 +11,6 @@ enum SecurityType { AUTO = 2; AES128_GCM = 3; CHACHA20_POLY1305 = 4; - NONE = 5; // [DEPRECATED 2023-06] - ZERO = 6; } message SecurityConfig { diff --git a/infra/conf/grpc.go b/infra/conf/grpc.go deleted file mode 100644 index 429186b0a..000000000 --- a/infra/conf/grpc.go +++ /dev/null @@ -1,41 +0,0 @@ -package conf - -import ( - "github.com/xtls/xray-core/transport/internet/grpc" - "google.golang.org/protobuf/proto" -) - -type GRPCConfig struct { - Authority string `json:"authority"` - ServiceName string `json:"serviceName"` - MultiMode bool `json:"multiMode"` - IdleTimeout int32 `json:"idle_timeout"` - HealthCheckTimeout int32 `json:"health_check_timeout"` - PermitWithoutStream bool `json:"permit_without_stream"` - InitialWindowsSize int32 `json:"initial_windows_size"` - UserAgent string `json:"user_agent"` -} - -func (g *GRPCConfig) Build() (proto.Message, error) { - if g.IdleTimeout <= 0 { - g.IdleTimeout = 0 - } - if g.HealthCheckTimeout <= 0 { - g.HealthCheckTimeout = 0 - } - if g.InitialWindowsSize < 0 { - // default window size of gRPC-go - g.InitialWindowsSize = 0 - } - - return &grpc.Config{ - Authority: g.Authority, - ServiceName: g.ServiceName, - MultiMode: g.MultiMode, - IdleTimeout: g.IdleTimeout, - HealthCheckTimeout: g.HealthCheckTimeout, - PermitWithoutStream: g.PermitWithoutStream, - InitialWindowsSize: g.InitialWindowsSize, - UserAgent: g.UserAgent, - }, nil -} diff --git a/infra/conf/shadowsocks.go b/infra/conf/shadowsocks.go index 2cfc09539..18451ab5c 100644 --- a/infra/conf/shadowsocks.go +++ b/infra/conf/shadowsocks.go @@ -24,8 +24,6 @@ func cipherFromString(c string) shadowsocks.CipherType { return shadowsocks.CipherType_CHACHA20_POLY1305 case "xchacha20-poly1305", "aead_xchacha20_poly1305", "xchacha20-ietf-poly1305": return shadowsocks.CipherType_XCHACHA20_POLY1305 - case "none", "plain": - return shadowsocks.CipherType_NONE default: return shadowsocks.CipherType_UNKNOWN } diff --git a/infra/conf/transport_authenticators.go b/infra/conf/transport_authenticators.go deleted file mode 100644 index 9afdd4f04..000000000 --- a/infra/conf/transport_authenticators.go +++ /dev/null @@ -1,208 +0,0 @@ -package conf - -import ( - "sort" - - "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/utils" - "github.com/xtls/xray-core/transport/internet/headers/http" - "github.com/xtls/xray-core/transport/internet/headers/noop" - "google.golang.org/protobuf/proto" -) - -type NoOpConnectionAuthenticator struct{} - -func (NoOpConnectionAuthenticator) Build() (proto.Message, error) { - return new(noop.ConnectionConfig), nil -} - -type AuthenticatorRequest struct { - Version string `json:"version"` - Method string `json:"method"` - Path StringList `json:"path"` - Headers map[string]*StringList `json:"headers"` -} - -func sortMapKeys(m map[string]*StringList) []string { - var keys []string - for key := range m { - keys = append(keys, key) - } - sort.Strings(keys) - return keys -} - -func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) { - config := &http.RequestConfig{ - Uri: []string{"/"}, - Header: []*http.Header{ - { - Name: "Host", - Value: []string{"www.baidu.com", "www.bing.com"}, - }, - { - Name: "User-Agent", - Value: []string{utils.ChromeUA}, - }, - { - Name: "Sec-CH-UA", - Value: []string{utils.ChromeUACH}, - }, - { - Name: "Sec-CH-UA-Mobile", - Value: []string{"?0"}, - }, - { - Name: "Sec-CH-UA-Platform", - Value: []string{"Windows"}, - }, - { - Name: "Sec-Fetch-Mode", - Value: []string{"no-cors", "cors", "same-origin"}, - }, - { - Name: "Sec-Fetch-Dest", - Value: []string{"empty"}, - }, - { - Name: "Sec-Fetch-Site", - Value: []string{"none"}, - }, - { - Name: "Sec-Fetch-User", - Value: []string{"?1"}, - }, - { - Name: "Accept-Encoding", - Value: []string{"gzip, deflate"}, - }, - { - Name: "Connection", - Value: []string{"keep-alive"}, - }, - { - Name: "Pragma", - Value: []string{"no-cache"}, - }, - }, - } - - if len(v.Version) > 0 { - config.Version = &http.Version{Value: v.Version} - } - - if len(v.Method) > 0 { - config.Method = &http.Method{Value: v.Method} - } - - if len(v.Path) > 0 { - config.Uri = append([]string(nil), v.Path...) - } - - if len(v.Headers) > 0 { - config.Header = make([]*http.Header, 0, len(v.Headers)) - headerNames := sortMapKeys(v.Headers) - for _, key := range headerNames { - value := v.Headers[key] - if value == nil { - return nil, errors.New("empty HTTP header value: " + key).AtError() - } - config.Header = append(config.Header, &http.Header{ - Name: key, - Value: append([]string(nil), (*value)...), - }) - } - } - - return config, nil -} - -type AuthenticatorResponse struct { - Version string `json:"version"` - Status string `json:"status"` - Reason string `json:"reason"` - Headers map[string]*StringList `json:"headers"` -} - -func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) { - config := &http.ResponseConfig{ - Header: []*http.Header{ - { - Name: "Content-Type", - Value: []string{"application/octet-stream", "video/mpeg"}, - }, - { - Name: "Transfer-Encoding", - Value: []string{"chunked"}, - }, - { - Name: "Connection", - Value: []string{"keep-alive"}, - }, - { - Name: "Pragma", - Value: []string{"no-cache"}, - }, - { - Name: "Cache-Control", - Value: []string{"private", "no-cache"}, - }, - }, - } - - if len(v.Version) > 0 { - config.Version = &http.Version{Value: v.Version} - } - - if len(v.Status) > 0 || len(v.Reason) > 0 { - config.Status = &http.Status{ - Code: "200", - Reason: "OK", - } - if len(v.Status) > 0 { - config.Status.Code = v.Status - } - if len(v.Reason) > 0 { - config.Status.Reason = v.Reason - } - } - - if len(v.Headers) > 0 { - config.Header = make([]*http.Header, 0, len(v.Headers)) - headerNames := sortMapKeys(v.Headers) - for _, key := range headerNames { - value := v.Headers[key] - if value == nil { - return nil, errors.New("empty HTTP header value: " + key).AtError() - } - config.Header = append(config.Header, &http.Header{ - Name: key, - Value: append([]string(nil), (*value)...), - }) - } - } - - return config, nil -} - -type Authenticator struct { - Request AuthenticatorRequest `json:"request"` - Response AuthenticatorResponse `json:"response"` -} - -func (v *Authenticator) Build() (proto.Message, error) { - config := new(http.Config) - requestConfig, err := v.Request.Build() - if err != nil { - return nil, err - } - config.Request = requestConfig - - responseConfig, err := v.Response.Build() - if err != nil { - return nil, err - } - config.Response = responseConfig - - return config, nil -} diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go new file mode 100644 index 000000000..bdcf8294e --- /dev/null +++ b/infra/conf/transport_finalmask.go @@ -0,0 +1,871 @@ +package conf + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "net/netip" + "net/url" + "regexp" + "strings" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet/finalmask/fragment" + "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" + "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" + "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header" + "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" + "github.com/xtls/xray-core/transport/internet/finalmask/noise" + "github.com/xtls/xray-core/transport/internet/finalmask/realm" + "github.com/xtls/xray-core/transport/internet/finalmask/salamander" + "github.com/xtls/xray-core/transport/internet/finalmask/sudoku" + "github.com/xtls/xray-core/transport/internet/finalmask/xdns" + "github.com/xtls/xray-core/transport/internet/finalmask/xicmp" + "github.com/xtls/xray-core/transport/internet/tls" + "google.golang.org/protobuf/proto" +) + +func PraseByteSlice(data json.RawMessage, typ string) ([]byte, error) { + switch strings.ToLower(typ) { + case "", "array": + if len(data) == 0 { + return data, nil + } + var packet []byte + if err := json.Unmarshal(data, &packet); err != nil { + return nil, err + } + return packet, nil + case "str": + var str string + if err := json.Unmarshal(data, &str); err != nil { + return nil, err + } + return []byte(str), nil + case "hex": + var str string + if err := json.Unmarshal(data, &str); err != nil { + return nil, err + } + return hex.DecodeString(str) + case "base64": + var str string + if err := json.Unmarshal(data, &str); err != nil { + return nil, err + } + return base64.StdEncoding.DecodeString(str) + default: + return nil, errors.New("unknown type") + } +} + +var ( + customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + + tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "header-custom": func() interface{} { return new(HeaderCustomTCP) }, + "fragment": func() interface{} { return new(FragmentMask) }, + "sudoku": func() interface{} { return new(Sudoku) }, + }, "type", "settings") + + udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "header-custom": func() interface{} { return new(HeaderCustomUDP) }, + "mkcp-legacy": func() interface{} { return new(MkcpLegacy) }, + "noise": func() interface{} { return new(NoiseMask) }, + "salamander": func() interface{} { return new(Salamander) }, + "sudoku": func() interface{} { return new(Sudoku) }, + "xdns": func() interface{} { return new(Xdns) }, + "xicmp": func() interface{} { return new(Xicmp) }, + "realm": func() interface{} { return new(Realm) }, + }, "type", "settings") +) + +type TCPItem struct { + Delay Int32Range `json:"delay"` + Rand int32 `json:"rand"` + RandRange *Int32Range `json:"randRange"` + Capture string `json:"capture"` + Type string `json:"type"` + Reuse string `json:"reuse"` + Transform *CustomTransform `json:"transform"` + Packet json.RawMessage `json:"packet"` +} + +type HeaderCustomTCP struct { + Clients [][]TCPItem `json:"clients"` + Servers [][]TCPItem `json:"servers"` + Errors [][]TCPItem `json:"errors"` +} + +func (c *HeaderCustomTCP) Build() (proto.Message, error) { + for _, value := range c.Clients { + for _, item := range value { + if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil { + return nil, err + } + } + } + for _, value := range c.Servers { + for _, item := range value { + if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil { + return nil, err + } + } + } + for _, value := range c.Errors { + for _, item := range value { + if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil { + return nil, err + } + } + } + + errInvalidRange := errors.New("invalid randRange") + + clients := make([]*custom.TCPSequence, len(c.Clients)) + for i, value := range c.Clients { + clients[i] = &custom.TCPSequence{} + for _, item := range value { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errInvalidRange + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + transform, err := buildCustomTransform(item.Transform) + if err != nil { + return nil, err + } + clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{ + DelayMin: int64(item.Delay.From), + DelayMax: int64(item.Delay.To), + Rand: item.Rand, + RandMin: item.RandRange.From, + RandMax: item.RandRange.To, + Packet: item.Packet, + Save: item.Capture, + Var: item.Reuse, + Expr: transform, + }) + } + } + + servers := make([]*custom.TCPSequence, len(c.Servers)) + for i, value := range c.Servers { + servers[i] = &custom.TCPSequence{} + for _, item := range value { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errInvalidRange + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + transform, err := buildCustomTransform(item.Transform) + if err != nil { + return nil, err + } + servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{ + DelayMin: int64(item.Delay.From), + DelayMax: int64(item.Delay.To), + Rand: item.Rand, + RandMin: item.RandRange.From, + RandMax: item.RandRange.To, + Packet: item.Packet, + Save: item.Capture, + Var: item.Reuse, + Expr: transform, + }) + } + } + + errors := make([]*custom.TCPSequence, len(c.Errors)) + for i, value := range c.Errors { + errors[i] = &custom.TCPSequence{} + for _, item := range value { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errInvalidRange + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + transform, err := buildCustomTransform(item.Transform) + if err != nil { + return nil, err + } + errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{ + DelayMin: int64(item.Delay.From), + DelayMax: int64(item.Delay.To), + Rand: item.Rand, + RandMin: item.RandRange.From, + RandMax: item.RandRange.To, + Packet: item.Packet, + Save: item.Capture, + Var: item.Reuse, + Expr: transform, + }) + } + } + + return &custom.TCPConfig{ + Clients: clients, + Servers: servers, + Errors: errors, + }, nil +} + +type FragmentMask struct { + 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) { + config := &fragment.Config{} + + switch strings.ToLower(c.Packets) { + case "tlshello": + config.PacketsFrom = 0 + config.PacketsTo = 1 + case "": + config.PacketsFrom = 0 + config.PacketsTo = 0 + default: + from, to, err := ParseRangeString(c.Packets) + if err != nil { + return nil, errors.New("Invalid PacketsFrom").Base(err) + } + config.PacketsFrom = int64(from) + config.PacketsTo = int64(to) + if config.PacketsFrom == 0 { + return nil, errors.New("PacketsFrom 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)) + } + + 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) + + return config, nil +} + +type NoiseItem struct { + Rand Int32Range `json:"rand"` + RandRange *Int32Range `json:"randRange"` + Type string `json:"type"` + Packet json.RawMessage `json:"packet"` + Delay Int32Range `json:"delay"` +} + +type NoiseMask struct { + Reset Int32Range `json:"reset"` + Noise []NoiseItem `json:"noise"` +} + +func (c *NoiseMask) Build() (proto.Message, error) { + 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 item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errors.New("invalid randRange") + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + noiseSlice = append(noiseSlice, &noise.Item{ + RandMin: int64(item.Rand.From), + RandMax: int64(item.Rand.To), + RandRangeMin: item.RandRange.From, + RandRangeMax: item.RandRange.To, + Packet: item.Packet, + DelayMin: int64(item.Delay.From), + DelayMax: int64(item.Delay.To), + }) + } + + return &noise.Config{ + ResetMin: int64(c.Reset.From), + ResetMax: int64(c.Reset.To), + Items: noiseSlice, + }, nil +} + +type UDPItem struct { + Rand int32 `json:"rand"` + RandRange *Int32Range `json:"randRange"` + Capture string `json:"capture"` + Type string `json:"type"` + Reuse string `json:"reuse"` + Transform *CustomTransform `json:"transform"` + Packet json.RawMessage `json:"packet"` +} + +type CustomTransform struct { + Op string `json:"op"` + Args []CustomTransformArg `json:"args"` +} + +type CustomTransformArg struct { + Type string `json:"type"` + Bytes json.RawMessage `json:"bytes"` + U64 *uint64 `json:"u64"` + Reuse string `json:"reuse"` + Metadata string `json:"metadata"` + Transform *CustomTransform `json:"transform"` +} + +func validateCustomVarName(name string) error { + if name == "" { + return nil + } + if !customVarNamePattern.MatchString(name) { + return errors.New("invalid variable name") + } + return nil +} + +func validateCustomItemSpec(capture string, packet json.RawMessage, rand int32, reuse string, transform *CustomTransform) error { + if err := validateCustomVarName(capture); err != nil { + return err + } + if err := validateCustomVarName(reuse); err != nil { + return err + } + + kindCount := 0 + if len(packet) > 0 { + kindCount++ + } + if rand > 0 { + kindCount++ + } + if reuse != "" { + kindCount++ + } + if transform != nil { + kindCount++ + } + if kindCount > 1 { + return errors.New("exactly one item kind must be set") + } + if kindCount == 0 && capture != "" { + return errors.New("exactly one item kind must be set") + } + + return nil +} + +func buildCustomTransform(transform *CustomTransform) (*custom.Expr, error) { + if transform == nil { + return nil, nil + } + if transform.Op == "" { + return nil, errors.New("transform op is required") + } + if len(transform.Args) == 0 { + return nil, errors.New("transform args are required") + } + + args := make([]*custom.ExprArg, 0, len(transform.Args)) + for _, arg := range transform.Args { + parsedArg, err := buildCustomTransformArg(arg) + if err != nil { + return nil, err + } + args = append(args, parsedArg) + } + + return &custom.Expr{ + Op: transform.Op, + Args: args, + }, nil +} + +func buildCustomTransformArg(arg CustomTransformArg) (*custom.ExprArg, error) { + kindCount := 0 + if len(arg.Bytes) > 0 { + kindCount++ + } + if arg.U64 != nil { + kindCount++ + } + if arg.Reuse != "" { + kindCount++ + } + if arg.Metadata != "" { + kindCount++ + } + if arg.Transform != nil { + kindCount++ + } + if kindCount != 1 { + return nil, errors.New("transform arg must set exactly one value") + } + + if len(arg.Bytes) > 0 { + value, err := PraseByteSlice(arg.Bytes, arg.Type) + if err != nil { + return nil, err + } + return &custom.ExprArg{ + Value: &custom.ExprArg_Bytes{ + Bytes: value, + }, + }, nil + } + if arg.U64 != nil { + return &custom.ExprArg{ + Value: &custom.ExprArg_U64{ + U64: *arg.U64, + }, + }, nil + } + if arg.Reuse != "" { + if err := validateCustomVarName(arg.Reuse); err != nil { + return nil, err + } + return &custom.ExprArg{ + Value: &custom.ExprArg_Var{ + Var: arg.Reuse, + }, + }, nil + } + if arg.Metadata != "" { + return &custom.ExprArg{ + Value: &custom.ExprArg_Metadata{ + Metadata: arg.Metadata, + }, + }, nil + } + + parsedExpr, err := buildCustomTransform(arg.Transform) + if err != nil { + return nil, err + } + return &custom.ExprArg{ + Value: &custom.ExprArg_Expr{ + Expr: parsedExpr, + }, + }, nil +} + +type HeaderCustomUDP struct { + Mode string `json:"mode"` + Client []UDPItem `json:"client"` + Server []UDPItem `json:"server"` +} + +func (c *HeaderCustomUDP) Build() (proto.Message, error) { + switch c.Mode { + case "", "prefix", "standalone": + default: + return nil, errors.New("unknown udp mode") + } + + for _, item := range c.Client { + if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil { + return nil, err + } + } + for _, item := range c.Server { + if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil { + return nil, err + } + } + + client := make([]*custom.UDPItem, 0, len(c.Client)) + for _, item := range c.Client { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errors.New("invalid randRange") + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + transform, err := buildCustomTransform(item.Transform) + if err != nil { + return nil, err + } + client = append(client, &custom.UDPItem{ + Rand: item.Rand, + RandMin: item.RandRange.From, + RandMax: item.RandRange.To, + Packet: item.Packet, + Save: item.Capture, + Var: item.Reuse, + Expr: transform, + }) + } + + server := make([]*custom.UDPItem, 0, len(c.Server)) + for _, item := range c.Server { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errors.New("invalid randRange") + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + transform, err := buildCustomTransform(item.Transform) + if err != nil { + return nil, err + } + server = append(server, &custom.UDPItem{ + Rand: item.Rand, + RandMin: item.RandRange.From, + RandMax: item.RandRange.To, + Packet: item.Packet, + Save: item.Capture, + Var: item.Reuse, + Expr: transform, + }) + } + + if c.Mode == "standalone" { + return &custom.UDPStandaloneConfig{ + Client: client, + Server: server, + }, nil + } else { + return &custom.UDPConfig{ + Client: client, + Server: server, + }, nil + } +} + +type MkcpLegacy struct { + Header string `json:"header"` + Value string `json:"value"` +} + +func (c *MkcpLegacy) Build() (proto.Message, error) { + if len(c.Header) == 0 { + if len(c.Value) == 0 { + return &original.Config{}, nil + } else { + return &aes128gcm.Config{Password: c.Value}, nil + } + } + switch strings.ToLower(c.Header) { + case "dns": + domain := c.Value + if len(domain) == 0 { + domain = "www.baidu.com" + } + return &header.Config{ID: 0, Domain: domain}, nil + case "dtls": + return &header.Config{ID: 1}, nil + case "srtp": + return &header.Config{ID: 2}, nil + case "utp": + return &header.Config{ID: 3}, nil + case "wechat": + return &header.Config{ID: 4}, nil + case "wireguard": + return &header.Config{ID: 5}, nil + default: + return nil, errors.New("invalid header ", c.Header) + } +} + +type Salamander struct { + Password string `json:"password"` + PacketSize Int32Range `json:"packetSize"` +} + +func (c *Salamander) Build() (proto.Message, error) { + if c.PacketSize.To > 0 { + if c.PacketSize.From <= 0 || c.PacketSize.To > 2048 { + return nil, errors.New("gecko: invalid min/max packet size") + } + return &salamander.GeckoConfig{ + Password: c.Password, + MinPacketSize: c.PacketSize.From, + MaxPacketSize: c.PacketSize.To, + }, nil + } + return &salamander.Config{ + Password: c.Password, + }, nil +} + +type Sudoku struct { + Password string `json:"password"` + ASCII string `json:"ascii"` + + CustomTable string `json:"customTable"` + LegacyCustomTable string `json:"custom_table"` + CustomTables []string `json:"customTables"` + LegacyCustomSets []string `json:"custom_tables"` + + PaddingMin uint32 `json:"paddingMin"` + LegacyPaddingMin uint32 `json:"padding_min"` + PaddingMax uint32 `json:"paddingMax"` + LegacyPaddingMax uint32 `json:"padding_max"` +} + +func (c *Sudoku) Build() (proto.Message, error) { + customTable := c.CustomTable + if customTable == "" { + customTable = c.LegacyCustomTable + } + customTables := c.CustomTables + if len(customTables) == 0 { + customTables = c.LegacyCustomSets + } + + paddingMin := c.PaddingMin + if paddingMin == 0 { + paddingMin = c.LegacyPaddingMin + } + paddingMax := c.PaddingMax + if paddingMax == 0 { + paddingMax = c.LegacyPaddingMax + } + + return &sudoku.Config{ + Password: c.Password, + Ascii: c.ASCII, + CustomTable: customTable, + CustomTables: customTables, + PaddingMin: paddingMin, + PaddingMax: paddingMax, + }, nil +} + +type Xdns struct { + Domain json.RawMessage `json:"domain"` + + Domains []string `json:"domains"` + Resolvers []string `json:"resolvers"` +} + +func (c *Xdns) Build() (proto.Message, error) { + if c.Domain != nil { + return nil, errors.PrintRemovedFeatureError("domain", "domains(server) & resolvers(client)") + } + + if len(c.Domains) == 0 && len(c.Resolvers) == 0 { + return nil, errors.New("empty domains & empty resolvers") + } + + for _, r := range c.Resolvers { + if !strings.Contains(r, "+udp://") { + return nil, errors.New("invalid resolver ", r) + } + } + + return &xdns.Config{ + Domains: c.Domains, + Resolvers: c.Resolvers, + }, nil +} + +type Xicmp struct { + DGRAM bool `json:"dgram"` + IPs []string `json:"ips"` +} + +func (c *Xicmp) Build() (proto.Message, error) { + for _, ip := range c.IPs { + if _, err := netip.ParseAddr(ip); err != nil { + return nil, err + } + } + + config := &xicmp.Config{ + DGRAM: c.DGRAM, + IPs: c.IPs, + } + + return config, nil +} + +type Realm struct { + Url string `json:"url"` + StunServers []string `json:"stunServers"` + TlsConfig *TLSConfig `json:"tlsConfig"` +} + +func (c *Realm) Build() (proto.Message, error) { + var scheme, host, port, token, id string + var stunServers []string + var tlsConfig *tls.Config + + u, err := url.Parse(c.Url) + if err != nil { + return nil, err + } + + switch u.Scheme { + case "realm": + scheme = "https" + case "realm+http": + scheme = "http" + default: + return nil, errors.New("invalid scheme", u.Scheme) + } + + host = u.Hostname() + if host == "" { + return nil, errors.New("invalid host", host) + } + + port = u.Port() + if port == "" { + port = "443" + if scheme == "http" { + port = "80" + } + } + + token, err = url.PathUnescape(u.User.String()) + if err != nil { + return nil, err + } + if token == "" { + return nil, errors.New("invalid token", token) + } + + id, err = url.PathUnescape(strings.TrimPrefix(u.EscapedPath(), "/")) + if err != nil { + return nil, err + } + if id == "" { + return nil, errors.New("invalid id", id) + } + + if len(c.StunServers) == 0 { + return nil, errors.New("empty stunServers") + } + + for _, s := range c.StunServers { + _, _, err = net.SplitHostPort(s) + if err != nil { + return nil, err + } + } + + stunServers = c.StunServers + + if c.TlsConfig != nil { + tc, err := c.TlsConfig.Build() + if err != nil { + return nil, err + } + tlsConfig = tc.(*tls.Config) + } + + return &realm.Config{ + Scheme: scheme, + Host: host, + Port: port, + Token: token, + ID: id, + StunServers: stunServers, + TlsConfig: tlsConfig, + }, nil +} + +type Mask struct { + Type string `json:"type"` + Settings *json.RawMessage `json:"settings"` +} + +func (c *Mask) Build(tcp bool) (proto.Message, error) { + loader := udpmaskLoader + if tcp { + loader = tcpmaskLoader + } + + settings := []byte("{}") + if c.Settings != nil { + settings = ([]byte)(*c.Settings) + } + rawConfig, err := loader.LoadWithID(settings, c.Type) + if err != nil { + return nil, err + } + ts, err := rawConfig.(Buildable).Build() + if err != nil { + return nil, err + } + return ts, nil +} + +type QuicParamsConfig struct { + Congestion string `json:"congestion"` + Debug bool `json:"debug"` + BbrProfile string `json:"bbrProfile"` + BrutalUp Bandwidth `json:"brutalUp"` + BrutalDown Bandwidth `json:"brutalDown"` + UdpHop UdpHop `json:"udpHop"` + InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"` + MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"` + InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"` + MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"` + MaxIdleTimeout int64 `json:"maxIdleTimeout"` + KeepAlivePeriod int64 `json:"keepAlivePeriod"` + DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"` + MaxIncomingStreams int64 `json:"maxIncomingStreams"` +} + +type FinalMask struct { + Tcp []Mask `json:"tcp"` + Udp []Mask `json:"udp"` + QuicParams *QuicParamsConfig `json:"quicParams"` +} diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 94744835f..574b6f7f8 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1,1004 +1,15 @@ package conf import ( - "context" - "encoding/base64" - "encoding/hex" - "encoding/json" - "math" - "math/big" - "net/netip" - "net/url" "os" - "regexp" - "runtime" - "strconv" "strings" - "syscall" - "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/transport/internet" - "github.com/xtls/xray-core/transport/internet/finalmask/fragment" - "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" - "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" - "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header" - "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" - "github.com/xtls/xray-core/transport/internet/finalmask/noise" - "github.com/xtls/xray-core/transport/internet/finalmask/realm" - "github.com/xtls/xray-core/transport/internet/finalmask/salamander" - finalsudoku "github.com/xtls/xray-core/transport/internet/finalmask/sudoku" - "github.com/xtls/xray-core/transport/internet/finalmask/xdns" - "github.com/xtls/xray-core/transport/internet/finalmask/xicmp" - "github.com/xtls/xray-core/transport/internet/httpupgrade" - "github.com/xtls/xray-core/transport/internet/hysteria" "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" - "github.com/xtls/xray-core/transport/internet/kcp" - "github.com/xtls/xray-core/transport/internet/reality" - "github.com/xtls/xray-core/transport/internet/splithttp" - "github.com/xtls/xray-core/transport/internet/tcp" - "github.com/xtls/xray-core/transport/internet/tls" - "github.com/xtls/xray-core/transport/internet/websocket" - "google.golang.org/protobuf/proto" ) -var tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{ - "none": func() interface{} { return new(NoOpConnectionAuthenticator) }, - "http": func() interface{} { return new(Authenticator) }, -}, "type", "") - -type KCPConfig struct { - Mtu *uint32 `json:"mtu"` - Tti *uint32 `json:"tti"` - UpCap *uint32 `json:"uplinkCapacity"` - DownCap *uint32 `json:"downlinkCapacity"` - CwndMultiplier *uint32 `json:"cwndMultiplier"` - MaxSendingWindow *uint32 `json:"maxSendingWindow"` - - HeaderConfig json.RawMessage `json:"header"` - Seed *string `json:"seed"` -} - -// Build implements Buildable. -func (c *KCPConfig) Build() (proto.Message, error) { - if c.HeaderConfig != nil || c.Seed != nil { - return nil, errors.PrintRemovedFeatureError("mkcp header & seed", "finalmask/udp header-* & mkcp-original & mkcp-aes128gcm") - } - - config := common.Must2(internet.CreateTransportConfig(kcp.ProtocolName)).(*kcp.Config) - - if c.Mtu != nil { - config.Mtu = *c.Mtu - } - if c.Tti != nil { - config.Tti = *c.Tti - } - if c.UpCap != nil { - config.UplinkCapacity = *c.UpCap - } - if c.DownCap != nil { - config.DownlinkCapacity = *c.DownCap - } - if c.CwndMultiplier != nil { - config.CwndMultiplier = *c.CwndMultiplier - } - if c.MaxSendingWindow != nil { - config.MaxSendingWindow = *c.MaxSendingWindow - } - - if config.Mtu < 21 { - return nil, errors.New("Mtu must be at least 21").AtError() - } - if config.Tti < 10 || config.Tti > 1000 { - return nil, errors.New("invalid mKCP TTI: ", c.Tti).AtError() - } - if config.CwndMultiplier < 1 { - return nil, errors.New("CwndMultiplier must be at least 1").AtError() - } - if config.GetSendingBufferSize() == 0 { - return nil, errors.New("MaxSendingWindow must be >= Mtu").AtError() - } - - return config, nil -} - -type TCPConfig struct { - HeaderConfig json.RawMessage `json:"header"` - AcceptProxyProtocol bool `json:"acceptProxyProtocol"` -} - -// Build implements Buildable. -func (c *TCPConfig) Build() (proto.Message, error) { - config := new(tcp.Config) - if len(c.HeaderConfig) > 0 { - headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig) - if err != nil { - return nil, errors.New("invalid TCP header config").Base(err).AtError() - } - ts, err := headerConfig.(Buildable).Build() - if err != nil { - return nil, errors.New("invalid TCP header config").Base(err).AtError() - } - config.HeaderSettings = serial.ToTypedMessage(ts) - } - if c.AcceptProxyProtocol { - config.AcceptProxyProtocol = c.AcceptProxyProtocol - } - return config, nil -} - -type WebSocketConfig struct { - Host string `json:"host"` - Path string `json:"path"` - Headers map[string]string `json:"headers"` - AcceptProxyProtocol bool `json:"acceptProxyProtocol"` - HeartbeatPeriod uint32 `json:"heartbeatPeriod"` -} - -// Build implements Buildable. -func (c *WebSocketConfig) Build() (proto.Message, error) { - path := c.Path - var ed uint32 - if u, err := url.Parse(path); err == nil { - if q := u.Query(); q.Get("ed") != "" { - Ed, _ := strconv.Atoi(q.Get("ed")) - ed = uint32(Ed) - q.Del("ed") - u.RawQuery = q.Encode() - path = u.String() - } - } - // Priority (client): host > serverName > address - for k, v := range c.Headers { - if strings.ToLower(k) == "host" { - errors.PrintDeprecatedFeatureWarning(`"host" in "headers"`, `independent "host"`) - if c.Host == "" { - c.Host = v - } - delete(c.Headers, k) - } - } - config := &websocket.Config{ - Path: path, - Host: c.Host, - Header: c.Headers, - AcceptProxyProtocol: c.AcceptProxyProtocol, - Ed: ed, - HeartbeatPeriod: c.HeartbeatPeriod, - } - return config, nil -} - -type HttpUpgradeConfig struct { - Host string `json:"host"` - Path string `json:"path"` - Headers map[string]string `json:"headers"` - AcceptProxyProtocol bool `json:"acceptProxyProtocol"` -} - -// Build implements Buildable. -func (c *HttpUpgradeConfig) Build() (proto.Message, error) { - path := c.Path - var ed uint32 - if u, err := url.Parse(path); err == nil { - if q := u.Query(); q.Get("ed") != "" { - Ed, _ := strconv.Atoi(q.Get("ed")) - ed = uint32(Ed) - q.Del("ed") - u.RawQuery = q.Encode() - path = u.String() - } - } - // Priority (client): host > serverName > address - for k := range c.Headers { - if strings.ToLower(k) == "host" { - return nil, errors.New(`"headers" can't contain "host"`) - } - } - config := &httpupgrade.Config{ - Path: path, - Host: c.Host, - Header: c.Headers, - AcceptProxyProtocol: c.AcceptProxyProtocol, - Ed: ed, - } - return config, nil -} - -type SplitHTTPConfig struct { - Host string `json:"host"` - Path string `json:"path"` - Mode string `json:"mode"` - Headers map[string]string `json:"headers"` - XPaddingBytes Int32Range `json:"xPaddingBytes"` - XPaddingObfsMode bool `json:"xPaddingObfsMode"` - XPaddingKey string `json:"xPaddingKey"` - XPaddingHeader string `json:"xPaddingHeader"` - XPaddingPlacement string `json:"xPaddingPlacement"` - XPaddingMethod string `json:"xPaddingMethod"` - UplinkHTTPMethod string `json:"uplinkHTTPMethod"` - SessionIDPlacement string `json:"sessionIDPlacement"` - SessionIDKey string `json:"sessionIDKey"` - SessionIDTable string `json:"sessionIDTable"` - SessionIDLength Int32Range `json:"sessionIDLength"` - SeqPlacement string `json:"seqPlacement"` - SeqKey string `json:"seqKey"` - UplinkDataPlacement string `json:"uplinkDataPlacement"` - UplinkDataKey string `json:"uplinkDataKey"` - UplinkChunkSize Int32Range `json:"uplinkChunkSize"` - NoGRPCHeader bool `json:"noGRPCHeader"` - NoSSEHeader bool `json:"noSSEHeader"` - ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"` - ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"` - ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"` - ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"` - ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"` - Xmux XmuxConfig `json:"xmux"` - DownloadSettings *StreamConfig `json:"downloadSettings"` - Extra json.RawMessage `json:"extra"` -} - -type XmuxConfig struct { - MaxConcurrency Int32Range `json:"maxConcurrency"` - MaxConnections Int32Range `json:"maxConnections"` - CMaxReuseTimes Int32Range `json:"cMaxReuseTimes"` - HMaxRequestTimes Int32Range `json:"hMaxRequestTimes"` - HMaxReusableSecs Int32Range `json:"hMaxReusableSecs"` - HKeepAlivePeriod int64 `json:"hKeepAlivePeriod"` -} - -func newRangeConfig(input Int32Range) *splithttp.RangeConfig { - return &splithttp.RangeConfig{ - From: input.From, - To: input.To, - } -} - -// Build implements Buildable. -func (c *SplitHTTPConfig) Build() (proto.Message, error) { - if c.Extra != nil { - var extra SplitHTTPConfig - if err := json.Unmarshal(c.Extra, &extra); err != nil { - return nil, errors.New(`Failed to unmarshal "extra".`).Base(err) - } - extra.Host = c.Host - extra.Path = c.Path - extra.Mode = c.Mode - c = &extra - } - - switch c.Mode { - case "": - c.Mode = "auto" - case "auto", "packet-up", "stream-up", "stream-one": - default: - return nil, errors.New("unsupported mode: " + c.Mode) - } - - // Priority (client): host > serverName > address - for k := range c.Headers { - if strings.ToLower(k) == "host" { - return nil, errors.New(`"headers" can't contain "host"`) - } - } - - if c.XPaddingBytes != (Int32Range{}) && (c.XPaddingBytes.From <= 0 || c.XPaddingBytes.To <= 0) { - return nil, errors.New("xPaddingBytes cannot be disabled") - } - - if c.XPaddingKey == "" { - c.XPaddingKey = "x_padding" - } - - if c.XPaddingHeader == "" { - c.XPaddingHeader = "X-Padding" - } - - switch c.XPaddingPlacement { - case "": - c.XPaddingPlacement = "queryInHeader" - case "cookie", "header", "query", "queryInHeader": - default: - return nil, errors.New("unsupported padding placement: " + c.XPaddingPlacement) - } - - switch c.XPaddingMethod { - case "": - c.XPaddingMethod = "repeat-x" - case "repeat-x", "tokenish": - default: - return nil, errors.New("unsupported padding method: " + c.XPaddingMethod) - } - - switch c.UplinkDataPlacement { - case "": - c.UplinkDataPlacement = splithttp.PlacementAuto - case splithttp.PlacementAuto, splithttp.PlacementBody: - case splithttp.PlacementCookie, splithttp.PlacementHeader: - if c.Mode != "packet-up" { - return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode") - } - default: - return nil, errors.New("unsupported uplink data placement: " + c.UplinkDataPlacement) - } - - if c.UplinkHTTPMethod == "" { - c.UplinkHTTPMethod = "POST" - } - c.UplinkHTTPMethod = strings.ToUpper(c.UplinkHTTPMethod) - - if c.UplinkHTTPMethod == "GET" && c.Mode != "packet-up" { - return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode") - } - - switch c.SessionIDPlacement { - case "": - c.SessionIDPlacement = "path" - case "path", "cookie", "header", "query": - default: - return nil, errors.New("unsupported session placement: " + c.SessionIDPlacement) - } - - switch c.SeqPlacement { - case "": - c.SeqPlacement = "path" - case "path", "cookie", "header", "query": - default: - return nil, errors.New("unsupported seq placement: " + c.SeqPlacement) - } - - if c.SessionIDPlacement != "path" && c.SessionIDKey == "" { - switch c.SessionIDPlacement { - case "cookie", "query": - c.SessionIDKey = "x_session" - case "header": - c.SessionIDKey = "X-Session" - } - } - - if c.SessionIDTable != "" { - if predefined, ok := splithttp.PredefinedTable[c.SessionIDTable]; ok { - c.SessionIDTable = predefined - } - room := roomSize(len(c.SessionIDTable), c.SessionIDLength.From, c.SessionIDLength.To) - // 2.1B possiblities should be enough - if room.Cmp(big.NewInt(2<<30)) < 0 { - return nil, errors.New("sessionIDTable or sessionIDLength is too small") - } - if c.SessionIDLength.From <= 0 { - return nil, errors.New("sessionIDLength.from must be greater than 0") - } - for i := 0; i < len(c.SessionIDTable); i++ { - if c.SessionIDTable[i] >= 0x80 { - return nil, errors.New("sessionIDTable must contain only ASCII characters") - } - } - } - - if c.SeqPlacement != "path" && c.SeqKey == "" { - switch c.SeqPlacement { - case "cookie", "query": - c.SeqKey = "x_seq" - case "header": - c.SeqKey = "X-Seq" - } - } - - if c.UplinkDataPlacement != splithttp.PlacementBody && c.UplinkDataKey == "" { - switch c.UplinkDataPlacement { - case splithttp.PlacementCookie: - c.UplinkDataKey = "x_data" - case splithttp.PlacementAuto, splithttp.PlacementHeader: - c.UplinkDataKey = "X-Data" - } - } - - if c.ServerMaxHeaderBytes < 0 { - return nil, errors.New("invalid negative value of maxHeaderBytes") - } - - if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 { - return nil, errors.New("maxConnections cannot be specified together with maxConcurrency") - } - if c.Xmux == (XmuxConfig{}) { - c.Xmux.MaxConnections.From = 6 - c.Xmux.MaxConnections.To = 6 - c.Xmux.HMaxRequestTimes.From = 600 - c.Xmux.HMaxRequestTimes.To = 900 - c.Xmux.HMaxReusableSecs.From = 1800 - c.Xmux.HMaxReusableSecs.To = 3000 - } - - config := &splithttp.Config{ - Host: c.Host, - Path: c.Path, - Mode: c.Mode, - Headers: c.Headers, - XPaddingBytes: newRangeConfig(c.XPaddingBytes), - XPaddingObfsMode: c.XPaddingObfsMode, - XPaddingKey: c.XPaddingKey, - XPaddingHeader: c.XPaddingHeader, - XPaddingPlacement: c.XPaddingPlacement, - XPaddingMethod: c.XPaddingMethod, - UplinkHTTPMethod: c.UplinkHTTPMethod, - SessionIDPlacement: c.SessionIDPlacement, - SeqPlacement: c.SeqPlacement, - SessionIDKey: c.SessionIDKey, - SeqKey: c.SeqKey, - UplinkDataPlacement: c.UplinkDataPlacement, - UplinkDataKey: c.UplinkDataKey, - UplinkChunkSize: newRangeConfig(c.UplinkChunkSize), - NoGRPCHeader: c.NoGRPCHeader, - NoSSEHeader: c.NoSSEHeader, - ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes), - ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs), - ScMaxBufferedPosts: c.ScMaxBufferedPosts, - ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs), - ServerMaxHeaderBytes: c.ServerMaxHeaderBytes, - SessionIDTable: c.SessionIDTable, - SessionIDLength: newRangeConfig(c.SessionIDLength), - Xmux: &splithttp.XmuxConfig{ - MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency), - MaxConnections: newRangeConfig(c.Xmux.MaxConnections), - CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes), - HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes), - HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs), - HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod, - }, - } - - if c.DownloadSettings != nil { - if c.Mode == "stream-one" { - return nil, errors.New(`Can not use "downloadSettings" in "stream-one" mode.`) - } - var err error - if config.DownloadSettings, err = c.DownloadSettings.Build(); err != nil { - return nil, errors.New(`Failed to build "downloadSettings".`).Base(err) - } - } - - return config, nil -} - -func roomSize(tableSize int, min, max int32) *big.Int { - base := big.NewInt(int64(tableSize)) - sum := new(big.Int) - term := new(big.Int) - for k := min; k <= max; k++ { - term.Exp(base, big.NewInt(int64(k)), nil) - sum.Add(sum, term) - } - return sum -} - -const ( - Byte = 1 - Kilobyte = 1024 * Byte - Megabyte = 1024 * Kilobyte - Gigabyte = 1024 * Megabyte - Terabyte = 1024 * Gigabyte -) - -type Bandwidth string - -func (b Bandwidth) Bps() (uint64, error) { - s := strings.TrimSpace(strings.ToLower(string(b))) - if s == "" { - return 0, nil - } - - idx := len(s) - for i, c := range s { - if (c < '0' || c > '9') && c != '.' { - idx = i - break - } - } - - numStr := s[:idx] - unit := strings.TrimSpace(s[idx:]) - - val, err := strconv.ParseFloat(numStr, 64) - if err != nil { - return 0, err - } - - mul := uint64(1) - switch unit { - case "", "b", "bps": - mul = Byte - case "k", "kb", "kbps": - mul = Kilobyte - case "m", "mb", "mbps": - mul = Megabyte - case "g", "gb", "gbps": - mul = Gigabyte - case "t", "tb", "tbps": - mul = Terabyte - default: - return 0, errors.New("unsupported unit: " + unit) - } - - return uint64(val*float64(mul)) / 8, nil -} - -type UdpHop struct { - PortList PortList `json:"ports"` - Interval Int32Range `json:"interval"` -} - -type Masquerade struct { - Type string `json:"type"` - - Dir string `json:"dir"` - - Url string `json:"url"` - RewriteHost bool `json:"rewriteHost"` - Insecure bool `json:"insecure"` - - Content string `json:"content"` - Headers map[string]string `json:"headers"` - StatusCode int32 `json:"statusCode"` -} - -type HysteriaConfig struct { - Version int32 `json:"version"` - Auth string `json:"auth"` - - Congestion *string `json:"congestion"` - Up *Bandwidth `json:"up"` - Down *Bandwidth `json:"down"` - UdpHop *UdpHop `json:"udphop"` - - UdpIdleTimeout int64 `json:"udpIdleTimeout"` - Masquerade Masquerade `json:"masquerade"` -} - -func (c *HysteriaConfig) Build() (proto.Message, error) { - if c.Version != 2 { - return nil, errors.New("version != 2") - } - - if c.Congestion != nil || c.Up != nil || c.Down != nil || c.UdpHop != nil { - errors.LogWarning(context.Background(), "congestion & up & down & udphop move to finalmask/quicParams") - } - - if c.UdpIdleTimeout != 0 && (c.UdpIdleTimeout < 2 || c.UdpIdleTimeout > 600) { - return nil, errors.New("UdpIdleTimeout must be between 2 and 600") - } - - config := &hysteria.Config{} - config.Auth = c.Auth - config.UdpIdleTimeout = c.UdpIdleTimeout - config.MasqType = c.Masquerade.Type - config.MasqFile = c.Masquerade.Dir - config.MasqUrl = c.Masquerade.Url - config.MasqUrlRewriteHost = c.Masquerade.RewriteHost - config.MasqUrlInsecure = c.Masquerade.Insecure - config.MasqString = c.Masquerade.Content - config.MasqStringHeaders = c.Masquerade.Headers - config.MasqStringStatusCode = c.Masquerade.StatusCode - - if config.UdpIdleTimeout == 0 { - config.UdpIdleTimeout = 60 - } - - return config, nil -} - -func readFileOrString(f string, s []string) ([]byte, error) { - if len(f) > 0 { - return filesystem.ReadCert(f) - } - if len(s) > 0 { - return []byte(strings.Join(s, "\n")), nil - } - return nil, errors.New("both file and bytes are empty.") -} - -type TLSCertConfig struct { - CertFile string `json:"certificateFile"` - CertStr []string `json:"certificate"` - KeyFile string `json:"keyFile"` - KeyStr []string `json:"key"` - Usage string `json:"usage"` - OcspStapling uint64 `json:"ocspStapling"` - OneTimeLoading bool `json:"oneTimeLoading"` - BuildChain bool `json:"buildChain"` -} - -// Build implements Buildable. -func (c *TLSCertConfig) Build() (*tls.Certificate, error) { - certificate := new(tls.Certificate) - - cert, err := readFileOrString(c.CertFile, c.CertStr) - if err != nil { - return nil, errors.New("failed to parse certificate").Base(err) - } - certificate.Certificate = cert - certificate.CertificatePath = c.CertFile - - if len(c.KeyFile) > 0 || len(c.KeyStr) > 0 { - key, err := readFileOrString(c.KeyFile, c.KeyStr) - if err != nil { - return nil, errors.New("failed to parse key").Base(err) - } - certificate.Key = key - certificate.KeyPath = c.KeyFile - } - - switch strings.ToLower(c.Usage) { - case "encipherment": - certificate.Usage = tls.Certificate_ENCIPHERMENT - case "verify": - certificate.Usage = tls.Certificate_AUTHORITY_VERIFY - case "issue": - certificate.Usage = tls.Certificate_AUTHORITY_ISSUE - default: - certificate.Usage = tls.Certificate_ENCIPHERMENT - } - if certificate.KeyPath == "" && certificate.CertificatePath == "" { - certificate.OneTimeLoading = true - } else { - certificate.OneTimeLoading = c.OneTimeLoading - } - certificate.OcspStapling = c.OcspStapling - certificate.BuildChain = c.BuildChain - - return certificate, nil -} - -type QuicParamsConfig struct { - Congestion string `json:"congestion"` - Debug bool `json:"debug"` - BbrProfile string `json:"bbrProfile"` - BrutalUp Bandwidth `json:"brutalUp"` - BrutalDown Bandwidth `json:"brutalDown"` - UdpHop UdpHop `json:"udpHop"` - InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"` - MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"` - InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"` - MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"` - MaxIdleTimeout int64 `json:"maxIdleTimeout"` - KeepAlivePeriod int64 `json:"keepAlivePeriod"` - DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"` - MaxIncomingStreams int64 `json:"maxIncomingStreams"` -} - -type TLSConfig struct { - AllowInsecure bool `json:"allowInsecure"` - Certs []*TLSCertConfig `json:"certificates"` - ServerName string `json:"serverName"` - ALPN *StringList `json:"alpn"` - EnableSessionResumption bool `json:"enableSessionResumption"` - DisableSystemRoot bool `json:"disableSystemRoot"` - MinVersion string `json:"minVersion"` - MaxVersion string `json:"maxVersion"` - CipherSuites string `json:"cipherSuites"` - Fingerprint string `json:"fingerprint"` - RejectUnknownSNI bool `json:"rejectUnknownSni"` - CurvePreferences *StringList `json:"curvePreferences"` - MasterKeyLog string `json:"masterKeyLog"` - PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"` - VerifyPeerCertByName string `json:"verifyPeerCertByName"` - ECHServerKeys string `json:"echServerKeys"` - ECHConfigList string `json:"echConfigList"` - ECHSocketSettings *SocketConfig `json:"echSockopt"` -} - -// Build implements Buildable. -func (c *TLSConfig) Build() (proto.Message, error) { - config := new(tls.Config) - config.Certificate = make([]*tls.Certificate, len(c.Certs)) - for idx, certConf := range c.Certs { - cert, err := certConf.Build() - if err != nil { - return nil, err - } - config.Certificate[idx] = cert - } - serverName := c.ServerName - if len(c.ServerName) > 0 { - config.ServerName = serverName - } - if c.ALPN != nil && len(*c.ALPN) > 0 { - config.NextProtocol = []string(*c.ALPN) - } - if len(config.NextProtocol) > 1 { - for _, p := range config.NextProtocol { - if tls.IsFromMitm(p) { - return nil, errors.New(`only one element is allowed in "alpn" when using "fromMitm" in it`) - } - } - } - if c.CurvePreferences != nil && len(*c.CurvePreferences) > 0 { - config.CurvePreferences = []string(*c.CurvePreferences) - } - config.EnableSessionResumption = c.EnableSessionResumption - config.DisableSystemRoot = c.DisableSystemRoot - config.MinVersion = c.MinVersion - config.MaxVersion = c.MaxVersion - config.CipherSuites = c.CipherSuites - config.Fingerprint = strings.ToLower(c.Fingerprint) - if config.Fingerprint != "unsafe" && tls.GetFingerprint(config.Fingerprint) == nil { - return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint) - } - config.RejectUnknownSni = c.RejectUnknownSNI - config.MasterKeyLog = c.MasterKeyLog - - if c.AllowInsecure { - return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn)`) - } - if c.PinnedPeerCertSha256 != "" { - for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") { - v = strings.TrimSpace(v) - if v == "" { - continue - } - // remove colons for OpenSSL format - hashValue, err := hex.DecodeString(strings.ReplaceAll(v, ":", "")) - if err != nil { - return nil, err - } - if len(hashValue) != 32 { - return nil, errors.New("incorrect pinnedPeerCertSha256 length: ", v) - } - config.PinnedPeerCertSha256 = append(config.PinnedPeerCertSha256, hashValue) - } - } - if c.VerifyPeerCertByName != "" { - for v := range strings.SplitSeq(c.VerifyPeerCertByName, ",") { - v = strings.TrimSpace(v) - if v == "" { - continue - } - config.VerifyPeerCertByName = append(config.VerifyPeerCertByName, v) - } - } - - if c.ECHServerKeys != "" { - EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHServerKeys) - if err != nil { - return nil, errors.New("invalid ECH Config", c.ECHServerKeys) - } - config.EchServerKeys = EchPrivateKey - } - config.EchConfigList = c.ECHConfigList - if c.ECHSocketSettings != nil { - ss, err := c.ECHSocketSettings.Build() - if err != nil { - return nil, errors.New("Failed to build ech sockopt.").Base(err) - } - config.EchSocketSettings = ss - } - - return config, nil -} - -type LimitFallback struct { - AfterBytes uint64 - BytesPerSec uint64 - BurstBytesPerSec uint64 -} - -type REALITYConfig struct { - MasterKeyLog string `json:"masterKeyLog"` - Show bool `json:"show"` - Target json.RawMessage `json:"target"` - Dest json.RawMessage `json:"dest"` - Type string `json:"type"` - Xver uint64 `json:"xver"` - ServerNames []string `json:"serverNames"` - PrivateKey string `json:"privateKey"` - MinClientVer string `json:"minClientVer"` - MaxClientVer string `json:"maxClientVer"` - MaxTimeDiff uint64 `json:"maxTimeDiff"` - ShortIds []string `json:"shortIds"` - Mldsa65Seed string `json:"mldsa65Seed"` - - LimitFallbackUpload LimitFallback `json:"limitFallbackUpload"` - LimitFallbackDownload LimitFallback `json:"limitFallbackDownload"` - - Fingerprint string `json:"fingerprint"` - ServerName string `json:"serverName"` - Password string `json:"password"` - PublicKey string `json:"publicKey"` - ShortId string `json:"shortId"` - Mldsa65Verify string `json:"mldsa65Verify"` - SpiderX string `json:"spiderX"` -} - -func (c *REALITYConfig) Build() (proto.Message, error) { - config := new(reality.Config) - config.MasterKeyLog = c.MasterKeyLog - config.Show = c.Show - var err error - if c.Target != nil { - c.Dest = c.Target - } - if c.Dest != nil { - var i uint16 - var s string - if err = json.Unmarshal(c.Dest, &i); err == nil { - s = strconv.Itoa(int(i)) - } else { - _ = json.Unmarshal(c.Dest, &s) - } - if c.Type == "" && s != "" { - switch s[0] { - case '@', '/': - c.Type = "unix" - if s[0] == '@' && len(s) > 1 && s[1] == '@' && (runtime.GOOS == "linux" || runtime.GOOS == "android") { - fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy - copy(fullAddr, s[1:]) - s = string(fullAddr) - } - default: - if _, err = strconv.Atoi(s); err == nil { - s = "localhost:" + s - } - if _, _, err = net.SplitHostPort(s); err == nil { - c.Type = "tcp" - } - } - } - if c.Type == "" { - return nil, errors.New(`please fill in a valid value for "target"`) - } - if c.Xver > 2 { - return nil, errors.New(`invalid PROXY protocol version, "xver" only accepts 0, 1, 2`) - } - if len(c.ServerNames) == 0 { - return nil, errors.New(`empty "serverNames"`) - } - if c.PrivateKey == "" { - return nil, errors.New(`empty "privateKey"`) - } - if config.PrivateKey, err = base64.RawURLEncoding.DecodeString(c.PrivateKey); err != nil || len(config.PrivateKey) != 32 { - return nil, errors.New(`invalid "privateKey": `, c.PrivateKey) - } - if c.MinClientVer != "" { - config.MinClientVer = make([]byte, 3) - var u uint64 - for i, s := range strings.Split(c.MinClientVer, ".") { - if i == 3 { - return nil, errors.New(`invalid "minClientVer": `, c.MinClientVer) - } - if u, err = strconv.ParseUint(s, 10, 8); err != nil { - return nil, errors.New(`"minClientVer[`, i, `]" should be less than 256`) - } else { - config.MinClientVer[i] = byte(u) - } - } - } - if c.MaxClientVer != "" { - config.MaxClientVer = make([]byte, 3) - var u uint64 - for i, s := range strings.Split(c.MaxClientVer, ".") { - if i == 3 { - return nil, errors.New(`invalid "maxClientVer": `, c.MaxClientVer) - } - if u, err = strconv.ParseUint(s, 10, 8); err != nil { - return nil, errors.New(`"maxClientVer[`, i, `]" should be less than 256`) - } else { - config.MaxClientVer[i] = byte(u) - } - } - } - if len(c.ShortIds) == 0 { - return nil, errors.New(`empty "shortIds"`) - } - config.ShortIds = make([][]byte, len(c.ShortIds)) - for i, s := range c.ShortIds { - if len(s) > 16 { - return nil, errors.New(`too long "shortIds[`, i, `]": `, s) - } - config.ShortIds[i] = make([]byte, 8) - if _, err = hex.Decode(config.ShortIds[i], []byte(s)); err != nil { - return nil, errors.New(`invalid "shortIds[`, i, `]": `, s) - } - } - config.Dest = s - config.Type = c.Type - config.Xver = c.Xver - config.ServerNames = c.ServerNames - config.MaxTimeDiff = c.MaxTimeDiff - - if c.Mldsa65Seed != "" { - if c.Mldsa65Seed == c.PrivateKey { - return nil, errors.New(`"mldsa65Seed" and "privateKey" can not be the same value: `, c.Mldsa65Seed) - } - if config.Mldsa65Seed, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Seed); err != nil || len(config.Mldsa65Seed) != 32 { - return nil, errors.New(`invalid "mldsa65Seed": `, c.Mldsa65Seed) - } - } - - for _, sn := range config.ServerNames { - if strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") { - errors.LogWarning(context.Background(), `REALITY: Choosing apple, icloud, etc. as the target may get your IP blocked by the GFW`) - } - } - - config.LimitFallbackUpload = new(reality.LimitFallback) - config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes - config.LimitFallbackUpload.BytesPerSec = c.LimitFallbackUpload.BytesPerSec - config.LimitFallbackUpload.BurstBytesPerSec = c.LimitFallbackUpload.BurstBytesPerSec - config.LimitFallbackDownload = new(reality.LimitFallback) - config.LimitFallbackDownload.AfterBytes = c.LimitFallbackDownload.AfterBytes - config.LimitFallbackDownload.BytesPerSec = c.LimitFallbackDownload.BytesPerSec - config.LimitFallbackDownload.BurstBytesPerSec = c.LimitFallbackDownload.BurstBytesPerSec - } else { - config.Fingerprint = strings.ToLower(c.Fingerprint) - if config.Fingerprint == "unsafe" || config.Fingerprint == "hellogolang" { - return nil, errors.New(`invalid "fingerprint": `, config.Fingerprint) - } - if tls.GetFingerprint(config.Fingerprint) == nil { - return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint) - } - if len(c.ServerNames) != 0 { - return nil, errors.New(`non-empty "serverNames", please use "serverName" instead`) - } - if c.Password != "" { - c.PublicKey = c.Password - } - if c.PublicKey == "" { - return nil, errors.New(`empty "password"`) - } - if config.PublicKey, err = base64.RawURLEncoding.DecodeString(c.PublicKey); err != nil || len(config.PublicKey) != 32 { - return nil, errors.New(`invalid "password": `, c.PublicKey) - } - if len(c.ShortIds) != 0 { - return nil, errors.New(`non-empty "shortIds", please use "shortId" instead`) - } - if len(c.ShortId) > 16 { - return nil, errors.New(`too long "shortId": `, c.ShortId) - } - config.ShortId = make([]byte, 8) - if _, err = hex.Decode(config.ShortId, []byte(c.ShortId)); err != nil { - return nil, errors.New(`invalid "shortId": `, c.ShortId) - } - if c.Mldsa65Verify != "" { - if config.Mldsa65Verify, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Verify); err != nil || len(config.Mldsa65Verify) != 1952 { - return nil, errors.New(`invalid "mldsa65Verify": `, c.Mldsa65Verify) - } - } - if c.SpiderX == "" { - c.SpiderX = "/" - } - if c.SpiderX[0] != '/' { - return nil, errors.New(`invalid "spiderX": `, c.SpiderX) - } - config.SpiderY = make([]int64, 10) - u, _ := url.Parse(c.SpiderX) - q := u.Query() - parse := func(param string, index int) { - if q.Get(param) != "" { - s := strings.Split(q.Get(param), "-") - if len(s) == 1 { - config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64) - config.SpiderY[index+1], _ = strconv.ParseInt(s[0], 10, 64) - } else { - config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64) - config.SpiderY[index+1], _ = strconv.ParseInt(s[1], 10, 64) - } - } - q.Del(param) - } - parse("p", 0) // padding - parse("c", 2) // concurrency - parse("t", 4) // times - parse("i", 6) // interval - parse("r", 8) // return - u.RawQuery = q.Encode() - config.SpiderX = u.String() - config.ServerName = c.ServerName - } - return config, nil -} - type TransportProtocol string // Build implements Buildable. @@ -1030,1013 +41,10 @@ func (p TransportProtocol) Build() (string, error) { } } -type CustomSockoptConfig struct { - Syetem string `json:"system"` - Network string `json:"network"` - Level string `json:"level"` - Opt string `json:"opt"` - Value string `json:"value"` - Type string `json:"type"` -} - -type HappyEyeballsConfig struct { - PrioritizeIPv6 bool `json:"prioritizeIPv6"` - TryDelayMs uint64 `json:"tryDelayMs"` - Interleave uint32 `json:"interleave"` - MaxConcurrentTry uint32 `json:"maxConcurrentTry"` -} - -func (h *HappyEyeballsConfig) UnmarshalJSON(data []byte) error { - innerHappyEyeballsConfig := struct { - PrioritizeIPv6 bool `json:"prioritizeIPv6"` - TryDelayMs uint64 `json:"tryDelayMs"` - Interleave uint32 `json:"interleave"` - MaxConcurrentTry uint32 `json:"maxConcurrentTry"` - }{PrioritizeIPv6: false, Interleave: 1, TryDelayMs: 0, MaxConcurrentTry: 4} - if err := json.Unmarshal(data, &innerHappyEyeballsConfig); err != nil { - return err - } - h.PrioritizeIPv6 = innerHappyEyeballsConfig.PrioritizeIPv6 - h.TryDelayMs = innerHappyEyeballsConfig.TryDelayMs - h.Interleave = innerHappyEyeballsConfig.Interleave - h.MaxConcurrentTry = innerHappyEyeballsConfig.MaxConcurrentTry - return nil -} - -type SocketConfig struct { - Mark int32 `json:"mark"` - TFO interface{} `json:"tcpFastOpen"` - TProxy string `json:"tproxy"` - AcceptProxyProtocol bool `json:"acceptProxyProtocol"` - DomainStrategy string `json:"domainStrategy"` - DialerProxy string `json:"dialerProxy"` - TCPKeepAliveInterval int32 `json:"tcpKeepAliveInterval"` - TCPKeepAliveIdle int32 `json:"tcpKeepAliveIdle"` - TCPCongestion string `json:"tcpCongestion"` - TCPWindowClamp int32 `json:"tcpWindowClamp"` - TCPMaxSeg int32 `json:"tcpMaxSeg"` - Penetrate bool `json:"penetrate"` - TCPUserTimeout int32 `json:"tcpUserTimeout"` - V6only bool `json:"v6only"` - Interface string `json:"interface"` - TcpMptcp bool `json:"tcpMptcp"` - CustomSockopt []*CustomSockoptConfig `json:"customSockopt"` - AddressPortStrategy string `json:"addressPortStrategy"` - HappyEyeballsSettings *HappyEyeballsConfig `json:"happyEyeballs"` - TrustedXForwardedFor []string `json:"trustedXForwardedFor"` -} - -// Build implements Buildable. -func (c *SocketConfig) Build() (*internet.SocketConfig, error) { - tfo := int32(0) // don't invoke setsockopt() for TFO - if c.TFO != nil { - switch v := c.TFO.(type) { - case bool: - if v { - tfo = 256 - } else { - tfo = -1 // TFO need to be disabled - } - case float64: - tfo = int32(math.Min(v, math.MaxInt32)) - default: - return nil, errors.New("tcpFastOpen: only boolean and integer value is acceptable") - } - } - var tproxy internet.SocketConfig_TProxyMode - switch strings.ToLower(c.TProxy) { - case "tproxy": - tproxy = internet.SocketConfig_TProxy - case "redirect": - tproxy = internet.SocketConfig_Redirect - default: - tproxy = internet.SocketConfig_Off - } - - dStrategy := internet.DomainStrategy_AS_IS - switch strings.ToLower(c.DomainStrategy) { - case "asis", "": - dStrategy = internet.DomainStrategy_AS_IS - case "useip": - dStrategy = internet.DomainStrategy_USE_IP - case "useipv4": - dStrategy = internet.DomainStrategy_USE_IP4 - case "useipv6": - dStrategy = internet.DomainStrategy_USE_IP6 - case "useipv4v6": - dStrategy = internet.DomainStrategy_USE_IP46 - case "useipv6v4": - dStrategy = internet.DomainStrategy_USE_IP64 - case "forceip": - dStrategy = internet.DomainStrategy_FORCE_IP - case "forceipv4": - dStrategy = internet.DomainStrategy_FORCE_IP4 - case "forceipv6": - dStrategy = internet.DomainStrategy_FORCE_IP6 - case "forceipv4v6": - dStrategy = internet.DomainStrategy_FORCE_IP46 - case "forceipv6v4": - dStrategy = internet.DomainStrategy_FORCE_IP64 - default: - return nil, errors.New("unsupported domain strategy: ", c.DomainStrategy) - } - - var customSockopts []*internet.CustomSockopt - - for _, copt := range c.CustomSockopt { - customSockopt := &internet.CustomSockopt{ - System: copt.Syetem, - Network: copt.Network, - Level: copt.Level, - Opt: copt.Opt, - Value: copt.Value, - Type: copt.Type, - } - customSockopts = append(customSockopts, customSockopt) - } - - addressPortStrategy := internet.AddressPortStrategy_None - switch strings.ToLower(c.AddressPortStrategy) { - case "none", "": - addressPortStrategy = internet.AddressPortStrategy_None - case "srvportonly": - addressPortStrategy = internet.AddressPortStrategy_SrvPortOnly - case "srvaddressonly": - addressPortStrategy = internet.AddressPortStrategy_SrvAddressOnly - case "srvportandaddress": - addressPortStrategy = internet.AddressPortStrategy_SrvPortAndAddress - case "txtportonly": - addressPortStrategy = internet.AddressPortStrategy_TxtPortOnly - case "txtaddressonly": - addressPortStrategy = internet.AddressPortStrategy_TxtAddressOnly - case "txtportandaddress": - addressPortStrategy = internet.AddressPortStrategy_TxtPortAndAddress - default: - return nil, errors.New("unsupported address and port strategy: ", c.AddressPortStrategy) - } - - happyEyeballs := &internet.HappyEyeballsConfig{Interleave: 1, PrioritizeIpv6: false, TryDelayMs: 0, MaxConcurrentTry: 4} - if c.HappyEyeballsSettings != nil { - happyEyeballs.PrioritizeIpv6 = c.HappyEyeballsSettings.PrioritizeIPv6 - happyEyeballs.Interleave = c.HappyEyeballsSettings.Interleave - happyEyeballs.TryDelayMs = c.HappyEyeballsSettings.TryDelayMs - happyEyeballs.MaxConcurrentTry = c.HappyEyeballsSettings.MaxConcurrentTry - } - - return &internet.SocketConfig{ - Mark: c.Mark, - Tfo: tfo, - Tproxy: tproxy, - DomainStrategy: dStrategy, - AcceptProxyProtocol: c.AcceptProxyProtocol, - DialerProxy: c.DialerProxy, - TcpKeepAliveInterval: c.TCPKeepAliveInterval, - TcpKeepAliveIdle: c.TCPKeepAliveIdle, - TcpCongestion: c.TCPCongestion, - TcpWindowClamp: c.TCPWindowClamp, - TcpMaxSeg: c.TCPMaxSeg, - Penetrate: c.Penetrate, - TcpUserTimeout: c.TCPUserTimeout, - V6Only: c.V6only, - Interface: c.Interface, - TcpMptcp: c.TcpMptcp, - CustomSockopt: customSockopts, - AddressPortStrategy: addressPortStrategy, - HappyEyeballs: happyEyeballs, - TrustedXForwardedFor: c.TrustedXForwardedFor, - }, nil -} - -func PraseByteSlice(data json.RawMessage, typ string) ([]byte, error) { - switch strings.ToLower(typ) { - case "", "array": - if len(data) == 0 { - return data, nil - } - var packet []byte - if err := json.Unmarshal(data, &packet); err != nil { - return nil, err - } - return packet, nil - case "str": - var str string - if err := json.Unmarshal(data, &str); err != nil { - return nil, err - } - return []byte(str), nil - case "hex": - var str string - if err := json.Unmarshal(data, &str); err != nil { - return nil, err - } - return hex.DecodeString(str) - case "base64": - var str string - if err := json.Unmarshal(data, &str); err != nil { - return nil, err - } - return base64.StdEncoding.DecodeString(str) - default: - return nil, errors.New("unknown type") - } -} - -var ( - customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) - - tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ - "header-custom": func() interface{} { return new(HeaderCustomTCP) }, - "fragment": func() interface{} { return new(FragmentMask) }, - "sudoku": func() interface{} { return new(Sudoku) }, - }, "type", "settings") - - udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ - "header-custom": func() interface{} { return new(HeaderCustomUDP) }, - "mkcp-legacy": func() interface{} { return new(MkcpLegacy) }, - "noise": func() interface{} { return new(NoiseMask) }, - "salamander": func() interface{} { return new(Salamander) }, - "sudoku": func() interface{} { return new(Sudoku) }, - "xdns": func() interface{} { return new(Xdns) }, - "xicmp": func() interface{} { return new(Xicmp) }, - "realm": func() interface{} { return new(Realm) }, - }, "type", "settings") -) - -type TCPItem struct { - Delay Int32Range `json:"delay"` - Rand int32 `json:"rand"` - RandRange *Int32Range `json:"randRange"` - Capture string `json:"capture"` - Type string `json:"type"` - Reuse string `json:"reuse"` - Transform *CustomTransform `json:"transform"` - Packet json.RawMessage `json:"packet"` -} - -type HeaderCustomTCP struct { - Clients [][]TCPItem `json:"clients"` - Servers [][]TCPItem `json:"servers"` - Errors [][]TCPItem `json:"errors"` -} - -func (c *HeaderCustomTCP) Build() (proto.Message, error) { - for _, value := range c.Clients { - for _, item := range value { - if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil { - return nil, err - } - } - } - for _, value := range c.Servers { - for _, item := range value { - if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil { - return nil, err - } - } - } - for _, value := range c.Errors { - for _, item := range value { - if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil { - return nil, err - } - } - } - - errInvalidRange := errors.New("invalid randRange") - - clients := make([]*custom.TCPSequence, len(c.Clients)) - for i, value := range c.Clients { - clients[i] = &custom.TCPSequence{} - for _, item := range value { - if item.RandRange == nil { - item.RandRange = &Int32Range{From: 0, To: 255} - } - if item.RandRange.From < 0 || item.RandRange.To > 255 { - return nil, errInvalidRange - } - var err error - if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { - return nil, err - } - transform, err := buildCustomTransform(item.Transform) - if err != nil { - return nil, err - } - clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{ - DelayMin: int64(item.Delay.From), - DelayMax: int64(item.Delay.To), - Rand: item.Rand, - RandMin: item.RandRange.From, - RandMax: item.RandRange.To, - Packet: item.Packet, - Save: item.Capture, - Var: item.Reuse, - Expr: transform, - }) - } - } - - servers := make([]*custom.TCPSequence, len(c.Servers)) - for i, value := range c.Servers { - servers[i] = &custom.TCPSequence{} - for _, item := range value { - if item.RandRange == nil { - item.RandRange = &Int32Range{From: 0, To: 255} - } - if item.RandRange.From < 0 || item.RandRange.To > 255 { - return nil, errInvalidRange - } - var err error - if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { - return nil, err - } - transform, err := buildCustomTransform(item.Transform) - if err != nil { - return nil, err - } - servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{ - DelayMin: int64(item.Delay.From), - DelayMax: int64(item.Delay.To), - Rand: item.Rand, - RandMin: item.RandRange.From, - RandMax: item.RandRange.To, - Packet: item.Packet, - Save: item.Capture, - Var: item.Reuse, - Expr: transform, - }) - } - } - - errors := make([]*custom.TCPSequence, len(c.Errors)) - for i, value := range c.Errors { - errors[i] = &custom.TCPSequence{} - for _, item := range value { - if item.RandRange == nil { - item.RandRange = &Int32Range{From: 0, To: 255} - } - if item.RandRange.From < 0 || item.RandRange.To > 255 { - return nil, errInvalidRange - } - var err error - if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { - return nil, err - } - transform, err := buildCustomTransform(item.Transform) - if err != nil { - return nil, err - } - errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{ - DelayMin: int64(item.Delay.From), - DelayMax: int64(item.Delay.To), - Rand: item.Rand, - RandMin: item.RandRange.From, - RandMax: item.RandRange.To, - Packet: item.Packet, - Save: item.Capture, - Var: item.Reuse, - Expr: transform, - }) - } - } - - return &custom.TCPConfig{ - Clients: clients, - Servers: servers, - Errors: errors, - }, nil -} - -type FragmentMask struct { - 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) { - config := &fragment.Config{} - - switch strings.ToLower(c.Packets) { - case "tlshello": - config.PacketsFrom = 0 - config.PacketsTo = 1 - case "": - config.PacketsFrom = 0 - config.PacketsTo = 0 - default: - from, to, err := ParseRangeString(c.Packets) - if err != nil { - return nil, errors.New("Invalid PacketsFrom").Base(err) - } - config.PacketsFrom = int64(from) - config.PacketsTo = int64(to) - if config.PacketsFrom == 0 { - return nil, errors.New("PacketsFrom 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)) - } - - 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) - - return config, nil -} - -type NoiseItem struct { - Rand Int32Range `json:"rand"` - RandRange *Int32Range `json:"randRange"` - Type string `json:"type"` - Packet json.RawMessage `json:"packet"` - Delay Int32Range `json:"delay"` -} - -type NoiseMask struct { - Reset Int32Range `json:"reset"` - Noise []NoiseItem `json:"noise"` -} - -func (c *NoiseMask) Build() (proto.Message, error) { - 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 item.RandRange == nil { - item.RandRange = &Int32Range{From: 0, To: 255} - } - if item.RandRange.From < 0 || item.RandRange.To > 255 { - return nil, errors.New("invalid randRange") - } - var err error - if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { - return nil, err - } - noiseSlice = append(noiseSlice, &noise.Item{ - RandMin: int64(item.Rand.From), - RandMax: int64(item.Rand.To), - RandRangeMin: item.RandRange.From, - RandRangeMax: item.RandRange.To, - Packet: item.Packet, - DelayMin: int64(item.Delay.From), - DelayMax: int64(item.Delay.To), - }) - } - - return &noise.Config{ - ResetMin: int64(c.Reset.From), - ResetMax: int64(c.Reset.To), - Items: noiseSlice, - }, nil -} - -type UDPItem struct { - Rand int32 `json:"rand"` - RandRange *Int32Range `json:"randRange"` - Capture string `json:"capture"` - Type string `json:"type"` - Reuse string `json:"reuse"` - Transform *CustomTransform `json:"transform"` - Packet json.RawMessage `json:"packet"` -} - -type CustomTransform struct { - Op string `json:"op"` - Args []CustomTransformArg `json:"args"` -} - -type CustomTransformArg struct { - Type string `json:"type"` - Bytes json.RawMessage `json:"bytes"` - U64 *uint64 `json:"u64"` - Reuse string `json:"reuse"` - Metadata string `json:"metadata"` - Transform *CustomTransform `json:"transform"` -} - -func validateCustomVarName(name string) error { - if name == "" { - return nil - } - if !customVarNamePattern.MatchString(name) { - return errors.New("invalid variable name") - } - return nil -} - -func validateCustomItemSpec(capture string, packet json.RawMessage, rand int32, reuse string, transform *CustomTransform) error { - if err := validateCustomVarName(capture); err != nil { - return err - } - if err := validateCustomVarName(reuse); err != nil { - return err - } - - kindCount := 0 - if len(packet) > 0 { - kindCount++ - } - if rand > 0 { - kindCount++ - } - if reuse != "" { - kindCount++ - } - if transform != nil { - kindCount++ - } - if kindCount > 1 { - return errors.New("exactly one item kind must be set") - } - if kindCount == 0 && capture != "" { - return errors.New("exactly one item kind must be set") - } - - return nil -} - -func buildCustomTransform(transform *CustomTransform) (*custom.Expr, error) { - if transform == nil { - return nil, nil - } - if transform.Op == "" { - return nil, errors.New("transform op is required") - } - if len(transform.Args) == 0 { - return nil, errors.New("transform args are required") - } - - args := make([]*custom.ExprArg, 0, len(transform.Args)) - for _, arg := range transform.Args { - parsedArg, err := buildCustomTransformArg(arg) - if err != nil { - return nil, err - } - args = append(args, parsedArg) - } - - return &custom.Expr{ - Op: transform.Op, - Args: args, - }, nil -} - -func buildCustomTransformArg(arg CustomTransformArg) (*custom.ExprArg, error) { - kindCount := 0 - if len(arg.Bytes) > 0 { - kindCount++ - } - if arg.U64 != nil { - kindCount++ - } - if arg.Reuse != "" { - kindCount++ - } - if arg.Metadata != "" { - kindCount++ - } - if arg.Transform != nil { - kindCount++ - } - if kindCount != 1 { - return nil, errors.New("transform arg must set exactly one value") - } - - if len(arg.Bytes) > 0 { - value, err := PraseByteSlice(arg.Bytes, arg.Type) - if err != nil { - return nil, err - } - return &custom.ExprArg{ - Value: &custom.ExprArg_Bytes{ - Bytes: value, - }, - }, nil - } - if arg.U64 != nil { - return &custom.ExprArg{ - Value: &custom.ExprArg_U64{ - U64: *arg.U64, - }, - }, nil - } - if arg.Reuse != "" { - if err := validateCustomVarName(arg.Reuse); err != nil { - return nil, err - } - return &custom.ExprArg{ - Value: &custom.ExprArg_Var{ - Var: arg.Reuse, - }, - }, nil - } - if arg.Metadata != "" { - return &custom.ExprArg{ - Value: &custom.ExprArg_Metadata{ - Metadata: arg.Metadata, - }, - }, nil - } - - parsedExpr, err := buildCustomTransform(arg.Transform) - if err != nil { - return nil, err - } - return &custom.ExprArg{ - Value: &custom.ExprArg_Expr{ - Expr: parsedExpr, - }, - }, nil -} - -type HeaderCustomUDP struct { - Mode string `json:"mode"` - Client []UDPItem `json:"client"` - Server []UDPItem `json:"server"` -} - -func (c *HeaderCustomUDP) Build() (proto.Message, error) { - switch c.Mode { - case "", "prefix", "standalone": - default: - return nil, errors.New("unknown udp mode") - } - - for _, item := range c.Client { - if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil { - return nil, err - } - } - for _, item := range c.Server { - if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil { - return nil, err - } - } - - client := make([]*custom.UDPItem, 0, len(c.Client)) - for _, item := range c.Client { - if item.RandRange == nil { - item.RandRange = &Int32Range{From: 0, To: 255} - } - if item.RandRange.From < 0 || item.RandRange.To > 255 { - return nil, errors.New("invalid randRange") - } - var err error - if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { - return nil, err - } - transform, err := buildCustomTransform(item.Transform) - if err != nil { - return nil, err - } - client = append(client, &custom.UDPItem{ - Rand: item.Rand, - RandMin: item.RandRange.From, - RandMax: item.RandRange.To, - Packet: item.Packet, - Save: item.Capture, - Var: item.Reuse, - Expr: transform, - }) - } - - server := make([]*custom.UDPItem, 0, len(c.Server)) - for _, item := range c.Server { - if item.RandRange == nil { - item.RandRange = &Int32Range{From: 0, To: 255} - } - if item.RandRange.From < 0 || item.RandRange.To > 255 { - return nil, errors.New("invalid randRange") - } - var err error - if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { - return nil, err - } - transform, err := buildCustomTransform(item.Transform) - if err != nil { - return nil, err - } - server = append(server, &custom.UDPItem{ - Rand: item.Rand, - RandMin: item.RandRange.From, - RandMax: item.RandRange.To, - Packet: item.Packet, - Save: item.Capture, - Var: item.Reuse, - Expr: transform, - }) - } - - if c.Mode == "standalone" { - return &custom.UDPStandaloneConfig{ - Client: client, - Server: server, - }, nil - } else { - return &custom.UDPConfig{ - Client: client, - Server: server, - }, nil - } -} - -type MkcpLegacy struct { - Header string `json:"header"` - Value string `json:"value"` -} - -func (c *MkcpLegacy) Build() (proto.Message, error) { - if len(c.Header) == 0 { - if len(c.Value) == 0 { - return &original.Config{}, nil - } else { - return &aes128gcm.Config{Password: c.Value}, nil - } - } - switch strings.ToLower(c.Header) { - case "dns": - domain := c.Value - if len(domain) == 0 { - domain = "www.baidu.com" - } - return &header.Config{ID: 0, Domain: domain}, nil - case "dtls": - return &header.Config{ID: 1}, nil - case "srtp": - return &header.Config{ID: 2}, nil - case "utp": - return &header.Config{ID: 3}, nil - case "wechat": - return &header.Config{ID: 4}, nil - case "wireguard": - return &header.Config{ID: 5}, nil - default: - return nil, errors.New("invalid header ", c.Header) - } -} - -type Salamander struct { - Password string `json:"password"` - PacketSize Int32Range `json:"packetSize"` -} - -func (c *Salamander) Build() (proto.Message, error) { - if c.PacketSize.To > 0 { - if c.PacketSize.From <= 0 || c.PacketSize.To > 2048 { - return nil, errors.New("gecko: invalid min/max packet size") - } - return &salamander.GeckoConfig{ - Password: c.Password, - MinPacketSize: c.PacketSize.From, - MaxPacketSize: c.PacketSize.To, - }, nil - } - return &salamander.Config{ - Password: c.Password, - }, nil -} - -type Sudoku struct { - Password string `json:"password"` - ASCII string `json:"ascii"` - - CustomTable string `json:"customTable"` - LegacyCustomTable string `json:"custom_table"` - CustomTables []string `json:"customTables"` - LegacyCustomSets []string `json:"custom_tables"` - - PaddingMin uint32 `json:"paddingMin"` - LegacyPaddingMin uint32 `json:"padding_min"` - PaddingMax uint32 `json:"paddingMax"` - LegacyPaddingMax uint32 `json:"padding_max"` -} - -func (c *Sudoku) Build() (proto.Message, error) { - customTable := c.CustomTable - if customTable == "" { - customTable = c.LegacyCustomTable - } - customTables := c.CustomTables - if len(customTables) == 0 { - customTables = c.LegacyCustomSets - } - - paddingMin := c.PaddingMin - if paddingMin == 0 { - paddingMin = c.LegacyPaddingMin - } - paddingMax := c.PaddingMax - if paddingMax == 0 { - paddingMax = c.LegacyPaddingMax - } - - return &finalsudoku.Config{ - Password: c.Password, - Ascii: c.ASCII, - CustomTable: customTable, - CustomTables: customTables, - PaddingMin: paddingMin, - PaddingMax: paddingMax, - }, nil -} - -type Xdns struct { - Domain json.RawMessage `json:"domain"` - - Domains []string `json:"domains"` - Resolvers []string `json:"resolvers"` -} - -func (c *Xdns) Build() (proto.Message, error) { - if c.Domain != nil { - return nil, errors.PrintRemovedFeatureError("domain", "domains(server) & resolvers(client)") - } - - if len(c.Domains) == 0 && len(c.Resolvers) == 0 { - return nil, errors.New("empty domains & empty resolvers") - } - - for _, r := range c.Resolvers { - if !strings.Contains(r, "+udp://") { - return nil, errors.New("invalid resolver ", r) - } - } - - return &xdns.Config{ - Domains: c.Domains, - Resolvers: c.Resolvers, - }, nil -} - -type Xicmp struct { - DGRAM bool `json:"dgram"` - IPs []string `json:"ips"` -} - -func (c *Xicmp) Build() (proto.Message, error) { - for _, ip := range c.IPs { - if _, err := netip.ParseAddr(ip); err != nil { - return nil, err - } - } - - config := &xicmp.Config{ - DGRAM: c.DGRAM, - IPs: c.IPs, - } - - return config, nil -} - -type Realm struct { - Url string `json:"url"` - StunServers []string `json:"stunServers"` - TlsConfig *TLSConfig `json:"tlsConfig"` -} - -func (c *Realm) Build() (proto.Message, error) { - var scheme, host, port, token, id string - var stunServers []string - var tlsConfig *tls.Config - - u, err := url.Parse(c.Url) - if err != nil { - return nil, err - } - - switch u.Scheme { - case "realm": - scheme = "https" - case "realm+http": - scheme = "http" - default: - return nil, errors.New("invalid scheme", u.Scheme) - } - - host = u.Hostname() - if host == "" { - return nil, errors.New("invalid host", host) - } - - port = u.Port() - if port == "" { - port = "443" - if scheme == "http" { - port = "80" - } - } - - token, err = url.PathUnescape(u.User.String()) - if err != nil { - return nil, err - } - if token == "" { - return nil, errors.New("invalid token", token) - } - - id, err = url.PathUnescape(strings.TrimPrefix(u.EscapedPath(), "/")) - if err != nil { - return nil, err - } - if id == "" { - return nil, errors.New("invalid id", id) - } - - if len(c.StunServers) == 0 { - return nil, errors.New("empty stunServers") - } - - for _, s := range c.StunServers { - _, _, err = net.SplitHostPort(s) - if err != nil { - return nil, err - } - } - - stunServers = c.StunServers - - if c.TlsConfig != nil { - tc, err := c.TlsConfig.Build() - if err != nil { - return nil, err - } - tlsConfig = tc.(*tls.Config) - } - - return &realm.Config{ - Scheme: scheme, - Host: host, - Port: port, - Token: token, - ID: id, - StunServers: stunServers, - TlsConfig: tlsConfig, - }, nil -} - -type Mask struct { - Type string `json:"type"` - Settings *json.RawMessage `json:"settings"` -} - -func (c *Mask) Build(tcp bool) (proto.Message, error) { - loader := udpmaskLoader - if tcp { - loader = tcpmaskLoader - } - - settings := []byte("{}") - if c.Settings != nil { - settings = ([]byte)(*c.Settings) - } - rawConfig, err := loader.LoadWithID(settings, c.Type) - if err != nil { - return nil, err - } - ts, err := rawConfig.(Buildable).Build() - if err != nil { - return nil, err - } - return ts, nil -} - -type FinalMask struct { - Tcp []Mask `json:"tcp"` - Udp []Mask `json:"udp"` - QuicParams *QuicParamsConfig `json:"quicParams"` -} - type StreamConfig struct { Address *Address `json:"address"` Port uint16 `json:"port"` + Method *TransportProtocol `json:"method"` Network *TransportProtocol `json:"network"` Security string `json:"security"` FinalMask *FinalMask `json:"finalmask"` @@ -2063,6 +71,9 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { if c.Address != nil { config.Address = c.Address.Build() } + if c.Method != nil { + c.Network = c.Method + } if c.Network != nil { protocol, err := c.Network.Build() if err != nil { diff --git a/infra/conf/transport_method.go b/infra/conf/transport_method.go new file mode 100644 index 000000000..2b3a238d3 --- /dev/null +++ b/infra/conf/transport_method.go @@ -0,0 +1,814 @@ +package conf + +import ( + "context" + "encoding/json" + "math/big" + "net/url" + "sort" + "strconv" + "strings" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform/filesystem" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/grpc" + "github.com/xtls/xray-core/transport/internet/headers/http" + "github.com/xtls/xray-core/transport/internet/headers/noop" + "github.com/xtls/xray-core/transport/internet/httpupgrade" + "github.com/xtls/xray-core/transport/internet/hysteria" + "github.com/xtls/xray-core/transport/internet/kcp" + "github.com/xtls/xray-core/transport/internet/splithttp" + "github.com/xtls/xray-core/transport/internet/tcp" + "github.com/xtls/xray-core/transport/internet/websocket" + "google.golang.org/protobuf/proto" +) + +type NoOpConnectionAuthenticator struct{} + +func (NoOpConnectionAuthenticator) Build() (proto.Message, error) { + return new(noop.ConnectionConfig), nil +} + +type AuthenticatorRequest struct { + Version string `json:"version"` + Method string `json:"method"` + Path StringList `json:"path"` + Headers map[string]*StringList `json:"headers"` +} + +func sortMapKeys(m map[string]*StringList) []string { + var keys []string + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) { + config := &http.RequestConfig{ + Uri: []string{"/"}, + Header: []*http.Header{ + { + Name: "Host", + Value: []string{"www.baidu.com", "www.bing.com"}, + }, + { + Name: "User-Agent", + Value: []string{utils.ChromeUA}, + }, + { + Name: "Sec-CH-UA", + Value: []string{utils.ChromeUACH}, + }, + { + Name: "Sec-CH-UA-Mobile", + Value: []string{"?0"}, + }, + { + Name: "Sec-CH-UA-Platform", + Value: []string{"Windows"}, + }, + { + Name: "Sec-Fetch-Mode", + Value: []string{"no-cors", "cors", "same-origin"}, + }, + { + Name: "Sec-Fetch-Dest", + Value: []string{"empty"}, + }, + { + Name: "Sec-Fetch-Site", + Value: []string{"none"}, + }, + { + Name: "Sec-Fetch-User", + Value: []string{"?1"}, + }, + { + Name: "Accept-Encoding", + Value: []string{"gzip, deflate"}, + }, + { + Name: "Connection", + Value: []string{"keep-alive"}, + }, + { + Name: "Pragma", + Value: []string{"no-cache"}, + }, + }, + } + + if len(v.Version) > 0 { + config.Version = &http.Version{Value: v.Version} + } + + if len(v.Method) > 0 { + config.Method = &http.Method{Value: v.Method} + } + + if len(v.Path) > 0 { + config.Uri = append([]string(nil), v.Path...) + } + + if len(v.Headers) > 0 { + config.Header = make([]*http.Header, 0, len(v.Headers)) + headerNames := sortMapKeys(v.Headers) + for _, key := range headerNames { + value := v.Headers[key] + if value == nil { + return nil, errors.New("empty HTTP header value: " + key).AtError() + } + config.Header = append(config.Header, &http.Header{ + Name: key, + Value: append([]string(nil), (*value)...), + }) + } + } + + return config, nil +} + +type AuthenticatorResponse struct { + Version string `json:"version"` + Status string `json:"status"` + Reason string `json:"reason"` + Headers map[string]*StringList `json:"headers"` +} + +func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) { + config := &http.ResponseConfig{ + Header: []*http.Header{ + { + Name: "Content-Type", + Value: []string{"application/octet-stream", "video/mpeg"}, + }, + { + Name: "Transfer-Encoding", + Value: []string{"chunked"}, + }, + { + Name: "Connection", + Value: []string{"keep-alive"}, + }, + { + Name: "Pragma", + Value: []string{"no-cache"}, + }, + { + Name: "Cache-Control", + Value: []string{"private", "no-cache"}, + }, + }, + } + + if len(v.Version) > 0 { + config.Version = &http.Version{Value: v.Version} + } + + if len(v.Status) > 0 || len(v.Reason) > 0 { + config.Status = &http.Status{ + Code: "200", + Reason: "OK", + } + if len(v.Status) > 0 { + config.Status.Code = v.Status + } + if len(v.Reason) > 0 { + config.Status.Reason = v.Reason + } + } + + if len(v.Headers) > 0 { + config.Header = make([]*http.Header, 0, len(v.Headers)) + headerNames := sortMapKeys(v.Headers) + for _, key := range headerNames { + value := v.Headers[key] + if value == nil { + return nil, errors.New("empty HTTP header value: " + key).AtError() + } + config.Header = append(config.Header, &http.Header{ + Name: key, + Value: append([]string(nil), (*value)...), + }) + } + } + + return config, nil +} + +type Authenticator struct { + Request AuthenticatorRequest `json:"request"` + Response AuthenticatorResponse `json:"response"` +} + +func (v *Authenticator) Build() (proto.Message, error) { + config := new(http.Config) + requestConfig, err := v.Request.Build() + if err != nil { + return nil, err + } + config.Request = requestConfig + + responseConfig, err := v.Response.Build() + if err != nil { + return nil, err + } + config.Response = responseConfig + + return config, nil +} + +var tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "none": func() interface{} { return new(NoOpConnectionAuthenticator) }, + "http": func() interface{} { return new(Authenticator) }, +}, "type", "") + +type TCPConfig struct { + HeaderConfig json.RawMessage `json:"header"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` +} + +// Build implements Buildable. +func (c *TCPConfig) Build() (proto.Message, error) { + config := new(tcp.Config) + if len(c.HeaderConfig) > 0 { + headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig) + if err != nil { + return nil, errors.New("invalid TCP header config").Base(err).AtError() + } + ts, err := headerConfig.(Buildable).Build() + if err != nil { + return nil, errors.New("invalid TCP header config").Base(err).AtError() + } + config.HeaderSettings = serial.ToTypedMessage(ts) + } + if c.AcceptProxyProtocol { + config.AcceptProxyProtocol = c.AcceptProxyProtocol + } + return config, nil +} + +type SplitHTTPConfig struct { + Host string `json:"host"` + Path string `json:"path"` + Mode string `json:"mode"` + Headers map[string]string `json:"headers"` + XPaddingBytes Int32Range `json:"xPaddingBytes"` + XPaddingObfsMode bool `json:"xPaddingObfsMode"` + XPaddingKey string `json:"xPaddingKey"` + XPaddingHeader string `json:"xPaddingHeader"` + XPaddingPlacement string `json:"xPaddingPlacement"` + XPaddingMethod string `json:"xPaddingMethod"` + UplinkHTTPMethod string `json:"uplinkHTTPMethod"` + SessionIDPlacement string `json:"sessionIDPlacement"` + SessionIDKey string `json:"sessionIDKey"` + SessionIDTable string `json:"sessionIDTable"` + SessionIDLength Int32Range `json:"sessionIDLength"` + SeqPlacement string `json:"seqPlacement"` + SeqKey string `json:"seqKey"` + UplinkDataPlacement string `json:"uplinkDataPlacement"` + UplinkDataKey string `json:"uplinkDataKey"` + UplinkChunkSize Int32Range `json:"uplinkChunkSize"` + NoGRPCHeader bool `json:"noGRPCHeader"` + NoSSEHeader bool `json:"noSSEHeader"` + ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"` + ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"` + ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"` + ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"` + ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"` + Xmux XmuxConfig `json:"xmux"` + DownloadSettings *StreamConfig `json:"downloadSettings"` + Extra json.RawMessage `json:"extra"` +} + +type XmuxConfig struct { + MaxConcurrency Int32Range `json:"maxConcurrency"` + MaxConnections Int32Range `json:"maxConnections"` + CMaxReuseTimes Int32Range `json:"cMaxReuseTimes"` + HMaxRequestTimes Int32Range `json:"hMaxRequestTimes"` + HMaxReusableSecs Int32Range `json:"hMaxReusableSecs"` + HKeepAlivePeriod int64 `json:"hKeepAlivePeriod"` +} + +func newRangeConfig(input Int32Range) *splithttp.RangeConfig { + return &splithttp.RangeConfig{ + From: input.From, + To: input.To, + } +} + +// Build implements Buildable. +func (c *SplitHTTPConfig) Build() (proto.Message, error) { + if c.Extra != nil { + var extra SplitHTTPConfig + if err := json.Unmarshal(c.Extra, &extra); err != nil { + return nil, errors.New(`Failed to unmarshal "extra".`).Base(err) + } + extra.Host = c.Host + extra.Path = c.Path + extra.Mode = c.Mode + c = &extra + } + + switch c.Mode { + case "": + c.Mode = "auto" + case "auto", "packet-up", "stream-up", "stream-one": + default: + return nil, errors.New("unsupported mode: " + c.Mode) + } + + // Priority (client): host > serverName > address + for k := range c.Headers { + if strings.ToLower(k) == "host" { + return nil, errors.New(`"headers" can't contain "host"`) + } + } + + if c.XPaddingBytes != (Int32Range{}) && (c.XPaddingBytes.From <= 0 || c.XPaddingBytes.To <= 0) { + return nil, errors.New("xPaddingBytes cannot be disabled") + } + + if c.XPaddingKey == "" { + c.XPaddingKey = "x_padding" + } + + if c.XPaddingHeader == "" { + c.XPaddingHeader = "X-Padding" + } + + switch c.XPaddingPlacement { + case "": + c.XPaddingPlacement = "queryInHeader" + case "cookie", "header", "query", "queryInHeader": + default: + return nil, errors.New("unsupported padding placement: " + c.XPaddingPlacement) + } + + switch c.XPaddingMethod { + case "": + c.XPaddingMethod = "repeat-x" + case "repeat-x", "tokenish": + default: + return nil, errors.New("unsupported padding method: " + c.XPaddingMethod) + } + + switch c.UplinkDataPlacement { + case "": + c.UplinkDataPlacement = splithttp.PlacementAuto + case splithttp.PlacementAuto, splithttp.PlacementBody: + case splithttp.PlacementCookie, splithttp.PlacementHeader: + if c.Mode != "packet-up" { + return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode") + } + default: + return nil, errors.New("unsupported uplink data placement: " + c.UplinkDataPlacement) + } + + if c.UplinkHTTPMethod == "" { + c.UplinkHTTPMethod = "POST" + } + c.UplinkHTTPMethod = strings.ToUpper(c.UplinkHTTPMethod) + + if c.UplinkHTTPMethod == "GET" && c.Mode != "packet-up" { + return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode") + } + + switch c.SessionIDPlacement { + case "": + c.SessionIDPlacement = "path" + case "path", "cookie", "header", "query": + default: + return nil, errors.New("unsupported session placement: " + c.SessionIDPlacement) + } + + switch c.SeqPlacement { + case "": + c.SeqPlacement = "path" + case "path", "cookie", "header", "query": + default: + return nil, errors.New("unsupported seq placement: " + c.SeqPlacement) + } + + if c.SessionIDPlacement != "path" && c.SessionIDKey == "" { + switch c.SessionIDPlacement { + case "cookie", "query": + c.SessionIDKey = "x_session" + case "header": + c.SessionIDKey = "X-Session" + } + } + + if c.SessionIDTable != "" { + if predefined, ok := splithttp.PredefinedTable[c.SessionIDTable]; ok { + c.SessionIDTable = predefined + } + room := roomSize(len(c.SessionIDTable), c.SessionIDLength.From, c.SessionIDLength.To) + // 2.1B possiblities should be enough + if room.Cmp(big.NewInt(2<<30)) < 0 { + return nil, errors.New("sessionIDTable or sessionIDLength is too small") + } + if c.SessionIDLength.From <= 0 { + return nil, errors.New("sessionIDLength.from must be greater than 0") + } + for i := 0; i < len(c.SessionIDTable); i++ { + if c.SessionIDTable[i] >= 0x80 { + return nil, errors.New("sessionIDTable must contain only ASCII characters") + } + } + } + + if c.SeqPlacement != "path" && c.SeqKey == "" { + switch c.SeqPlacement { + case "cookie", "query": + c.SeqKey = "x_seq" + case "header": + c.SeqKey = "X-Seq" + } + } + + if c.UplinkDataPlacement != splithttp.PlacementBody && c.UplinkDataKey == "" { + switch c.UplinkDataPlacement { + case splithttp.PlacementCookie: + c.UplinkDataKey = "x_data" + case splithttp.PlacementAuto, splithttp.PlacementHeader: + c.UplinkDataKey = "X-Data" + } + } + + if c.ServerMaxHeaderBytes < 0 { + return nil, errors.New("invalid negative value of maxHeaderBytes") + } + + if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 { + return nil, errors.New("maxConnections cannot be specified together with maxConcurrency") + } + if c.Xmux == (XmuxConfig{}) { + c.Xmux.MaxConnections.From = 6 + c.Xmux.MaxConnections.To = 6 + c.Xmux.HMaxRequestTimes.From = 600 + c.Xmux.HMaxRequestTimes.To = 900 + c.Xmux.HMaxReusableSecs.From = 1800 + c.Xmux.HMaxReusableSecs.To = 3000 + } + + config := &splithttp.Config{ + Host: c.Host, + Path: c.Path, + Mode: c.Mode, + Headers: c.Headers, + XPaddingBytes: newRangeConfig(c.XPaddingBytes), + XPaddingObfsMode: c.XPaddingObfsMode, + XPaddingKey: c.XPaddingKey, + XPaddingHeader: c.XPaddingHeader, + XPaddingPlacement: c.XPaddingPlacement, + XPaddingMethod: c.XPaddingMethod, + UplinkHTTPMethod: c.UplinkHTTPMethod, + SessionIDPlacement: c.SessionIDPlacement, + SeqPlacement: c.SeqPlacement, + SessionIDKey: c.SessionIDKey, + SeqKey: c.SeqKey, + UplinkDataPlacement: c.UplinkDataPlacement, + UplinkDataKey: c.UplinkDataKey, + UplinkChunkSize: newRangeConfig(c.UplinkChunkSize), + NoGRPCHeader: c.NoGRPCHeader, + NoSSEHeader: c.NoSSEHeader, + ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes), + ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs), + ScMaxBufferedPosts: c.ScMaxBufferedPosts, + ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs), + ServerMaxHeaderBytes: c.ServerMaxHeaderBytes, + SessionIDTable: c.SessionIDTable, + SessionIDLength: newRangeConfig(c.SessionIDLength), + Xmux: &splithttp.XmuxConfig{ + MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency), + MaxConnections: newRangeConfig(c.Xmux.MaxConnections), + CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes), + HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes), + HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs), + HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod, + }, + } + + if c.DownloadSettings != nil { + if c.Mode == "stream-one" { + return nil, errors.New(`Can not use "downloadSettings" in "stream-one" mode.`) + } + var err error + if config.DownloadSettings, err = c.DownloadSettings.Build(); err != nil { + return nil, errors.New(`Failed to build "downloadSettings".`).Base(err) + } + } + + return config, nil +} + +func roomSize(tableSize int, min, max int32) *big.Int { + base := big.NewInt(int64(tableSize)) + sum := new(big.Int) + term := new(big.Int) + for k := min; k <= max; k++ { + term.Exp(base, big.NewInt(int64(k)), nil) + sum.Add(sum, term) + } + return sum +} + +type KCPConfig struct { + Mtu *uint32 `json:"mtu"` + Tti *uint32 `json:"tti"` + UpCap *uint32 `json:"uplinkCapacity"` + DownCap *uint32 `json:"downlinkCapacity"` + CwndMultiplier *uint32 `json:"cwndMultiplier"` + MaxSendingWindow *uint32 `json:"maxSendingWindow"` + + HeaderConfig json.RawMessage `json:"header"` + Seed *string `json:"seed"` +} + +// Build implements Buildable. +func (c *KCPConfig) Build() (proto.Message, error) { + if c.HeaderConfig != nil || c.Seed != nil { + return nil, errors.PrintRemovedFeatureError("mkcp header & seed", "finalmask/udp header-* & mkcp-original & mkcp-aes128gcm") + } + + config := common.Must2(internet.CreateTransportConfig(kcp.ProtocolName)).(*kcp.Config) + + if c.Mtu != nil { + config.Mtu = *c.Mtu + } + if c.Tti != nil { + config.Tti = *c.Tti + } + if c.UpCap != nil { + config.UplinkCapacity = *c.UpCap + } + if c.DownCap != nil { + config.DownlinkCapacity = *c.DownCap + } + if c.CwndMultiplier != nil { + config.CwndMultiplier = *c.CwndMultiplier + } + if c.MaxSendingWindow != nil { + config.MaxSendingWindow = *c.MaxSendingWindow + } + + if config.Mtu < 21 { + return nil, errors.New("Mtu must be at least 21").AtError() + } + if config.Tti < 10 || config.Tti > 1000 { + return nil, errors.New("invalid mKCP TTI: ", c.Tti).AtError() + } + if config.CwndMultiplier < 1 { + return nil, errors.New("CwndMultiplier must be at least 1").AtError() + } + if config.GetSendingBufferSize() == 0 { + return nil, errors.New("MaxSendingWindow must be >= Mtu").AtError() + } + + return config, nil +} + +type GRPCConfig struct { + Authority string `json:"authority"` + ServiceName string `json:"serviceName"` + MultiMode bool `json:"multiMode"` + IdleTimeout int32 `json:"idle_timeout"` + HealthCheckTimeout int32 `json:"health_check_timeout"` + PermitWithoutStream bool `json:"permit_without_stream"` + InitialWindowsSize int32 `json:"initial_windows_size"` + UserAgent string `json:"user_agent"` +} + +func (g *GRPCConfig) Build() (proto.Message, error) { + if g.IdleTimeout <= 0 { + g.IdleTimeout = 0 + } + if g.HealthCheckTimeout <= 0 { + g.HealthCheckTimeout = 0 + } + if g.InitialWindowsSize < 0 { + // default window size of gRPC-go + g.InitialWindowsSize = 0 + } + + return &grpc.Config{ + Authority: g.Authority, + ServiceName: g.ServiceName, + MultiMode: g.MultiMode, + IdleTimeout: g.IdleTimeout, + HealthCheckTimeout: g.HealthCheckTimeout, + PermitWithoutStream: g.PermitWithoutStream, + InitialWindowsSize: g.InitialWindowsSize, + UserAgent: g.UserAgent, + }, nil +} + +type WebSocketConfig struct { + Host string `json:"host"` + Path string `json:"path"` + Headers map[string]string `json:"headers"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` + HeartbeatPeriod uint32 `json:"heartbeatPeriod"` +} + +// Build implements Buildable. +func (c *WebSocketConfig) Build() (proto.Message, error) { + path := c.Path + var ed uint32 + if u, err := url.Parse(path); err == nil { + if q := u.Query(); q.Get("ed") != "" { + Ed, _ := strconv.Atoi(q.Get("ed")) + ed = uint32(Ed) + q.Del("ed") + u.RawQuery = q.Encode() + path = u.String() + } + } + // Priority (client): host > serverName > address + for k, v := range c.Headers { + if strings.ToLower(k) == "host" { + errors.PrintDeprecatedFeatureWarning(`"host" in "headers"`, `independent "host"`) + if c.Host == "" { + c.Host = v + } + delete(c.Headers, k) + } + } + config := &websocket.Config{ + Path: path, + Host: c.Host, + Header: c.Headers, + AcceptProxyProtocol: c.AcceptProxyProtocol, + Ed: ed, + HeartbeatPeriod: c.HeartbeatPeriod, + } + return config, nil +} + +type HttpUpgradeConfig struct { + Host string `json:"host"` + Path string `json:"path"` + Headers map[string]string `json:"headers"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` +} + +// Build implements Buildable. +func (c *HttpUpgradeConfig) Build() (proto.Message, error) { + path := c.Path + var ed uint32 + if u, err := url.Parse(path); err == nil { + if q := u.Query(); q.Get("ed") != "" { + Ed, _ := strconv.Atoi(q.Get("ed")) + ed = uint32(Ed) + q.Del("ed") + u.RawQuery = q.Encode() + path = u.String() + } + } + // Priority (client): host > serverName > address + for k := range c.Headers { + if strings.ToLower(k) == "host" { + return nil, errors.New(`"headers" can't contain "host"`) + } + } + config := &httpupgrade.Config{ + Path: path, + Host: c.Host, + Header: c.Headers, + AcceptProxyProtocol: c.AcceptProxyProtocol, + Ed: ed, + } + return config, nil +} + +const ( + Byte = 1 + Kilobyte = 1024 * Byte + Megabyte = 1024 * Kilobyte + Gigabyte = 1024 * Megabyte + Terabyte = 1024 * Gigabyte +) + +type Bandwidth string + +func (b Bandwidth) Bps() (uint64, error) { + s := strings.TrimSpace(strings.ToLower(string(b))) + if s == "" { + return 0, nil + } + + idx := len(s) + for i, c := range s { + if (c < '0' || c > '9') && c != '.' { + idx = i + break + } + } + + numStr := s[:idx] + unit := strings.TrimSpace(s[idx:]) + + val, err := strconv.ParseFloat(numStr, 64) + if err != nil { + return 0, err + } + + mul := uint64(1) + switch unit { + case "", "b", "bps": + mul = Byte + case "k", "kb", "kbps": + mul = Kilobyte + case "m", "mb", "mbps": + mul = Megabyte + case "g", "gb", "gbps": + mul = Gigabyte + case "t", "tb", "tbps": + mul = Terabyte + default: + return 0, errors.New("unsupported unit: " + unit) + } + + return uint64(val*float64(mul)) / 8, nil +} + +type UdpHop struct { + PortList PortList `json:"ports"` + Interval Int32Range `json:"interval"` +} + +type Masquerade struct { + Type string `json:"type"` + + Dir string `json:"dir"` + + Url string `json:"url"` + RewriteHost bool `json:"rewriteHost"` + Insecure bool `json:"insecure"` + + Content string `json:"content"` + Headers map[string]string `json:"headers"` + StatusCode int32 `json:"statusCode"` +} + +type HysteriaConfig struct { + Version int32 `json:"version"` + Auth string `json:"auth"` + + Congestion *string `json:"congestion"` + Up *Bandwidth `json:"up"` + Down *Bandwidth `json:"down"` + UdpHop *UdpHop `json:"udphop"` + + UdpIdleTimeout int64 `json:"udpIdleTimeout"` + Masquerade Masquerade `json:"masquerade"` +} + +func (c *HysteriaConfig) Build() (proto.Message, error) { + if c.Version != 2 { + return nil, errors.New("version != 2") + } + + if c.Congestion != nil || c.Up != nil || c.Down != nil || c.UdpHop != nil { + errors.LogWarning(context.Background(), "congestion & up & down & udphop move to finalmask/quicParams") + } + + if c.UdpIdleTimeout != 0 && (c.UdpIdleTimeout < 2 || c.UdpIdleTimeout > 600) { + return nil, errors.New("UdpIdleTimeout must be between 2 and 600") + } + + config := &hysteria.Config{} + config.Auth = c.Auth + config.UdpIdleTimeout = c.UdpIdleTimeout + config.MasqType = c.Masquerade.Type + config.MasqFile = c.Masquerade.Dir + config.MasqUrl = c.Masquerade.Url + config.MasqUrlRewriteHost = c.Masquerade.RewriteHost + config.MasqUrlInsecure = c.Masquerade.Insecure + config.MasqString = c.Masquerade.Content + config.MasqStringHeaders = c.Masquerade.Headers + config.MasqStringStatusCode = c.Masquerade.StatusCode + + if config.UdpIdleTimeout == 0 { + config.UdpIdleTimeout = 60 + } + + return config, nil +} + +func readFileOrString(f string, s []string) ([]byte, error) { + if len(f) > 0 { + return filesystem.ReadCert(f) + } + if len(s) > 0 { + return []byte(strings.Join(s, "\n")), nil + } + return nil, errors.New("both file and bytes are empty.") +} diff --git a/infra/conf/transport_security.go b/infra/conf/transport_security.go new file mode 100644 index 000000000..7d0e0529d --- /dev/null +++ b/infra/conf/transport_security.go @@ -0,0 +1,402 @@ +package conf + +import ( + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "net/url" + "runtime" + "strconv" + "strings" + "syscall" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/tls" + "google.golang.org/protobuf/proto" +) + +type LimitFallback struct { + AfterBytes uint64 + BytesPerSec uint64 + BurstBytesPerSec uint64 +} + +type REALITYConfig struct { + MasterKeyLog string `json:"masterKeyLog"` + Show bool `json:"show"` + Target json.RawMessage `json:"target"` + Dest json.RawMessage `json:"dest"` + Type string `json:"type"` + Xver uint64 `json:"xver"` + ServerNames []string `json:"serverNames"` + PrivateKey string `json:"privateKey"` + MinClientVer string `json:"minClientVer"` + MaxClientVer string `json:"maxClientVer"` + MaxTimeDiff uint64 `json:"maxTimeDiff"` + ShortIds []string `json:"shortIds"` + Mldsa65Seed string `json:"mldsa65Seed"` + + LimitFallbackUpload LimitFallback `json:"limitFallbackUpload"` + LimitFallbackDownload LimitFallback `json:"limitFallbackDownload"` + + Fingerprint string `json:"fingerprint"` + ServerName string `json:"serverName"` + Password string `json:"password"` + PublicKey string `json:"publicKey"` + ShortId string `json:"shortId"` + Mldsa65Verify string `json:"mldsa65Verify"` + SpiderX string `json:"spiderX"` +} + +func (c *REALITYConfig) Build() (proto.Message, error) { + config := new(reality.Config) + config.MasterKeyLog = c.MasterKeyLog + config.Show = c.Show + var err error + if c.Target != nil { + c.Dest = c.Target + } + if c.Dest != nil { + var i uint16 + var s string + if err = json.Unmarshal(c.Dest, &i); err == nil { + s = strconv.Itoa(int(i)) + } else { + _ = json.Unmarshal(c.Dest, &s) + } + if c.Type == "" && s != "" { + switch s[0] { + case '@', '/': + c.Type = "unix" + if s[0] == '@' && len(s) > 1 && s[1] == '@' && (runtime.GOOS == "linux" || runtime.GOOS == "android") { + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy + copy(fullAddr, s[1:]) + s = string(fullAddr) + } + default: + if _, err = strconv.Atoi(s); err == nil { + s = "localhost:" + s + } + if _, _, err = net.SplitHostPort(s); err == nil { + c.Type = "tcp" + } + } + } + if c.Type == "" { + return nil, errors.New(`please fill in a valid value for "target"`) + } + if c.Xver > 2 { + return nil, errors.New(`invalid PROXY protocol version, "xver" only accepts 0, 1, 2`) + } + if len(c.ServerNames) == 0 { + return nil, errors.New(`empty "serverNames"`) + } + if c.PrivateKey == "" { + return nil, errors.New(`empty "privateKey"`) + } + if config.PrivateKey, err = base64.RawURLEncoding.DecodeString(c.PrivateKey); err != nil || len(config.PrivateKey) != 32 { + return nil, errors.New(`invalid "privateKey": `, c.PrivateKey) + } + if c.MinClientVer != "" { + config.MinClientVer = make([]byte, 3) + var u uint64 + for i, s := range strings.Split(c.MinClientVer, ".") { + if i == 3 { + return nil, errors.New(`invalid "minClientVer": `, c.MinClientVer) + } + if u, err = strconv.ParseUint(s, 10, 8); err != nil { + return nil, errors.New(`"minClientVer[`, i, `]" should be less than 256`) + } else { + config.MinClientVer[i] = byte(u) + } + } + } + if c.MaxClientVer != "" { + config.MaxClientVer = make([]byte, 3) + var u uint64 + for i, s := range strings.Split(c.MaxClientVer, ".") { + if i == 3 { + return nil, errors.New(`invalid "maxClientVer": `, c.MaxClientVer) + } + if u, err = strconv.ParseUint(s, 10, 8); err != nil { + return nil, errors.New(`"maxClientVer[`, i, `]" should be less than 256`) + } else { + config.MaxClientVer[i] = byte(u) + } + } + } + if len(c.ShortIds) == 0 { + return nil, errors.New(`empty "shortIds"`) + } + config.ShortIds = make([][]byte, len(c.ShortIds)) + for i, s := range c.ShortIds { + if len(s) > 16 { + return nil, errors.New(`too long "shortIds[`, i, `]": `, s) + } + config.ShortIds[i] = make([]byte, 8) + if _, err = hex.Decode(config.ShortIds[i], []byte(s)); err != nil { + return nil, errors.New(`invalid "shortIds[`, i, `]": `, s) + } + } + config.Dest = s + config.Type = c.Type + config.Xver = c.Xver + config.ServerNames = c.ServerNames + config.MaxTimeDiff = c.MaxTimeDiff + + if c.Mldsa65Seed != "" { + if c.Mldsa65Seed == c.PrivateKey { + return nil, errors.New(`"mldsa65Seed" and "privateKey" can not be the same value: `, c.Mldsa65Seed) + } + if config.Mldsa65Seed, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Seed); err != nil || len(config.Mldsa65Seed) != 32 { + return nil, errors.New(`invalid "mldsa65Seed": `, c.Mldsa65Seed) + } + } + + for _, sn := range config.ServerNames { + if strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") { + errors.LogWarning(context.Background(), `REALITY: Choosing apple, icloud, etc. as the target may get your IP blocked by the GFW`) + } + } + + config.LimitFallbackUpload = new(reality.LimitFallback) + config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes + config.LimitFallbackUpload.BytesPerSec = c.LimitFallbackUpload.BytesPerSec + config.LimitFallbackUpload.BurstBytesPerSec = c.LimitFallbackUpload.BurstBytesPerSec + config.LimitFallbackDownload = new(reality.LimitFallback) + config.LimitFallbackDownload.AfterBytes = c.LimitFallbackDownload.AfterBytes + config.LimitFallbackDownload.BytesPerSec = c.LimitFallbackDownload.BytesPerSec + config.LimitFallbackDownload.BurstBytesPerSec = c.LimitFallbackDownload.BurstBytesPerSec + } else { + config.Fingerprint = strings.ToLower(c.Fingerprint) + if config.Fingerprint == "unsafe" || config.Fingerprint == "hellogolang" { + return nil, errors.New(`invalid "fingerprint": `, config.Fingerprint) + } + if tls.GetFingerprint(config.Fingerprint) == nil { + return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint) + } + if len(c.ServerNames) != 0 { + return nil, errors.New(`non-empty "serverNames", please use "serverName" instead`) + } + if c.Password != "" { + c.PublicKey = c.Password + } + if c.PublicKey == "" { + return nil, errors.New(`empty "password"`) + } + if config.PublicKey, err = base64.RawURLEncoding.DecodeString(c.PublicKey); err != nil || len(config.PublicKey) != 32 { + return nil, errors.New(`invalid "password": `, c.PublicKey) + } + if len(c.ShortIds) != 0 { + return nil, errors.New(`non-empty "shortIds", please use "shortId" instead`) + } + if len(c.ShortId) > 16 { + return nil, errors.New(`too long "shortId": `, c.ShortId) + } + config.ShortId = make([]byte, 8) + if _, err = hex.Decode(config.ShortId, []byte(c.ShortId)); err != nil { + return nil, errors.New(`invalid "shortId": `, c.ShortId) + } + if c.Mldsa65Verify != "" { + if config.Mldsa65Verify, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Verify); err != nil || len(config.Mldsa65Verify) != 1952 { + return nil, errors.New(`invalid "mldsa65Verify": `, c.Mldsa65Verify) + } + } + if c.SpiderX == "" { + c.SpiderX = "/" + } + if c.SpiderX[0] != '/' { + return nil, errors.New(`invalid "spiderX": `, c.SpiderX) + } + config.SpiderY = make([]int64, 10) + u, _ := url.Parse(c.SpiderX) + q := u.Query() + parse := func(param string, index int) { + if q.Get(param) != "" { + s := strings.Split(q.Get(param), "-") + if len(s) == 1 { + config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64) + config.SpiderY[index+1], _ = strconv.ParseInt(s[0], 10, 64) + } else { + config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64) + config.SpiderY[index+1], _ = strconv.ParseInt(s[1], 10, 64) + } + } + q.Del(param) + } + parse("p", 0) // padding + parse("c", 2) // concurrency + parse("t", 4) // times + parse("i", 6) // interval + parse("r", 8) // return + u.RawQuery = q.Encode() + config.SpiderX = u.String() + config.ServerName = c.ServerName + } + return config, nil +} + +type TLSCertConfig struct { + CertFile string `json:"certificateFile"` + CertStr []string `json:"certificate"` + KeyFile string `json:"keyFile"` + KeyStr []string `json:"key"` + Usage string `json:"usage"` + OcspStapling uint64 `json:"ocspStapling"` + OneTimeLoading bool `json:"oneTimeLoading"` + BuildChain bool `json:"buildChain"` +} + +// Build implements Buildable. +func (c *TLSCertConfig) Build() (*tls.Certificate, error) { + certificate := new(tls.Certificate) + + cert, err := readFileOrString(c.CertFile, c.CertStr) + if err != nil { + return nil, errors.New("failed to parse certificate").Base(err) + } + certificate.Certificate = cert + certificate.CertificatePath = c.CertFile + + if len(c.KeyFile) > 0 || len(c.KeyStr) > 0 { + key, err := readFileOrString(c.KeyFile, c.KeyStr) + if err != nil { + return nil, errors.New("failed to parse key").Base(err) + } + certificate.Key = key + certificate.KeyPath = c.KeyFile + } + + switch strings.ToLower(c.Usage) { + case "encipherment": + certificate.Usage = tls.Certificate_ENCIPHERMENT + case "verify": + certificate.Usage = tls.Certificate_AUTHORITY_VERIFY + case "issue": + certificate.Usage = tls.Certificate_AUTHORITY_ISSUE + default: + certificate.Usage = tls.Certificate_ENCIPHERMENT + } + if certificate.KeyPath == "" && certificate.CertificatePath == "" { + certificate.OneTimeLoading = true + } else { + certificate.OneTimeLoading = c.OneTimeLoading + } + certificate.OcspStapling = c.OcspStapling + certificate.BuildChain = c.BuildChain + + return certificate, nil +} + +type TLSConfig struct { + AllowInsecure bool `json:"allowInsecure"` + Certs []*TLSCertConfig `json:"certificates"` + ServerName string `json:"serverName"` + ALPN *StringList `json:"alpn"` + EnableSessionResumption bool `json:"enableSessionResumption"` + DisableSystemRoot bool `json:"disableSystemRoot"` + MinVersion string `json:"minVersion"` + MaxVersion string `json:"maxVersion"` + CipherSuites string `json:"cipherSuites"` + Fingerprint string `json:"fingerprint"` + RejectUnknownSNI bool `json:"rejectUnknownSni"` + CurvePreferences *StringList `json:"curvePreferences"` + MasterKeyLog string `json:"masterKeyLog"` + PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"` + VerifyPeerCertByName string `json:"verifyPeerCertByName"` + ECHServerKeys string `json:"echServerKeys"` + ECHConfigList string `json:"echConfigList"` + ECHSocketSettings *SocketConfig `json:"echSockopt"` +} + +// Build implements Buildable. +func (c *TLSConfig) Build() (proto.Message, error) { + config := new(tls.Config) + config.Certificate = make([]*tls.Certificate, len(c.Certs)) + for idx, certConf := range c.Certs { + cert, err := certConf.Build() + if err != nil { + return nil, err + } + config.Certificate[idx] = cert + } + serverName := c.ServerName + if len(c.ServerName) > 0 { + config.ServerName = serverName + } + if c.ALPN != nil && len(*c.ALPN) > 0 { + config.NextProtocol = []string(*c.ALPN) + } + if len(config.NextProtocol) > 1 { + for _, p := range config.NextProtocol { + if tls.IsFromMitm(p) { + return nil, errors.New(`only one element is allowed in "alpn" when using "fromMitm" in it`) + } + } + } + if c.CurvePreferences != nil && len(*c.CurvePreferences) > 0 { + config.CurvePreferences = []string(*c.CurvePreferences) + } + config.EnableSessionResumption = c.EnableSessionResumption + config.DisableSystemRoot = c.DisableSystemRoot + config.MinVersion = c.MinVersion + config.MaxVersion = c.MaxVersion + config.CipherSuites = c.CipherSuites + config.Fingerprint = strings.ToLower(c.Fingerprint) + if config.Fingerprint != "unsafe" && tls.GetFingerprint(config.Fingerprint) == nil { + return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint) + } + config.RejectUnknownSni = c.RejectUnknownSNI + config.MasterKeyLog = c.MasterKeyLog + + if c.AllowInsecure { + return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn)`) + } + if c.PinnedPeerCertSha256 != "" { + for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") { + v = strings.TrimSpace(v) + if v == "" { + continue + } + // remove colons for OpenSSL format + hashValue, err := hex.DecodeString(strings.ReplaceAll(v, ":", "")) + if err != nil { + return nil, err + } + if len(hashValue) != 32 { + return nil, errors.New("incorrect pinnedPeerCertSha256 length: ", v) + } + config.PinnedPeerCertSha256 = append(config.PinnedPeerCertSha256, hashValue) + } + } + if c.VerifyPeerCertByName != "" { + for v := range strings.SplitSeq(c.VerifyPeerCertByName, ",") { + v = strings.TrimSpace(v) + if v == "" { + continue + } + config.VerifyPeerCertByName = append(config.VerifyPeerCertByName, v) + } + } + + if c.ECHServerKeys != "" { + EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHServerKeys) + if err != nil { + return nil, errors.New("invalid ECH Config", c.ECHServerKeys) + } + config.EchServerKeys = EchPrivateKey + } + config.EchConfigList = c.ECHConfigList + if c.ECHSocketSettings != nil { + ss, err := c.ECHSocketSettings.Build() + if err != nil { + return nil, errors.New("Failed to build ech sockopt.").Base(err) + } + config.EchSocketSettings = ss + } + + return config, nil +} diff --git a/infra/conf/transport_sockopt.go b/infra/conf/transport_sockopt.go new file mode 100644 index 000000000..7001cd4d1 --- /dev/null +++ b/infra/conf/transport_sockopt.go @@ -0,0 +1,187 @@ +package conf + +import ( + "encoding/json" + "math" + "strings" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet" +) + +type CustomSockoptConfig struct { + Syetem string `json:"system"` + Network string `json:"network"` + Level string `json:"level"` + Opt string `json:"opt"` + Value string `json:"value"` + Type string `json:"type"` +} + +type HappyEyeballsConfig struct { + PrioritizeIPv6 bool `json:"prioritizeIPv6"` + TryDelayMs uint64 `json:"tryDelayMs"` + Interleave uint32 `json:"interleave"` + MaxConcurrentTry uint32 `json:"maxConcurrentTry"` +} + +func (h *HappyEyeballsConfig) UnmarshalJSON(data []byte) error { + innerHappyEyeballsConfig := struct { + PrioritizeIPv6 bool `json:"prioritizeIPv6"` + TryDelayMs uint64 `json:"tryDelayMs"` + Interleave uint32 `json:"interleave"` + MaxConcurrentTry uint32 `json:"maxConcurrentTry"` + }{PrioritizeIPv6: false, Interleave: 1, TryDelayMs: 0, MaxConcurrentTry: 4} + if err := json.Unmarshal(data, &innerHappyEyeballsConfig); err != nil { + return err + } + h.PrioritizeIPv6 = innerHappyEyeballsConfig.PrioritizeIPv6 + h.TryDelayMs = innerHappyEyeballsConfig.TryDelayMs + h.Interleave = innerHappyEyeballsConfig.Interleave + h.MaxConcurrentTry = innerHappyEyeballsConfig.MaxConcurrentTry + return nil +} + +type SocketConfig struct { + Mark int32 `json:"mark"` + TFO interface{} `json:"tcpFastOpen"` + TProxy string `json:"tproxy"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` + DomainStrategy string `json:"domainStrategy"` + DialerProxy string `json:"dialerProxy"` + TCPKeepAliveInterval int32 `json:"tcpKeepAliveInterval"` + TCPKeepAliveIdle int32 `json:"tcpKeepAliveIdle"` + TCPCongestion string `json:"tcpCongestion"` + TCPWindowClamp int32 `json:"tcpWindowClamp"` + TCPMaxSeg int32 `json:"tcpMaxSeg"` + Penetrate bool `json:"penetrate"` + TCPUserTimeout int32 `json:"tcpUserTimeout"` + V6only bool `json:"v6only"` + Interface string `json:"interface"` + TcpMptcp bool `json:"tcpMptcp"` + CustomSockopt []*CustomSockoptConfig `json:"customSockopt"` + AddressPortStrategy string `json:"addressPortStrategy"` + HappyEyeballsSettings *HappyEyeballsConfig `json:"happyEyeballs"` + TrustedXForwardedFor []string `json:"trustedXForwardedFor"` +} + +// Build implements Buildable. +func (c *SocketConfig) Build() (*internet.SocketConfig, error) { + tfo := int32(0) // don't invoke setsockopt() for TFO + if c.TFO != nil { + switch v := c.TFO.(type) { + case bool: + if v { + tfo = 256 + } else { + tfo = -1 // TFO need to be disabled + } + case float64: + tfo = int32(math.Min(v, math.MaxInt32)) + default: + return nil, errors.New("tcpFastOpen: only boolean and integer value is acceptable") + } + } + var tproxy internet.SocketConfig_TProxyMode + switch strings.ToLower(c.TProxy) { + case "tproxy": + tproxy = internet.SocketConfig_TProxy + case "redirect": + tproxy = internet.SocketConfig_Redirect + default: + tproxy = internet.SocketConfig_Off + } + + dStrategy := internet.DomainStrategy_AS_IS + switch strings.ToLower(c.DomainStrategy) { + case "asis", "": + dStrategy = internet.DomainStrategy_AS_IS + case "useip": + dStrategy = internet.DomainStrategy_USE_IP + case "useipv4": + dStrategy = internet.DomainStrategy_USE_IP4 + case "useipv6": + dStrategy = internet.DomainStrategy_USE_IP6 + case "useipv4v6": + dStrategy = internet.DomainStrategy_USE_IP46 + case "useipv6v4": + dStrategy = internet.DomainStrategy_USE_IP64 + case "forceip": + dStrategy = internet.DomainStrategy_FORCE_IP + case "forceipv4": + dStrategy = internet.DomainStrategy_FORCE_IP4 + case "forceipv6": + dStrategy = internet.DomainStrategy_FORCE_IP6 + case "forceipv4v6": + dStrategy = internet.DomainStrategy_FORCE_IP46 + case "forceipv6v4": + dStrategy = internet.DomainStrategy_FORCE_IP64 + default: + return nil, errors.New("unsupported domain strategy: ", c.DomainStrategy) + } + + var customSockopts []*internet.CustomSockopt + + for _, copt := range c.CustomSockopt { + customSockopt := &internet.CustomSockopt{ + System: copt.Syetem, + Network: copt.Network, + Level: copt.Level, + Opt: copt.Opt, + Value: copt.Value, + Type: copt.Type, + } + customSockopts = append(customSockopts, customSockopt) + } + + addressPortStrategy := internet.AddressPortStrategy_None + switch strings.ToLower(c.AddressPortStrategy) { + case "none", "": + addressPortStrategy = internet.AddressPortStrategy_None + case "srvportonly": + addressPortStrategy = internet.AddressPortStrategy_SrvPortOnly + case "srvaddressonly": + addressPortStrategy = internet.AddressPortStrategy_SrvAddressOnly + case "srvportandaddress": + addressPortStrategy = internet.AddressPortStrategy_SrvPortAndAddress + case "txtportonly": + addressPortStrategy = internet.AddressPortStrategy_TxtPortOnly + case "txtaddressonly": + addressPortStrategy = internet.AddressPortStrategy_TxtAddressOnly + case "txtportandaddress": + addressPortStrategy = internet.AddressPortStrategy_TxtPortAndAddress + default: + return nil, errors.New("unsupported address and port strategy: ", c.AddressPortStrategy) + } + + happyEyeballs := &internet.HappyEyeballsConfig{Interleave: 1, PrioritizeIpv6: false, TryDelayMs: 0, MaxConcurrentTry: 4} + if c.HappyEyeballsSettings != nil { + happyEyeballs.PrioritizeIpv6 = c.HappyEyeballsSettings.PrioritizeIPv6 + happyEyeballs.Interleave = c.HappyEyeballsSettings.Interleave + happyEyeballs.TryDelayMs = c.HappyEyeballsSettings.TryDelayMs + happyEyeballs.MaxConcurrentTry = c.HappyEyeballsSettings.MaxConcurrentTry + } + + return &internet.SocketConfig{ + Mark: c.Mark, + Tfo: tfo, + Tproxy: tproxy, + DomainStrategy: dStrategy, + AcceptProxyProtocol: c.AcceptProxyProtocol, + DialerProxy: c.DialerProxy, + TcpKeepAliveInterval: c.TCPKeepAliveInterval, + TcpKeepAliveIdle: c.TCPKeepAliveIdle, + TcpCongestion: c.TCPCongestion, + TcpWindowClamp: c.TCPWindowClamp, + TcpMaxSeg: c.TCPMaxSeg, + Penetrate: c.Penetrate, + TcpUserTimeout: c.TCPUserTimeout, + V6Only: c.V6only, + Interface: c.Interface, + TcpMptcp: c.TcpMptcp, + CustomSockopt: customSockopts, + AddressPortStrategy: addressPortStrategy, + HappyEyeballs: happyEyeballs, + TrustedXForwardedFor: c.TrustedXForwardedFor, + }, nil +} diff --git a/infra/conf/vmess.go b/infra/conf/vmess.go index a73883b6f..6ff274690 100644 --- a/infra/conf/vmess.go +++ b/infra/conf/vmess.go @@ -31,10 +31,6 @@ func (a *VMessAccount) Build() *vmess.Account { st = protocol.SecurityType_CHACHA20_POLY1305 case "auto": st = protocol.SecurityType_AUTO - case "none": - st = protocol.SecurityType_NONE - case "zero": - st = protocol.SecurityType_ZERO default: st = protocol.SecurityType_AUTO } diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 1e25e9b97..cb66131d4 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -221,6 +221,40 @@ type OutboundDetourConfig struct { TargetStrategy string `json:"targetStrategy"` } +func requiresTransportSecurity(address *Address) bool { + if address == nil || address.Address == nil { + return false + } + if address.Family().IsIP() { + return !geodata.GetPrivateIPMatcher().Match(address.IP()) + } + domain := strings.TrimSuffix(strings.ToLower(address.Domain()), ".") + return !geodata.GetPrivateDomainMatcher().MatchAny(domain) +} + +func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *proxyman.SenderConfig) error { + if senderSettings.StreamSettings != nil && senderSettings.StreamSettings.GetSecurityType() != "" { + return nil + } + + if vlessCfg, ok := rawConfig.(*VLessOutboundConfig); ok { + if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" { + return nil + } + if requiresTransportSecurity(vlessCfg.Address) { + return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain") + } + } + + if tjCfg, ok := rawConfig.(*TrojanClientConfig); ok { + if requiresTransportSecurity(tjCfg.Address) { + return errors.New("trojan without TLS is prohibited unless the server address is a private IP or domain") + } + } + + return nil +} + // Build implements Buildable. func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) { if c.ProxySettings != nil { @@ -295,6 +329,9 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) { if err != nil { return nil, errors.New("failed to load outbound detour config for protocol ", c.Protocol).Base(err) } + if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil { + return nil, err + } ts, err := rawConfig.(Buildable).Build() if err != nil { return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err) diff --git a/proxy/freedom/freedom.go b/proxy/freedom/freedom.go index c490046ef..4a0393dbe 100644 --- a/proxy/freedom/freedom.go +++ b/proxy/freedom/freedom.go @@ -68,26 +68,7 @@ func init() { defaultBlockPrivateRule = &FinalRule{ action: RuleAction_Block, network: allNetworks, - ip: common.Must2(geodata.IPReg.BuildIPMatcher(common.Must2(geodata.ParseIPRules([]string{ - "0.0.0.0/8", - "10.0.0.0/8", - "100.64.0.0/10", - "127.0.0.0/8", - "169.254.0.0/16", - "172.16.0.0/12", - "192.0.0.0/24", - "192.0.2.0/24", - "192.88.99.0/24", - "192.168.0.0/16", - "198.18.0.0/15", - "198.51.100.0/24", - "203.0.113.0/24", - "224.0.0.0/3", - "::/127", - "fc00::/7", - "fe80::/10", - "ff00::/8", - })))), + ip: geodata.GetPrivateIPMatcher(), } defaultBlockAllRule = &FinalRule{ diff --git a/proxy/shadowsocks/config.go b/proxy/shadowsocks/config.go index dc8bff0b5..2a178a8ae 100644 --- a/proxy/shadowsocks/config.go +++ b/proxy/shadowsocks/config.go @@ -85,8 +85,6 @@ func (a *Account) getCipher() (Cipher, error) { IVBytes: 32, AEADAuthCreator: createXChaCha20Poly1305, }, nil - case CipherType_NONE: - return NoneCipher{}, nil default: return nil, errors.New("Unsupported cipher.") } @@ -186,30 +184,6 @@ func (c *AEADCipher) DecodePacket(key []byte, b *buf.Buffer) error { return nil } -type NoneCipher struct{} - -func (NoneCipher) KeySize() int32 { return 0 } -func (NoneCipher) IVSize() int32 { return 0 } -func (NoneCipher) IsAEAD() bool { - return false -} - -func (NoneCipher) NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) { - return buf.NewReader(reader), nil -} - -func (NoneCipher) NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) { - return buf.NewWriter(writer), nil -} - -func (NoneCipher) EncodePacket(key []byte, b *buf.Buffer) error { - return nil -} - -func (NoneCipher) DecodePacket(key []byte, b *buf.Buffer) error { - return nil -} - func passwordToCipherKey(password []byte, keySize int32) []byte { key := make([]byte, 0, keySize) diff --git a/proxy/shadowsocks/config.pb.go b/proxy/shadowsocks/config.pb.go index ca450a0ef..f4ff3dc36 100644 --- a/proxy/shadowsocks/config.pb.go +++ b/proxy/shadowsocks/config.pb.go @@ -31,7 +31,6 @@ const ( CipherType_AES_256_GCM CipherType = 6 CipherType_CHACHA20_POLY1305 CipherType = 7 CipherType_XCHACHA20_POLY1305 CipherType = 8 - CipherType_NONE CipherType = 9 ) // Enum value maps for CipherType. @@ -42,7 +41,6 @@ var ( 6: "AES_256_GCM", 7: "CHACHA20_POLY1305", 8: "XCHACHA20_POLY1305", - 9: "NONE", } CipherType_value = map[string]int32{ "UNKNOWN": 0, @@ -50,7 +48,6 @@ var ( "AES_256_GCM": 6, "CHACHA20_POLY1305": 7, "XCHACHA20_POLY1305": 8, - "NONE": 9, } ) @@ -251,15 +248,14 @@ const file_proxy_shadowsocks_config_proto_rawDesc = "" + "\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x122\n" + "\anetwork\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"L\n" + "\fClientConfig\x12<\n" + - "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server*t\n" + + "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server*j\n" + "\n" + "CipherType\x12\v\n" + "\aUNKNOWN\x10\x00\x12\x0f\n" + "\vAES_128_GCM\x10\x05\x12\x0f\n" + "\vAES_256_GCM\x10\x06\x12\x15\n" + "\x11CHACHA20_POLY1305\x10\a\x12\x16\n" + - "\x12XCHACHA20_POLY1305\x10\b\x12\b\n" + - "\x04NONE\x10\tBd\n" + + "\x12XCHACHA20_POLY1305\x10\bBd\n" + "\x1acom.xray.proxy.shadowsocksP\x01Z+github.com/xtls/xray-core/proxy/shadowsocks\xaa\x02\x16Xray.Proxy.Shadowsocksb\x06proto3" var ( diff --git a/proxy/shadowsocks/config.proto b/proxy/shadowsocks/config.proto index 879fb77bf..b6dc5f1b8 100644 --- a/proxy/shadowsocks/config.proto +++ b/proxy/shadowsocks/config.proto @@ -23,7 +23,6 @@ enum CipherType { AES_256_GCM = 6; CHACHA20_POLY1305 = 7; XCHACHA20_POLY1305 = 8; - NONE = 9; } message ServerConfig { diff --git a/proxy/shadowsocks/protocol_test.go b/proxy/shadowsocks/protocol_test.go index 4083905d9..5e288d45f 100644 --- a/proxy/shadowsocks/protocol_test.go +++ b/proxy/shadowsocks/protocol_test.go @@ -38,19 +38,6 @@ func TestUDPEncodingDecoding(t *testing.T) { }), }, }, - { - Version: Version, - Command: protocol.RequestCommandUDP, - Address: net.LocalHostIP, - Port: 1234, - User: &protocol.MemoryUser{ - Email: "love@example.com", - Account: toAccount(&Account{ - Password: "123", - CipherType: CipherType_NONE, - }), - }, - }, } for _, request := range testRequests { @@ -80,10 +67,6 @@ func TestUDPDecodingWithPayloadTooShort(t *testing.T) { Password: "password", CipherType: CipherType_AES_128_GCM, }), - toAccount(&Account{ - Password: "password", - CipherType: CipherType_NONE, - }), } for _, account := range testAccounts { diff --git a/proxy/shadowsocks/validator.go b/proxy/shadowsocks/validator.go index 2c48334f4..e8b33738f 100644 --- a/proxy/shadowsocks/validator.go +++ b/proxy/shadowsocks/validator.go @@ -145,7 +145,6 @@ func (v *Validator) Get(bs []byte, command protocol.RequestCommand) (u *protocol } else { u = user ivLen = user.Account.(*MemoryAccount).Cipher.IVSize() - // err = user.Account.(*MemoryAccount).CheckIV(bs[:ivLen]) // The IV size of None Cipher is 0. return } } diff --git a/proxy/tun/README.md b/proxy/tun/README.md index 8d4421ccf..d1c3aeb77 100644 --- a/proxy/tun/README.md +++ b/proxy/tun/README.md @@ -15,8 +15,9 @@ Plainly enabling it in the config probably will result nothing, or lock your rou ## DETAILS By default, enabling the feature will only bring the tun interface up. \ -When configured explicitly, Windows and Linux can also apply interface addresses from `gateway` and on-link routes from `autoSystemRoutingTable`. -Linux does not configure system DNS from the `dns` field; system DNS remains managed by the OS or distribution-specific network services. \ +When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS uses the first IPv4 prefix from `gateway` to configure the utun point-to-point address. \ +Windows, Linux and macOS can also apply system routes from `autoSystemRoutingTable`. +Linux and macOS do not configure system DNS from the `dns` field; system DNS remains managed by the OS or distribution-specific network services. \ For more advanced routing policies or rules, OS level configuration can still manage the named interface (e.g. xray0) when it appears. This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \ Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README. @@ -206,6 +207,11 @@ ifconfig Produced list will have all system interfaces listed, from which you will see how many "utun" ones already exists. It's not required to select next available number, e.g. if you have utun1-utun7 interfaces, it's not required to have "utun8" in the config. You can choose any available name, even utun20, to get surely available interface number. +macOS requires the utun interface to have a point-to-point IPv4 address before IPv4 routes can use it. \ +By default Xray uses `169.254.10.1/30` as the remote gateway address and assigns the next address in the prefix to the local utun side. \ +You can override this by setting `gateway`; macOS uses the first IPv4 prefix in the list. IPv6 `gateway` entries are not used for utun addressing, and IPv6 routes use the interface route instead. \ +The `dns` field does not change macOS system DNS. + To attach routing to the interface, route command like following can be executed: ``` sudo route add -net 1.1.1.0/24 -iface utun10 @@ -214,6 +220,7 @@ sudo route add -net 1.1.1.0/24 -iface utun10 sudo route add -inet6 -host 2606:4700:4700::1111 -iface utun10 sudo route add -inet6 -host 2606:4700:4700::1001 -iface utun10 ``` +Alternatively, configure `autoSystemRoutingTable` and Xray will add and remove those system routes while it is running. Important to remember that everything written above about Linux routing concept, also apply to Mac OS X. If you simply route default route through utun interface, that will result network loop and immediate network failure. ## ANDROID SUPPORT diff --git a/proxy/tun/tun_darwin.go b/proxy/tun/tun_darwin.go index fdaafd3e1..0bb73b5e7 100644 --- a/proxy/tun/tun_darwin.go +++ b/proxy/tun/tun_darwin.go @@ -24,11 +24,11 @@ import ( ) const ( - utunControlName = "com.apple.net.utun_control" - sysprotoControl = 2 - gateway = "169.254.10.1/30" - utunHeaderSize = 4 - UTUN_OPT_IFNAME = 2 + utunControlName = "com.apple.net.utun_control" + sysprotoControl = 2 + defaultDarwinGateway = "169.254.10.1/30" + utunHeaderSize = 4 + UTUN_OPT_IFNAME = 2 ) const ( @@ -50,6 +50,7 @@ type DarwinTun struct { routeMonitor *os.File routeMonitorOnce sync.Once systemRoutes []netip.Prefix + gateway netip.Prefix } var ( @@ -85,7 +86,13 @@ func NewTun(options *Config) (Tun, error) { return nil, err } - err = setup(options.Name, options.MTU) + gateway, err := selectDarwinGateway(options.Gateway) + if err != nil { + _ = tunFile.Close() + return nil, err + } + + err = setup(options.Name, options.MTU, gateway) if err != nil { _ = tunFile.Close() return nil, err @@ -96,6 +103,7 @@ func NewTun(options *Config) (Tun, error) { options: options, tunFd: int(tunFile.Fd()), ownsFd: true, + gateway: gateway, }, nil } @@ -281,24 +289,56 @@ func open(name string) (*os.File, error) { } // setup the interface by name -func setup(name string, MTU uint32) error { +func setup(name string, MTU uint32, gateway netip.Prefix) error { if err := setMTU(name, MTU); err != nil { return err } /* * Darwin routing require tunnel type interface to have local and remote address, to be routable. - * To simplify inevitable task, assign the interface static ip address, which in current implementation - * is just some random ip from link-local pool, allowing to not bother about existing routing intersection. + * To simplify inevitable task, assign the interface static ip address. */ - syntheticIP, _ := netip.ParsePrefix(gateway) - if err := setIPAddress(name, syntheticIP); err != nil { + if err := setIPAddress(name, gateway); err != nil { return err } return nil } +func selectDarwinGateway(configured []string) (netip.Prefix, error) { + if len(configured) == 0 { + return netip.ParsePrefix(defaultDarwinGateway) + } + + for _, value := range configured { + prefix, err := netip.ParsePrefix(value) + if err != nil { + return netip.Prefix{}, xerrors.New("invalid macOS gateway ", value).Base(err) + } + if !prefix.Addr().Is4() { + continue + } + local, ok := nextDarwinLocalIPv4(prefix) + if !ok || !prefix.Contains(local) { + return netip.Prefix{}, xerrors.New("macOS gateway ", value, " must contain at least one usable local IPv4 address after the gateway address") + } + return prefix, nil + } + + return netip.Prefix{}, xerrors.New("macOS gateway requires at least one IPv4 prefix") +} + +func nextDarwinLocalIPv4(gateway netip.Prefix) (netip.Addr, bool) { + local4 := gateway.Addr().As4() + for i := len(local4) - 1; i >= 0; i-- { + local4[i]++ + if local4[i] != 0 { + return netip.AddrFrom4(local4), true + } + } + return netip.Addr{}, false +} + // setMTU sets MTU on the interface by given name func setMTU(name string, mtu uint32) error { socket, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) @@ -344,8 +384,11 @@ func setIPAddress(name string, gateway netip.Prefix) error { defer unix.Close(socket4) // assume local ip address is next one from the remote address - local4 := gateway.Addr().As4() - local4[3]++ + local, ok := nextDarwinLocalIPv4(gateway) + if !ok || !gateway.Contains(local) { + return xerrors.New("macOS gateway ", gateway.String(), " must contain at least one usable local IPv4 address after the gateway address") + } + local4 := local.As4() // fill the configuration for ipv4 ifReq4 := ifAliasReq4{ @@ -534,7 +577,7 @@ func (t *DarwinTun) setSystemRoutes() error { return err } for _, destination := range routes { - if err := execDarwinRoute(unix.RTM_ADD, tunIndex, destination); err != nil { + if err := execDarwinRoute(unix.RTM_ADD, tunIndex, destination, t.gateway); err != nil { _ = t.unsetSystemRoutes() return xerrors.New("failed to add system route ", destination).Base(err) } @@ -551,7 +594,7 @@ func (t *DarwinTun) unsetSystemRoutes() error { } for i := len(t.systemRoutes) - 1; i >= 0; i-- { destination := t.systemRoutes[i] - if err := execDarwinRoute(unix.RTM_DELETE, tunIndex, destination); err != nil && !errors.Is(err, unix.ESRCH) { + if err := execDarwinRoute(unix.RTM_DELETE, tunIndex, destination, t.gateway); err != nil && !errors.Is(err, unix.ESRCH) { errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err)) } } @@ -606,7 +649,7 @@ func darwinProtectedDefaultRoutes(ipv4 bool) []netip.Prefix { return routes } -func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Prefix) error { +func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Prefix, gateway netip.Prefix) error { message := route.RouteMessage{ Type: messageType, Version: unix.RTM_VERSION, @@ -618,11 +661,10 @@ func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Pref } if destination.Addr().Is4() { - gatewayPrefix := netip.MustParsePrefix(gateway) message.Addrs = []route.Addr{ unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()}, unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())}, - unix.RTAX_GATEWAY: &route.Inet4Addr{IP: gatewayPrefix.Addr().As4()}, + unix.RTAX_GATEWAY: &route.Inet4Addr{IP: gateway.Addr().As4()}, } } else { message.Flags &^= unix.RTF_GATEWAY diff --git a/proxy/tun/tun_darwin_test.go b/proxy/tun/tun_darwin_test.go new file mode 100644 index 000000000..0d8fe7ad4 --- /dev/null +++ b/proxy/tun/tun_darwin_test.go @@ -0,0 +1,49 @@ +//go:build darwin + +package tun + +import ( + "testing" +) + +func TestSelectDarwinGatewayDefault(t *testing.T) { + gateway, err := selectDarwinGateway(nil) + if err != nil { + t.Fatal(err) + } + if got := gateway.String(); got != defaultDarwinGateway { + t.Fatalf("unexpected default gateway: got %s, want %s", got, defaultDarwinGateway) + } +} + +func TestSelectDarwinGatewayConfiguredIPv4(t *testing.T) { + gateway, err := selectDarwinGateway([]string{"198.18.0.1/15"}) + if err != nil { + t.Fatal(err) + } + if got := gateway.String(); got != "198.18.0.1/15" { + t.Fatalf("unexpected gateway: got %s", got) + } +} + +func TestSelectDarwinGatewaySkipsIPv6(t *testing.T) { + gateway, err := selectDarwinGateway([]string{"fc00::1/64", "198.18.0.1/15"}) + if err != nil { + t.Fatal(err) + } + if got := gateway.String(); got != "198.18.0.1/15" { + t.Fatalf("unexpected gateway: got %s", got) + } +} + +func TestSelectDarwinGatewayRequiresIPv4(t *testing.T) { + if _, err := selectDarwinGateway([]string{"fc00::1/64"}); err == nil { + t.Fatal("expected error") + } +} + +func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) { + if _, err := selectDarwinGateway([]string{"198.18.0.1/32"}); err == nil { + t.Fatal("expected error") + } +} diff --git a/proxy/vmess/encoding/auth.go b/proxy/vmess/encoding/auth.go index 99bdaa49c..0640bb5a6 100644 --- a/proxy/vmess/encoding/auth.go +++ b/proxy/vmess/encoding/auth.go @@ -17,27 +17,6 @@ func Authenticate(b []byte) uint32 { return fnv1hash.Sum32() } -// [DEPRECATED 2023-06] -type NoOpAuthenticator struct{} - -func (NoOpAuthenticator) NonceSize() int { - return 0 -} - -func (NoOpAuthenticator) Overhead() int { - return 0 -} - -// Seal implements AEAD.Seal(). -func (NoOpAuthenticator) Seal(dst, nonce, plaintext, additionalData []byte) []byte { - return append(dst[:0], plaintext...) -} - -// Open implements AEAD.Open(). -func (NoOpAuthenticator) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { - return append(dst[:0], ciphertext...), nil -} - // GenerateChacha20Poly1305Key generates a 32-byte key from a given 16-byte array. func GenerateChacha20Poly1305Key(b []byte) []byte { key := make([]byte, 32) diff --git a/proxy/vmess/encoding/client.go b/proxy/vmess/encoding/client.go index d48eddd7c..e5b38d391 100644 --- a/proxy/vmess/encoding/client.go +++ b/proxy/vmess/encoding/client.go @@ -116,20 +116,6 @@ func (c *ClientSession) EncodeRequestBody(request *protocol.RequestHeader, write } switch request.Security { - case protocol.SecurityType_NONE: - if request.Option.Has(protocol.RequestOptionChunkStream) { - if request.Command.TransferType() == protocol.TransferTypeStream { - return crypto.NewChunkStreamWriter(sizeParser, writer), nil - } - auth := &crypto.AEADAuthenticator{ - AEAD: new(NoOpAuthenticator), - NonceGenerator: crypto.GenerateEmptyBytes(), - AdditionalDataGenerator: crypto.GenerateEmptyBytes(), - } - return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding), nil - } - - return buf.NewWriter(writer), nil case protocol.SecurityType_AES128_GCM: aead := crypto.NewAesGcm(c.requestBodyKey[:]) auth := &crypto.AEADAuthenticator{ @@ -267,22 +253,6 @@ func (c *ClientSession) DecodeResponseBody(request *protocol.RequestHeader, read } switch request.Security { - case protocol.SecurityType_NONE: - if request.Option.Has(protocol.RequestOptionChunkStream) { - if request.Command.TransferType() == protocol.TransferTypeStream { - return crypto.NewChunkStreamReader(sizeParser, reader), nil - } - - auth := &crypto.AEADAuthenticator{ - AEAD: new(NoOpAuthenticator), - NonceGenerator: crypto.GenerateEmptyBytes(), - AdditionalDataGenerator: crypto.GenerateEmptyBytes(), - } - - return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding), nil - } - - return buf.NewReader(reader), nil case protocol.SecurityType_AES128_GCM: aead := crypto.NewAesGcm(c.responseBodyKey[:]) diff --git a/proxy/vmess/encoding/server.go b/proxy/vmess/encoding/server.go index 3a11c7475..b8640505d 100644 --- a/proxy/vmess/encoding/server.go +++ b/proxy/vmess/encoding/server.go @@ -262,21 +262,6 @@ func (s *ServerSession) DecodeRequestBody(request *protocol.RequestHeader, reade } switch request.Security { - case protocol.SecurityType_NONE: - if request.Option.Has(protocol.RequestOptionChunkStream) { - if request.Command.TransferType() == protocol.TransferTypeStream { - return crypto.NewChunkStreamReader(sizeParser, reader), nil - } - - auth := &crypto.AEADAuthenticator{ - AEAD: new(NoOpAuthenticator), - NonceGenerator: crypto.GenerateEmptyBytes(), - AdditionalDataGenerator: crypto.GenerateEmptyBytes(), - } - return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding), nil - } - return buf.NewReader(reader), nil - case protocol.SecurityType_AES128_GCM: aead := crypto.NewAesGcm(s.requestBodyKey[:]) auth := &crypto.AEADAuthenticator{ @@ -384,21 +369,6 @@ func (s *ServerSession) EncodeResponseBody(request *protocol.RequestHeader, writ } switch request.Security { - case protocol.SecurityType_NONE: - if request.Option.Has(protocol.RequestOptionChunkStream) { - if request.Command.TransferType() == protocol.TransferTypeStream { - return crypto.NewChunkStreamWriter(sizeParser, writer), nil - } - - auth := &crypto.AEADAuthenticator{ - AEAD: new(NoOpAuthenticator), - NonceGenerator: crypto.GenerateEmptyBytes(), - AdditionalDataGenerator: crypto.GenerateEmptyBytes(), - } - return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding), nil - } - return buf.NewWriter(writer), nil - case protocol.SecurityType_AES128_GCM: aead := crypto.NewAesGcm(s.responseBodyKey[:]) auth := &crypto.AEADAuthenticator{ diff --git a/proxy/vmess/outbound/outbound.go b/proxy/vmess/outbound/outbound.go index 903e73ce3..bdc3ecc25 100644 --- a/proxy/vmess/outbound/outbound.go +++ b/proxy/vmess/outbound/outbound.go @@ -105,7 +105,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte account := request.User.Account.(*vmess.MemoryAccount) request.Security = account.Security - if request.Security == protocol.SecurityType_AES128_GCM || request.Security == protocol.SecurityType_NONE || request.Security == protocol.SecurityType_CHACHA20_POLY1305 { + if request.Security == protocol.SecurityType_AES128_GCM || request.Security == protocol.SecurityType_CHACHA20_POLY1305 { request.Option.Set(protocol.RequestOptionChunkMasking) } @@ -113,12 +113,6 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte request.Option.Set(protocol.RequestOptionGlobalPadding) } - if request.Security == protocol.SecurityType_ZERO { - request.Security = protocol.SecurityType_NONE - request.Option.Clear(protocol.RequestOptionChunkStream) - request.Option.Clear(protocol.RequestOptionChunkMasking) - } - if account.AuthenticatedLengthExperiment { request.Option.Set(protocol.RequestOptionAuthenticatedLength) } diff --git a/testing/scenarios/shadowsocks_test.go b/testing/scenarios/shadowsocks_test.go index affa4124b..2f4536993 100644 --- a/testing/scenarios/shadowsocks_test.go +++ b/testing/scenarios/shadowsocks_test.go @@ -390,88 +390,3 @@ func TestShadowsocksAES128GCMUDPMux(t *testing.T) { t.Error(err) } } - -func TestShadowsocksNone(t *testing.T) { - tcpServer := tcp.Server{ - MsgProcessor: xor, - } - dest, err := tcpServer.Start() - common.Must(err) - - defer tcpServer.Close() - - account := serial.ToTypedMessage(&shadowsocks.Account{ - Password: "shadowsocks-password", - CipherType: shadowsocks.CipherType_NONE, - }) - - serverPort := tcp.PickPort() - serverConfig := &core.Config{ - Inbound: []*core.InboundHandlerConfig{ - { - ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ - PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, - Listen: net.NewIPOrDomain(net.LocalHostIP), - }), - ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ - Users: []*protocol.User{{ - Account: account, - Level: 1, - }}, - Network: []net.Network{net.Network_TCP}, - }), - }, - }, - Outbound: []*core.OutboundHandlerConfig{ - { - ProxySettings: serial.ToTypedMessage(&freedom.Config{ - FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}, - }), - }, - }, - } - - clientPort := tcp.PickPort() - clientConfig := &core.Config{ - Inbound: []*core.InboundHandlerConfig{ - { - ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ - PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, - Listen: net.NewIPOrDomain(net.LocalHostIP), - }), - ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ - RewriteAddress: net.NewIPOrDomain(dest.Address), - RewritePort: uint32(dest.Port), - AllowedNetworks: []net.Network{net.Network_TCP}, - }), - }, - }, - Outbound: []*core.OutboundHandlerConfig{ - { - ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ - Server: &protocol.ServerEndpoint{ - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: &protocol.User{ - Account: account, - }, - }, - }), - }, - }, - } - - servers, err := InitializeServerConfigs(serverConfig, clientConfig) - common.Must(err) - - defer CloseAllServers(servers) - - var errGroup errgroup.Group - for range 3 { - errGroup.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) - } - - if err := errGroup.Wait(); err != nil { - t.Fatal(err) - } -} diff --git a/testing/scenarios/vmess_test.go b/testing/scenarios/vmess_test.go index 28b4f0e00..b1d73b20c 100644 --- a/testing/scenarios/vmess_test.go +++ b/testing/scenarios/vmess_test.go @@ -423,103 +423,6 @@ func TestVMessChacha20(t *testing.T) { } } -func TestVMessNone(t *testing.T) { - tcpServer := tcp.Server{ - MsgProcessor: xor, - } - dest, err := tcpServer.Start() - common.Must(err) - defer tcpServer.Close() - - userID := protocol.NewID(uuid.New()) - serverPort := tcp.PickPort() - serverConfig := &core.Config{ - App: []*serial.TypedMessage{ - serial.ToTypedMessage(&log.Config{ - ErrorLogLevel: clog.Severity_Debug, - ErrorLogType: log.LogType_Console, - }), - }, - Inbound: []*core.InboundHandlerConfig{ - { - ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ - PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, - Listen: net.NewIPOrDomain(net.LocalHostIP), - }), - ProxySettings: serial.ToTypedMessage(&inbound.Config{ - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, - }), - }, - }, - Outbound: []*core.OutboundHandlerConfig{ - { - ProxySettings: serial.ToTypedMessage(&freedom.Config{ - FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}, - }), - }, - }, - } - - clientPort := tcp.PickPort() - clientConfig := &core.Config{ - App: []*serial.TypedMessage{ - serial.ToTypedMessage(&log.Config{ - ErrorLogLevel: clog.Severity_Debug, - ErrorLogType: log.LogType_Console, - }), - }, - Inbound: []*core.InboundHandlerConfig{ - { - ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ - PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, - Listen: net.NewIPOrDomain(net.LocalHostIP), - }), - ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ - RewriteAddress: net.NewIPOrDomain(dest.Address), - RewritePort: uint32(dest.Port), - AllowedNetworks: []net.Network{net.Network_TCP}, - }), - }, - }, - Outbound: []*core.OutboundHandlerConfig{ - { - ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: &protocol.ServerEndpoint{ - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: &protocol.User{ - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_NONE, - }, - }), - }, - }, - }), - }, - }, - } - - servers, err := InitializeServerConfigs(serverConfig, clientConfig) - common.Must(err) - defer CloseAllServers(servers) - - var errg errgroup.Group - for range 3 { - errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30)) - } - if err := errg.Wait(); err != nil { - t.Error(err) - } -} - func TestVMessKCP(t *testing.T) { tcpServer := tcp.Server{ MsgProcessor: xor, @@ -970,103 +873,6 @@ func TestVMessGCMMuxUDP(t *testing.T) { }() } -func TestVMessZero(t *testing.T) { - tcpServer := tcp.Server{ - MsgProcessor: xor, - } - dest, err := tcpServer.Start() - common.Must(err) - defer tcpServer.Close() - - userID := protocol.NewID(uuid.New()) - serverPort := tcp.PickPort() - serverConfig := &core.Config{ - App: []*serial.TypedMessage{ - serial.ToTypedMessage(&log.Config{ - ErrorLogLevel: clog.Severity_Debug, - ErrorLogType: log.LogType_Console, - }), - }, - Inbound: []*core.InboundHandlerConfig{ - { - ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ - PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, - Listen: net.NewIPOrDomain(net.LocalHostIP), - }), - ProxySettings: serial.ToTypedMessage(&inbound.Config{ - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, - }), - }, - }, - Outbound: []*core.OutboundHandlerConfig{ - { - ProxySettings: serial.ToTypedMessage(&freedom.Config{ - FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}, - }), - }, - }, - } - - clientPort := tcp.PickPort() - clientConfig := &core.Config{ - App: []*serial.TypedMessage{ - serial.ToTypedMessage(&log.Config{ - ErrorLogLevel: clog.Severity_Debug, - ErrorLogType: log.LogType_Console, - }), - }, - Inbound: []*core.InboundHandlerConfig{ - { - ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ - PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, - Listen: net.NewIPOrDomain(net.LocalHostIP), - }), - ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ - RewriteAddress: net.NewIPOrDomain(dest.Address), - RewritePort: uint32(dest.Port), - AllowedNetworks: []net.Network{net.Network_TCP}, - }), - }, - }, - Outbound: []*core.OutboundHandlerConfig{ - { - ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: &protocol.ServerEndpoint{ - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: &protocol.User{ - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_ZERO, - }, - }), - }, - }, - }), - }, - }, - } - - servers, err := InitializeServerConfigs(serverConfig, clientConfig) - common.Must(err) - defer CloseAllServers(servers) - - var errg errgroup.Group - for range 3 { - errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30)) - } - if err := errg.Wait(); err != nil { - t.Error(err) - } -} - func TestVMessGCMLengthAuth(t *testing.T) { tcpServer := tcp.Server{ MsgProcessor: xor,