From fb548f54d22856446e883c6d13b32d60f0dda9bd Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:26:15 +0800 Subject: [PATCH] infra/conf/transport_internet.go: Split into multiple files; Rename `network` to `method` (#6426) https://github.com/XTLS/Xray-core/issues/6376#issuecomment-4817591444 https://github.com/XTLS/Xray-core/pull/6426#issuecomment-4899048255 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- infra/conf/grpc.go | 41 - infra/conf/transport_authenticators.go | 208 --- infra/conf/transport_finalmask.go | 871 +++++++++++ infra/conf/transport_internet.go | 1997 +----------------------- infra/conf/transport_method.go | 814 ++++++++++ infra/conf/transport_security.go | 402 +++++ infra/conf/transport_sockopt.go | 187 +++ 7 files changed, 2278 insertions(+), 2242 deletions(-) delete mode 100644 infra/conf/grpc.go delete mode 100644 infra/conf/transport_authenticators.go create mode 100644 infra/conf/transport_finalmask.go create mode 100644 infra/conf/transport_method.go create mode 100644 infra/conf/transport_security.go create mode 100644 infra/conf/transport_sockopt.go 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/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 a6052ff9f..24b5fba7c 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 +}