From 5d6ecab128a967537a9e8efaa28207007c70af5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 25 Feb 2023 15:04:18 +0800 Subject: [PATCH] Add reality TLS --- Makefile | 4 +- common/tls/client.go | 9 +- common/tls/config.go | 9 +- common/tls/ech_client.go | 8 +- common/tls/reality_client.go | 187 ++++++++++++++++++++++++ common/tls/reality_server.go | 194 +++++++++++++++++++++++++ common/tls/reality_stub.go | 16 ++ common/tls/server.go | 16 +- common/tls/std_client.go | 4 +- common/tls/std_server.go | 8 +- common/tls/utls_client.go | 70 +++++---- common/tls/utls_stub.go | 4 + go.mod | 3 +- go.sum | 6 +- option/tls.go | 69 ++++++--- test/clash_test.go | 1 - test/go.mod | 5 +- test/go.sum | 6 +- test/vless_test.go | 132 ++++++++++++++++- transport/vless/client.go | 2 +- transport/vless/{conn.go => vision.go} | 38 +++-- transport/vless/vision_reality.go | 23 +++ transport/vless/vision_utls.go | 22 +++ 23 files changed, 743 insertions(+), 93 deletions(-) create mode 100644 common/tls/reality_client.go create mode 100644 common/tls/reality_server.go create mode 100644 common/tls/reality_stub.go rename transport/vless/{conn.go => vision.go} (91%) create mode 100644 transport/vless/vision_reality.go create mode 100644 transport/vless/vision_utls.go diff --git a/Makefile b/Makefile index aa3cfc29..f546cb54 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ NAME = sing-box COMMIT = $(shell git rev-parse --short HEAD) -TAGS ?= with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api -TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_shadowsocksr +TAGS ?= with_gvisor,with_quic,with_wireguard,with_utls,with_reality_server,with_clash_api +TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr PARAMS = -v -trimpath -tags "$(TAGS)" -ldflags "-s -w -buildid=" MAIN = ./cmd/sing-box diff --git a/common/tls/client.go b/common/tls/client.go index 1507e6d1..910872b1 100644 --- a/common/tls/client.go +++ b/common/tls/client.go @@ -31,6 +31,8 @@ func NewClient(router adapter.Router, serverAddress string, options option.Outbo } if options.ECH != nil && options.ECH.Enabled { return NewECHClient(router, serverAddress, options) + } else if options.Reality != nil && options.Reality.Enabled { + return NewRealityClient(router, serverAddress, options) } else if options.UTLS != nil && options.UTLS.Enabled { return NewUTLSClient(router, serverAddress, options) } else { @@ -39,10 +41,13 @@ func NewClient(router adapter.Router, serverAddress string, options option.Outbo } func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) { - tlsConn := config.Client(conn) ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout) defer cancel() - err := tlsConn.HandshakeContext(ctx) + tlsConn, err := config.Client(conn) + if err != nil { + return nil, err + } + err = tlsConn.HandshakeContext(ctx) if err != nil { return nil, err } diff --git a/common/tls/config.go b/common/tls/config.go index b71925a4..d729b7f2 100644 --- a/common/tls/config.go +++ b/common/tls/config.go @@ -21,7 +21,7 @@ type Config interface { NextProtos() []string SetNextProtos(nextProto []string) Config() (*STDConfig, error) - Client(conn net.Conn) Conn + Client(conn net.Conn) (Conn, error) Clone() Config } @@ -32,7 +32,12 @@ type ConfigWithSessionIDGenerator interface { type ServerConfig interface { Config adapter.Service - Server(conn net.Conn) Conn + Server(conn net.Conn) (Conn, error) +} + +type ServerConfigCompat interface { + ServerConfig + ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) } type Conn interface { diff --git a/common/tls/ech_client.go b/common/tls/ech_client.go index f94824a2..57b9ca9a 100644 --- a/common/tls/ech_client.go +++ b/common/tls/ech_client.go @@ -44,8 +44,8 @@ func (e *ECHClientConfig) Config() (*STDConfig, error) { return nil, E.New("unsupported usage for ECH") } -func (e *ECHClientConfig) Client(conn net.Conn) Conn { - return &echConnWrapper{cftls.Client(conn, e.config)} +func (e *ECHClientConfig) Client(conn net.Conn) (Conn, error) { + return &echConnWrapper{cftls.Client(conn, e.config)}, nil } func (e *ECHClientConfig) Clone() Config { @@ -76,6 +76,10 @@ func (c *echConnWrapper) ConnectionState() tls.ConnectionState { } } +func (c *echConnWrapper) Upstream() any { + return c.Conn +} + func NewECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) { var serverName string if options.ServerName != "" { diff --git a/common/tls/reality_client.go b/common/tls/reality_client.go new file mode 100644 index 00000000..2ce61832 --- /dev/null +++ b/common/tls/reality_client.go @@ -0,0 +1,187 @@ +//go:build with_utls + +package tls + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/ed25519" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "fmt" + "net" + "reflect" + "time" + "unsafe" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/debug" + E "github.com/sagernet/sing/common/exceptions" + utls "github.com/sagernet/utls" + + "golang.org/x/crypto/hkdf" +) + +var _ Config = (*RealityClientConfig)(nil) + +type RealityClientConfig struct { + uClient *UTLSClientConfig + publicKey []byte + shortID []byte +} + +func NewRealityClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (*RealityClientConfig, error) { + if options.UTLS == nil || !options.UTLS.Enabled { + return nil, E.New("uTLS is required by reality client") + } + + uClient, err := NewUTLSClient(router, serverAddress, options) + if err != nil { + return nil, err + } + + publicKey, err := base64.RawURLEncoding.DecodeString(options.Reality.PublicKey) + if err != nil { + return nil, E.Cause(err, "decode public_key") + } + if len(publicKey) != 32 { + return nil, E.New("invalid public_key") + } + shortID, err := hex.DecodeString(options.Reality.ShortID) + if err != nil { + return nil, E.Cause(err, "decode short_id") + } + if len(shortID) != 8 { + return nil, E.New("invalid short_id") + } + return &RealityClientConfig{uClient, publicKey, shortID}, nil +} + +func (e *RealityClientConfig) ServerName() string { + return e.uClient.ServerName() +} + +func (e *RealityClientConfig) SetServerName(serverName string) { + e.uClient.SetServerName(serverName) +} + +func (e *RealityClientConfig) NextProtos() []string { + return e.uClient.NextProtos() +} + +func (e *RealityClientConfig) SetNextProtos(nextProto []string) { + e.uClient.SetNextProtos(nextProto) +} + +func (e *RealityClientConfig) Config() (*STDConfig, error) { + return nil, E.New("unsupported usage for reality") +} + +func (e *RealityClientConfig) Client(conn net.Conn) (Conn, error) { + verifier := &realityVerifier{ + serverName: e.uClient.ServerName(), + } + uConfig := e.uClient.config.Clone() + uConfig.InsecureSkipVerify = true + uConfig.SessionTicketsDisabled = true + uConfig.VerifyPeerCertificate = verifier.VerifyPeerCertificate + uConn := utls.UClient(conn, uConfig, e.uClient.id) + verifier.UConn = uConn + err := uConn.BuildHandshakeState() + if err != nil { + return nil, err + } + hello := uConn.HandshakeState.Hello + hello.SessionId = make([]byte, 32) + copy(hello.Raw[39:], hello.SessionId) + + var nowTime time.Time + if uConfig.Time != nil { + nowTime = uConfig.Time() + } else { + nowTime = time.Now() + } + binary.BigEndian.PutUint64(hello.SessionId, uint64(nowTime.Unix())) + + hello.SessionId[0] = 1 + hello.SessionId[1] = 7 + hello.SessionId[2] = 5 + copy(hello.SessionId[8:], e.shortID) + + if debug.Enabled { + fmt.Printf("REALITY hello.sessionId[:16]: %v\n", hello.SessionId[:16]) + } + + authKey := uConn.HandshakeState.State13.EcdheParams.SharedKey(e.publicKey) + if authKey == nil { + return nil, E.New("nil auth_key") + } + verifier.authKey = authKey + _, err = hkdf.New(sha256.New, authKey, hello.Random[:20], []byte("REALITY")).Read(authKey) + if err != nil { + return nil, err + } + aesBlock, _ := aes.NewCipher(authKey) + aesGcmCipher, _ := cipher.NewGCM(aesBlock) + aesGcmCipher.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw) + copy(hello.Raw[39:], hello.SessionId) + if debug.Enabled { + fmt.Printf("REALITY hello.sessionId: %v\n", hello.SessionId) + fmt.Printf("REALITY uConn.AuthKey: %v\n", authKey) + } + + return &utlsConnWrapper{uConn}, nil +} + +func (e *RealityClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) { + e.uClient.config.SessionIDGenerator = generator +} + +func (e *RealityClientConfig) Clone() Config { + return &RealityClientConfig{ + e.uClient.Clone().(*UTLSClientConfig), + e.publicKey, + e.shortID, + } +} + +type realityVerifier struct { + *utls.UConn + serverName string + authKey []byte + verified bool +} + +func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates") + certs := *(*([]*x509.Certificate))(unsafe.Pointer(uintptr(unsafe.Pointer(c.Conn)) + p.Offset)) + if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok { + h := hmac.New(sha512.New, c.authKey) + h.Write(pub) + if bytes.Equal(h.Sum(nil), certs[0].Signature) { + c.verified = true + return nil + } + } + opts := x509.VerifyOptions{ + DNSName: c.serverName, + Intermediates: x509.NewCertPool(), + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + if _, err := certs[0].Verify(opts); err != nil { + return err + } + if !c.verified { + return E.New("reality verification failed") + } + return nil +} diff --git a/common/tls/reality_server.go b/common/tls/reality_server.go new file mode 100644 index 00000000..be688d16 --- /dev/null +++ b/common/tls/reality_server.go @@ -0,0 +1,194 @@ +//go:build with_reality_server + +package tls + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/hex" + "net" + "os" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/debug" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/nekohasekai/reality" +) + +var _ ServerConfigCompat = (*RealityServerConfig)(nil) + +type RealityServerConfig struct { + config *reality.Config +} + +func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) { + var tlsConfig reality.Config + + if options.ACME != nil && len(options.ACME.Domain) > 0 { + return nil, E.New("acme is unavailable in reality") + } + tlsConfig.Time = router.TimeFunc() + if options.ServerName != "" { + tlsConfig.ServerName = options.ServerName + } + if len(options.ALPN) > 0 { + tlsConfig.NextProtos = append(tlsConfig.NextProtos, options.ALPN...) + } + if options.MinVersion != "" { + minVersion, err := ParseTLSVersion(options.MinVersion) + if err != nil { + return nil, E.Cause(err, "parse min_version") + } + tlsConfig.MinVersion = minVersion + } + if options.MaxVersion != "" { + maxVersion, err := ParseTLSVersion(options.MaxVersion) + if err != nil { + return nil, E.Cause(err, "parse max_version") + } + tlsConfig.MaxVersion = maxVersion + } + if options.CipherSuites != nil { + find: + for _, cipherSuite := range options.CipherSuites { + for _, tlsCipherSuite := range tls.CipherSuites() { + if cipherSuite == tlsCipherSuite.Name { + tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID) + continue find + } + } + return nil, E.New("unknown cipher_suite: ", cipherSuite) + } + } + if options.Certificate != "" || options.CertificatePath != "" { + return nil, E.New("certificate is unavailable in reality") + } + if options.Key != "" || options.KeyPath != "" { + return nil, E.New("key is unavailable in reality") + } + + tlsConfig.SessionTicketsDisabled = true + tlsConfig.Type = N.NetworkTCP + tlsConfig.Dest = options.Reality.Handshake.ServerOptions.Build().String() + + tlsConfig.ServerNames = map[string]bool{options.ServerName: true} + privateKey, err := base64.RawURLEncoding.DecodeString(options.Reality.PrivateKey) + if err != nil { + return nil, E.Cause(err, "decode private key") + } + if len(privateKey) != 32 { + return nil, E.New("invalid private key") + } + tlsConfig.PrivateKey = privateKey + tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference) + + tlsConfig.ShortIds = make(map[[8]byte]bool) + for i, shortID := range options.Reality.ShortID { + var shortIDBytesArray [8]byte + decodedLen, err := hex.Decode(shortIDBytesArray[:], []byte(shortID)) + if err != nil { + return nil, E.Cause(err, "decode short_id[", i, "]: ", shortID) + } + if decodedLen != 8 { + return nil, E.New("invalid short_id[", i, "]: ", shortID) + } + tlsConfig.ShortIds[shortIDBytesArray] = true + } + + handshakeDialer := dialer.New(router, options.Reality.Handshake.DialerOptions) + tlsConfig.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + } + + if debug.Enabled { + tlsConfig.Show = true + } + + return &RealityServerConfig{&tlsConfig}, nil +} + +func (c *RealityServerConfig) ServerName() string { + return c.config.ServerName +} + +func (c *RealityServerConfig) SetServerName(serverName string) { + c.config.ServerName = serverName +} + +func (c *RealityServerConfig) NextProtos() []string { + return c.config.NextProtos +} + +func (c *RealityServerConfig) SetNextProtos(nextProto []string) { + c.config.NextProtos = nextProto +} + +func (c *RealityServerConfig) Config() (*tls.Config, error) { + return nil, E.New("unsupported usage for reality") +} + +func (c *RealityServerConfig) Client(conn net.Conn) (Conn, error) { + return nil, os.ErrInvalid +} + +func (c *RealityServerConfig) Start() error { + return nil +} + +func (c *RealityServerConfig) Close() error { + return nil +} + +func (c *RealityServerConfig) Server(conn net.Conn) (Conn, error) { + return nil, os.ErrInvalid +} + +func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) { + tlsConn, err := reality.Server(ctx, conn, c.config) + if err != nil { + return nil, err + } + return &realityConnWrapper{Conn: tlsConn}, nil +} + +func (c *RealityServerConfig) Clone() Config { + return &RealityServerConfig{ + config: c.config.Clone(), + } +} + +var _ Conn = (*realityConnWrapper)(nil) + +type realityConnWrapper struct { + *reality.Conn +} + +func (c *realityConnWrapper) ConnectionState() ConnectionState { + state := c.Conn.ConnectionState() + return tls.ConnectionState{ + Version: state.Version, + HandshakeComplete: state.HandshakeComplete, + DidResume: state.DidResume, + CipherSuite: state.CipherSuite, + NegotiatedProtocol: state.NegotiatedProtocol, + NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual, + ServerName: state.ServerName, + PeerCertificates: state.PeerCertificates, + VerifiedChains: state.VerifiedChains, + SignedCertificateTimestamps: state.SignedCertificateTimestamps, + OCSPResponse: state.OCSPResponse, + TLSUnique: state.TLSUnique, + } +} + +func (c *realityConnWrapper) Upstream() any { + return c.Conn +} diff --git a/common/tls/reality_stub.go b/common/tls/reality_stub.go new file mode 100644 index 00000000..766c01df --- /dev/null +++ b/common/tls/reality_stub.go @@ -0,0 +1,16 @@ +//go:build !with_reality_server + +package tls + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) { + return nil, E.New(`reality server is not included in this build, rebuild with -tags with_reality_server`) +} diff --git a/common/tls/server.go b/common/tls/server.go index dbace148..091325d5 100644 --- a/common/tls/server.go +++ b/common/tls/server.go @@ -16,14 +16,24 @@ func NewServer(ctx context.Context, router adapter.Router, logger log.Logger, op if !options.Enabled { return nil, nil } - return NewSTDServer(ctx, router, logger, options) + if options.Reality != nil && options.Reality.Enabled { + return NewRealityServer(ctx, router, logger, options) + } else { + return NewSTDServer(ctx, router, logger, options) + } } func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) { - tlsConn := config.Server(conn) ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout) defer cancel() - err := tlsConn.HandshakeContext(ctx) + if compatServer, isCompat := config.(ServerConfigCompat); isCompat { + return compatServer.ServerHandshake(ctx, conn) + } + tlsConn, err := config.Server(conn) + if err != nil { + return nil, err + } + err = tlsConn.HandshakeContext(ctx) if err != nil { return nil, err } diff --git a/common/tls/std_client.go b/common/tls/std_client.go index cf630c66..85df7b74 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -36,8 +36,8 @@ func (s *STDClientConfig) Config() (*STDConfig, error) { return s.config, nil } -func (s *STDClientConfig) Client(conn net.Conn) Conn { - return tls.Client(conn, s.config) +func (s *STDClientConfig) Client(conn net.Conn) (Conn, error) { + return tls.Client(conn, s.config), nil } func (s *STDClientConfig) Clone() Config { diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 8414ab36..11c78f48 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -48,12 +48,12 @@ func (c *STDServerConfig) Config() (*STDConfig, error) { return c.config, nil } -func (c *STDServerConfig) Client(conn net.Conn) Conn { - return tls.Client(conn, c.config) +func (c *STDServerConfig) Client(conn net.Conn) (Conn, error) { + return tls.Client(conn, c.config), nil } -func (c *STDServerConfig) Server(conn net.Conn) Conn { - return tls.Server(conn, c.config) +func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) { + return tls.Server(conn, c.config), nil } func (c *STDServerConfig) Clone() Config { diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go index a01c4840..9fda43a3 100644 --- a/common/tls/utls_client.go +++ b/common/tls/utls_client.go @@ -40,14 +40,21 @@ func (e *UTLSClientConfig) Config() (*STDConfig, error) { return nil, E.New("unsupported usage for uTLS") } -func (e *UTLSClientConfig) Client(conn net.Conn) Conn { - return &utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)} +func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { + return &utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, nil } func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) { e.config.SessionIDGenerator = generator } +func (e *UTLSClientConfig) Clone() Config { + return &UTLSClientConfig{ + config: e.config.Clone(), + id: e.id, + } +} + type utlsConnWrapper struct { *utls.UConn } @@ -70,14 +77,11 @@ func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState { } } -func (e *UTLSClientConfig) Clone() Config { - return &UTLSClientConfig{ - config: e.config.Clone(), - id: e.id, - } +func (c *utlsConnWrapper) Upstream() any { + return c.UConn } -func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) { +func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) { var serverName string if options.ServerName != "" { serverName = options.ServerName @@ -148,28 +152,34 @@ func NewUTLSClient(router adapter.Router, serverAddress string, options option.O } tlsConfig.RootCAs = certPool } - var id utls.ClientHelloID - switch options.UTLS.Fingerprint { - case "chrome", "": - id = utls.HelloChrome_Auto - case "firefox": - id = utls.HelloFirefox_Auto - case "edge": - id = utls.HelloEdge_Auto - case "safari": - id = utls.HelloSafari_Auto - case "360": - id = utls.Hello360_Auto - case "qq": - id = utls.HelloQQ_Auto - case "ios": - id = utls.HelloIOS_Auto - case "android": - id = utls.HelloAndroid_11_OkHttp - case "random": - id = utls.HelloRandomized - default: - return nil, E.New("unknown uTLS fingerprint: ", options.UTLS.Fingerprint) + id, err := uTLSClientHelloID(options.UTLS.Fingerprint) + if err != nil { + return nil, err } return &UTLSClientConfig{&tlsConfig, id}, nil } + +func uTLSClientHelloID(name string) (utls.ClientHelloID, error) { + switch name { + case "chrome", "": + return utls.HelloChrome_Auto, nil + case "firefox": + return utls.HelloFirefox_Auto, nil + case "edge": + return utls.HelloEdge_Auto, nil + case "safari": + return utls.HelloSafari_Auto, nil + case "360": + return utls.Hello360_Auto, nil + case "qq": + return utls.HelloQQ_Auto, nil + case "ios": + return utls.HelloIOS_Auto, nil + case "android": + return utls.HelloAndroid_11_OkHttp, nil + case "random": + return utls.HelloRandomized, nil + default: + return utls.ClientHelloID{}, E.New("unknown uTLS fingerprint: ", name) + } +} diff --git a/common/tls/utls_stub.go b/common/tls/utls_stub.go index f9dbec69..7d0ce80e 100644 --- a/common/tls/utls_stub.go +++ b/common/tls/utls_stub.go @@ -11,3 +11,7 @@ import ( func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) { return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`) } + +func NewRealityClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + return nil, E.New(`uTLS, which is required by reality client is not included in this build, rebuild with -tags with_utls`) +} diff --git a/go.mod b/go.mod index aab70548..646c6366 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/logrusorgru/aurora v2.0.3+incompatible github.com/mholt/acmez v1.1.0 github.com/miekg/dns v1.1.50 + github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd github.com/oschwald/maxminddb-golang v1.10.0 github.com/pires/go-proxyproto v0.6.2 github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 @@ -31,7 +32,7 @@ require ( github.com/sagernet/sing-vmess v0.1.2 github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d - github.com/sagernet/utls v0.0.0-20230220130002-c08891932056 + github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01 github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c github.com/spf13/cobra v1.6.1 diff --git a/go.sum b/go.sum index 5c3a51f4..6e3d522f 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,8 @@ github.com/mholt/acmez v1.1.0 h1:IQ9CGHKOHokorxnffsqDvmmE30mDenO1lptYZ1AYkHY= github.com/mholt/acmez v1.1.0/go.mod h1:zwo5+fbLLTowAX8o8ETfQzbDtwGEXnPhkmGdKIP+bgs= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd h1:vd4qbG9ZTW10e1uqo8PDLshe5XL2yPhdINhGlJYaOoQ= +github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd/go.mod h1:C+iqSNDBQ8qMhlNZ0JSUO9POEWq8qX87hukGfmO7/fA= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= @@ -143,8 +145,8 @@ github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 h1:5VBIbVw9q7aKbrFdT github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195/go.mod h1:yedWtra8nyGJ+SyI+ziwuaGMzBatbB10P1IOOZbbSK8= github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d h1:trP/l6ZPWvQ/5Gv99Z7/t/v8iYy06akDMejxW1sznUk= github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d/go.mod h1:jk6Ii8Y3En+j2KQDLgdgQGwb3M6y7EL567jFnGYhN9g= -github.com/sagernet/utls v0.0.0-20230220130002-c08891932056 h1:gDXi/0uYe8dA48UyUI1LM2la5QYN0IvsDvR2H2+kFnA= -github.com/sagernet/utls v0.0.0-20230220130002-c08891932056/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= +github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01 h1:m4MI13+NRKddIvbdSN0sFHK8w5ROTa60Zi9diZ7EE08= +github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e h1:7uw2njHFGE+VpWamge6o56j2RWk4omF6uLKKxMmcWvs= github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e/go.mod h1:45TUl8+gH4SIKr4ykREbxKWTxkDlSzFENzctB1dVRRY= github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo= diff --git a/option/tls.go b/option/tls.go index 72a74966..2ff5f2e4 100644 --- a/option/tls.go +++ b/option/tls.go @@ -1,33 +1,48 @@ package option type InboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate string `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - Key string `json:"key,omitempty"` - KeyPath string `json:"key_path,omitempty"` - ACME *InboundACMEOptions `json:"acme,omitempty"` + Enabled bool `json:"enabled,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites Listable[string] `json:"cipher_suites,omitempty"` + Certificate string `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + Key string `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` + ACME *InboundACMEOptions `json:"acme,omitempty"` + Reality *InboundRealityOptions `json:"reality,omitempty"` } type OutboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - DisableSNI bool `json:"disable_sni,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate string `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - ECH *OutboundECHOptions `json:"ech,omitempty"` - UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Enabled bool `json:"enabled,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites Listable[string] `json:"cipher_suites,omitempty"` + Certificate string `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ECH *OutboundECHOptions `json:"ech,omitempty"` + UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Reality *OutboundRealityOptions `json:"reality,omitempty"` +} + +type InboundRealityOptions struct { + Enabled bool `json:"enabled,omitempty"` + Handshake InboundRealityHandshakeOptions `json:"handshake,omitempty"` + PrivateKey string `json:"private_key,omitempty"` + ShortID Listable[string] `json:"short_id,omitempty"` + MaxTimeDifference Duration `json:"max_time_difference,omitempty"` +} + +type InboundRealityHandshakeOptions struct { + ServerOptions + DialerOptions } type OutboundECHOptions struct { @@ -41,3 +56,9 @@ type OutboundUTLSOptions struct { Enabled bool `json:"enabled,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` } + +type OutboundRealityOptions struct { + Enabled bool `json:"enabled,omitempty"` + PublicKey string `json:"public_key,omitempty"` + ShortID string `json:"short_id,omitempty"` +} diff --git a/test/clash_test.go b/test/clash_test.go index 790de587..466ba5e8 100644 --- a/test/clash_test.go +++ b/test/clash_test.go @@ -58,7 +58,6 @@ var allImages = []string{ var localIP = netip.MustParseAddr("127.0.0.1") func init() { - dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) diff --git a/test/go.mod b/test/go.mod index 0deccb9e..bd30018b 100644 --- a/test/go.mod +++ b/test/go.mod @@ -18,6 +18,8 @@ require ( golang.org/x/net v0.7.0 ) +replace github.com/xtls/reality => github.com/nekohasekai/reality v0.0.0-20230225043811-04070a6bdbea + require ( berty.tech/go-libtor v1.0.385 // indirect github.com/Dreamacro/clash v1.13.0 // indirect @@ -51,6 +53,7 @@ require ( github.com/miekg/dns v1.1.50 // indirect github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd // indirect github.com/onsi/ginkgo/v2 v2.2.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect @@ -73,7 +76,7 @@ require ( github.com/sagernet/sing-vmess v0.1.2 // indirect github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 // indirect github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d // indirect - github.com/sagernet/utls v0.0.0-20230220130002-c08891932056 // indirect + github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01 // indirect github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e // indirect github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c // indirect github.com/sirupsen/logrus v1.9.0 // indirect diff --git a/test/go.sum b/test/go.sum index 07bce98d..f9738079 100644 --- a/test/go.sum +++ b/test/go.sum @@ -104,6 +104,8 @@ github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c h1:RC8WMpjonrBfyAh6VN/PO github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c/go.mod h1:9OcmHNQQUTbk4XCffrLgN1NEKc2mh5u++biHVrvHsSU= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd h1:vd4qbG9ZTW10e1uqo8PDLshe5XL2yPhdINhGlJYaOoQ= +github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd/go.mod h1:C+iqSNDBQ8qMhlNZ0JSUO9POEWq8qX87hukGfmO7/fA= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= @@ -156,8 +158,8 @@ github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 h1:5VBIbVw9q7aKbrFdT github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195/go.mod h1:yedWtra8nyGJ+SyI+ziwuaGMzBatbB10P1IOOZbbSK8= github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d h1:trP/l6ZPWvQ/5Gv99Z7/t/v8iYy06akDMejxW1sznUk= github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d/go.mod h1:jk6Ii8Y3En+j2KQDLgdgQGwb3M6y7EL567jFnGYhN9g= -github.com/sagernet/utls v0.0.0-20230220130002-c08891932056 h1:gDXi/0uYe8dA48UyUI1LM2la5QYN0IvsDvR2H2+kFnA= -github.com/sagernet/utls v0.0.0-20230220130002-c08891932056/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= +github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01 h1:m4MI13+NRKddIvbdSN0sFHK8w5ROTa60Zi9diZ7EE08= +github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e h1:7uw2njHFGE+VpWamge6o56j2RWk4omF6uLKKxMmcWvs= github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e/go.mod h1:45TUl8+gH4SIKr4ykREbxKWTxkDlSzFENzctB1dVRRY= github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo= diff --git a/test/vless_test.go b/test/vless_test.go index 7c557660..988f04fa 100644 --- a/test/vless_test.go +++ b/test/vless_test.go @@ -319,7 +319,6 @@ func testVLESSSelfTLS(t *testing.T, flow string) { }, }, { - Type: C.TypeTrojan, Tag: "trojan", TrojanOptions: option.TrojanInboundOptions{ @@ -396,3 +395,134 @@ func testVLESSSelfTLS(t *testing.T, flow string) { }) testSuit(t, clientPort, testPort) } + +func TestVLESSVisionReality(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + + userUUID := newUUID() + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVLESS, + VLESSOptions: option.VLESSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Users: []option.VLESSUser{ + { + Name: "sekai", + UUID: userUUID.String(), + }, + }, + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.InboundRealityOptions{ + Enabled: true, + Handshake: option.InboundRealityHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, + }, + ShortID: []string{"0123456789abcdef"}, + PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", + }, + }, + }, + }, + { + Type: C.TypeTrojan, + Tag: "trojan", + TrojanOptions: option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: otherPort, + }, + Users: []option.TrojanUser{ + { + Name: "sekai", + Password: userUUID.String(), + }, + }, + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "trojan-out", + TrojanOptions: option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: otherPort, + }, + Password: userUUID.String(), + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + DialerOptions: option.DialerOptions{ + Detour: "vless-out", + }, + }, + }, + { + Type: C.TypeVLESS, + Tag: "vless-out", + VLESSOptions: option.VLESSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: userUUID.String(), + Flow: vless.FlowVision, + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.OutboundRealityOptions{ + Enabled: true, + ShortID: "0123456789abcdef", + PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + }, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "trojan-out", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/transport/vless/client.go b/transport/vless/client.go index d2e3fccb..5a9fd2ed 100644 --- a/transport/vless/client.go +++ b/transport/vless/client.go @@ -36,7 +36,7 @@ func (c *Client) prepareConn(conn net.Conn) (net.Conn, error) { if c.flow == FlowVision { vConn, err := NewVisionConn(conn, c.key) if err != nil { - return nil, err + return nil, E.Cause(err, "initialize vision") } conn = vConn } diff --git a/transport/vless/conn.go b/transport/vless/vision.go similarity index 91% rename from transport/vless/conn.go rename to transport/vless/vision.go index 849649c3..742e8355 100644 --- a/transport/vless/conn.go +++ b/transport/vless/vision.go @@ -17,9 +17,20 @@ import ( "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" - utls "github.com/sagernet/utls" ) +var tlsRegistry []func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer uintptr) + +func init() { + tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer uintptr) { + tlsConn, loaded := conn.(*tls.Conn) + if !loaded { + return + } + return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), uintptr(unsafe.Pointer(tlsConn)) + }) +} + type VisionConn struct { net.Conn writer N.VectorisedWriter @@ -46,18 +57,19 @@ type VisionConn struct { } func NewVisionConn(conn net.Conn, userUUID [16]byte) (*VisionConn, error) { - var reflectType reflect.Type - var reflectPointer uintptr - var netConn net.Conn - if tlsConn, ok := conn.(*tls.Conn); ok { - netConn = tlsConn.NetConn() - reflectType = reflect.TypeOf(tlsConn).Elem() - reflectPointer = uintptr(unsafe.Pointer(tlsConn)) - } else if uConn, ok := conn.(*utls.UConn); ok { - netConn = uConn.NetConn() - reflectType = reflect.TypeOf(uConn).Elem() - reflectPointer = uintptr(unsafe.Pointer(uConn)) - } else { + var ( + loaded bool + reflectType reflect.Type + reflectPointer uintptr + netConn net.Conn + ) + for _, tlsCreator := range tlsRegistry { + loaded, netConn, reflectType, reflectPointer = tlsCreator(conn) + if loaded { + break + } + } + if !loaded { return nil, C.ErrTLSRequired } input, _ := reflectType.FieldByName("input") diff --git a/transport/vless/vision_reality.go b/transport/vless/vision_reality.go new file mode 100644 index 00000000..31e913b6 --- /dev/null +++ b/transport/vless/vision_reality.go @@ -0,0 +1,23 @@ +//go:build with_reality_server + +package vless + +import ( + "net" + "reflect" + "unsafe" + + "github.com/sagernet/sing/common" + + "github.com/nekohasekai/reality" +) + +func init() { + tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer uintptr) { + tlsConn, loaded := common.Cast[*reality.Conn](conn) + if !loaded { + return + } + return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), uintptr(unsafe.Pointer(tlsConn)) + }) +} diff --git a/transport/vless/vision_utls.go b/transport/vless/vision_utls.go new file mode 100644 index 00000000..e2469c88 --- /dev/null +++ b/transport/vless/vision_utls.go @@ -0,0 +1,22 @@ +//go:build with_utls + +package vless + +import ( + "net" + "reflect" + "unsafe" + + "github.com/sagernet/sing/common" + utls "github.com/sagernet/utls" +) + +func init() { + tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer uintptr) { + tlsConn, loaded := common.Cast[*utls.UConn](conn) + if !loaded { + return + } + return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), uintptr(unsafe.Pointer(tlsConn.Conn)) + }) +}