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/docs/configuration/inbound/index.md b/docs/configuration/inbound/index.md index 27b2d22d..88cdb1aa 100644 --- a/docs/configuration/inbound/index.md +++ b/docs/configuration/inbound/index.md @@ -27,6 +27,7 @@ | `naive` | [Naive](./naive) | X | | `hysteria` | [Hysteria](./hysteria) | X | | `shadowtls` | [ShadowTLS](./shadowtls) | TCP | +| `vless` | [VLESS](./vless) | TCP | | `tun` | [Tun](./tun) | X | | `redirect` | [Redirect](./redirect) | X | | `tproxy` | [TProxy](./tproxy) | X | diff --git a/docs/configuration/inbound/vless.md b/docs/configuration/inbound/vless.md new file mode 100644 index 00000000..d6b310ca --- /dev/null +++ b/docs/configuration/inbound/vless.md @@ -0,0 +1,39 @@ +### Structure + +```json +{ + "type": "vless", + "tag": "vless-in", + + ... // Listen Fields + + "users": [ + { + "name": "sekai", + "uuid": "bf000d23-0752-40b4-affe-68f7707a9661" + } + ], + "tls": {}, + "transport": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen) for details. + +### Fields + +#### users + +==Required== + +VLESS users. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +#### transport + +V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport). diff --git a/docs/configuration/inbound/vless.zh.md b/docs/configuration/inbound/vless.zh.md new file mode 100644 index 00000000..88d455e2 --- /dev/null +++ b/docs/configuration/inbound/vless.zh.md @@ -0,0 +1,39 @@ +### 结构 + +```json +{ + "type": "vless", + "tag": "vless-in", + + ... // 监听字段 + + "users": [ + { + "name": "sekai", + "uuid": "bf000d23-0752-40b4-affe-68f7707a9661" + } + ], + "tls": {}, + "transport": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### users + +==必填== + +VLESS 用户。 + +#### tls + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 + +#### transport + +V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。 diff --git a/docs/configuration/outbound/vless.md b/docs/configuration/outbound/vless.md index 0fd45474..25ed1d0f 100644 --- a/docs/configuration/outbound/vless.md +++ b/docs/configuration/outbound/vless.md @@ -8,6 +8,7 @@ "server": "127.0.0.1", "server_port": 1080, "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", + "flow": "xtls-rprx-vision", "network": "tcp", "tls": {}, "packet_encoding": "", @@ -17,10 +18,6 @@ } ``` -!!! warning "" - - The VLESS protocol is architecturally coupled to v2ray and is unmaintained. This outbound is provided for compatibility purposes only. - ### Fields #### server @@ -41,6 +38,14 @@ The server port. The VLESS user id. +#### flow + +VLESS Sub-protocol. + +Available values: + +* `xtls-rprx-vision` + #### network Enabled network diff --git a/docs/configuration/outbound/vless.zh.md b/docs/configuration/outbound/vless.zh.md index 39949b4d..5892f76d 100644 --- a/docs/configuration/outbound/vless.zh.md +++ b/docs/configuration/outbound/vless.zh.md @@ -8,6 +8,7 @@ "server": "127.0.0.1", "server_port": 1080, "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", + "flow": "xtls-rprx-vision", "network": "tcp", "tls": {}, "packet_encoding": "", @@ -17,10 +18,6 @@ } ``` -!!! warning "" - - VLESS 协议与 v2ray 架构耦合且无人维护。 提供此出站仅出于兼容性目的。 - ### 字段 #### server @@ -41,6 +38,14 @@ VLESS 用户 ID。 +#### flow + +VLESS 子协议。 + +可用值: + +* `xtls-rprx-vision` + #### network 启用的网络协议。 diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 694ce14d..d8ad42be 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -26,6 +26,20 @@ "key_id": "", "mac_key": "" } + }, + "reality": { + "enabled": false, + "handshake": { + "server": "google.com", + "server_port": 443, + + ... // Dial Fields + }, + "private_key": "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", + "short_id": [ + "0123456789abcdef" + ], + "max_time_difference": "1m" } } ``` @@ -53,6 +67,11 @@ "utls": { "enabled": false, "fingerprint": "" + }, + "reality": { + "enabled": false, + "public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + "short_id": "0123456789abcdef" } } ``` @@ -275,6 +294,54 @@ The key identifier. The MAC key. +### Reality Fields + +!!! warning "" + + reality server is not included by default, see [Installation](/#installation). + +!!! warning "" + + uTLS, which is required by reality client is not included by default, see [Installation](/#installation). + +#### handshake + +==Server only== + +==Required== + +Handshake server address and [Dial options](/configuration/shared/dial). + +#### private_key + +==Server only== + +==Required== + +Private key, generated by `./xray x25519`. + +#### public_key + +==Client only== + +==Required== + +Public key, generated by `./xray x25519`. + +#### short_id + +==Required== + +A 8-bit hex string. + +#### max_time_difference + +==Server only== + +The maximum time difference between the server and the client. + +Check disabled if empty. + ### Reload For server configuration, certificate and key will be automatically reloaded if modified. \ No newline at end of file diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 0f85bc90..42cff9e2 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -26,6 +26,20 @@ "key_id": "", "mac_key": "" } + }, + "reality": { + "enabled": false, + "handshake": { + "server": "google.com", + "server_port": 443, + + ... // 拨号字段 + }, + "private_key": "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", + "short_id": [ + "0123456789abcdef" + ], + "max_time_difference": "1m" } } ``` @@ -53,6 +67,11 @@ "utls": { "enabled": false, "fingerprint": "" + }, + "reality": { + "enabled": false, + "public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + "short_id": "0123456789abcdef" } } ``` @@ -271,6 +290,52 @@ EAB(外部帐户绑定)包含将 ACME 帐户绑定或映射到其他已知 MAC 密钥。 +### Reality 字段 + +!!! warning "" + + 默认安装不包含 reality 服务器,参阅 [安装](/zh/#_2)。 + +!!! warning "" + + 默认安装不包含被 reality 客户端需要的 uTLS, 参阅 [安装](/zh/#_2)。 + +#### handshake + +==仅服务器== + +==必填== + +握手服务器地址和 [拨号参数](/zh/configuration/shared/dial/)。 + +#### private_key + +==仅服务器== + +==必填== + +私钥,由 `./xray x25519` 生成。 + +#### public_key + +==仅客户端== + +==必填== + +公钥,由 `./xray x25519` 生成。 + +#### short_id + +==必填== + +一个八位十六进制的字符串。 + +#### max_time_difference + +服务器与和客户端之间允许的最大时间差。 + +默认禁用检查。 + ### 重载 对于服务器配置,如果修改,证书和密钥将自动重新加载。 \ No newline at end of file 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/inbound/builder.go b/inbound/builder.go index 727169f4..d62225a1 100644 --- a/inbound/builder.go +++ b/inbound/builder.go @@ -42,6 +42,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions) case C.TypeShadowTLS: return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions) + case C.TypeVLESS: + return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions) default: return nil, E.New("unknown inbound type: ", options.Type) } diff --git a/inbound/vless.go b/inbound/vless.go new file mode 100644 index 00000000..24c7d6fe --- /dev/null +++ b/inbound/vless.go @@ -0,0 +1,193 @@ +package inbound + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2ray" + "github.com/sagernet/sing-box/transport/vless" + "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing-vmess/packetaddr" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var ( + _ adapter.Inbound = (*VLESS)(nil) + _ adapter.InjectableInbound = (*VLESS)(nil) +) + +type VLESS struct { + myInboundAdapter + ctx context.Context + users []option.VLESSUser + service *vless.Service[int] + tlsConfig tls.ServerConfig + transport adapter.V2RayServerTransport +} + +func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (*VLESS, error) { + inbound := &VLESS{ + myInboundAdapter: myInboundAdapter{ + protocol: C.TypeVLESS, + network: []string{N.NetworkTCP}, + ctx: ctx, + router: router, + logger: logger, + tag: tag, + listenOptions: options.ListenOptions, + }, + ctx: ctx, + users: options.Users, + } + service := vless.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound)) + service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int { + return index + }), common.Map(inbound.users, func(it option.VLESSUser) string { + return it.UUID + })) + inbound.service = service + var err error + if options.TLS != nil { + inbound.tlsConfig, err = tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + } + if options.Transport != nil { + inbound.transport, err = v2ray.NewServerTransport(ctx, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vlessTransportHandler)(inbound)) + if err != nil { + return nil, E.Cause(err, "create server transport: ", options.Transport.Type) + } + } + inbound.connHandler = inbound + return inbound, nil +} + +func (h *VLESS) Start() error { + err := common.Start( + h.service, + h.tlsConfig, + ) + if err != nil { + return err + } + if h.transport == nil { + return h.myInboundAdapter.Start() + } + if common.Contains(h.transport.Network(), N.NetworkTCP) { + tcpListener, err := h.myInboundAdapter.ListenTCP() + if err != nil { + return err + } + go func() { + sErr := h.transport.Serve(tcpListener) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + if common.Contains(h.transport.Network(), N.NetworkUDP) { + udpConn, err := h.myInboundAdapter.ListenUDP() + if err != nil { + return err + } + go func() { + sErr := h.transport.ServePacket(udpConn) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + return nil +} + +func (h *VLESS) Close() error { + return common.Close( + h.service, + &h.myInboundAdapter, + h.tlsConfig, + h.transport, + ) +} + +func (h *VLESS) newTransportConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + h.injectTCP(conn, metadata) + return nil +} + +func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + var err error + if h.tlsConfig != nil && h.transport == nil { + conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) + if err != nil { + return err + } + } + return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) +} + +func (h *VLESS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return os.ErrInvalid +} + +func (h *VLESS) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + return os.ErrInvalid + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + return h.router.RouteConnection(ctx, conn, metadata) +} + +func (h *VLESS) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + return os.ErrInvalid + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress { + metadata.Destination = M.Socksaddr{} + conn = packetaddr.NewConn(conn.(vmess.PacketConn), metadata.Destination) + h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection") + } else { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + } + return h.router.RoutePacketConnection(ctx, conn, metadata) +} + +var _ adapter.V2RayServerTransportHandler = (*vlessTransportHandler)(nil) + +type vlessTransportHandler VLESS + +func (t *vlessTransportHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + return (*VLESS)(t).newTransportConnection(ctx, conn, adapter.InboundContext{ + Source: metadata.Source, + Destination: metadata.Destination, + }) +} + +func (t *vlessTransportHandler) FallbackConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + return os.ErrInvalid +} diff --git a/mkdocs.yml b/mkdocs.yml index f0358522..773534ba 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -71,6 +71,7 @@ nav: - Naive: configuration/inbound/naive.md - Hysteria: configuration/inbound/hysteria.md - ShadowTLS: configuration/inbound/shadowtls.md + - VLESS: configuration/inbound/vless.md - Tun: configuration/inbound/tun.md - Redirect: configuration/inbound/redirect.md - TProxy: configuration/inbound/tproxy.md diff --git a/option/inbound.go b/option/inbound.go index 89e580e7..d24a1de8 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -22,6 +22,7 @@ type _Inbound struct { NaiveOptions NaiveInboundOptions `json:"-"` HysteriaOptions HysteriaInboundOptions `json:"-"` ShadowTLSOptions ShadowTLSInboundOptions `json:"-"` + VLESSOptions VLESSInboundOptions `json:"-"` } type Inbound _Inbound @@ -55,6 +56,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) { v = h.HysteriaOptions case C.TypeShadowTLS: v = h.ShadowTLSOptions + case C.TypeVLESS: + v = h.VLESSOptions default: return nil, E.New("unknown inbound type: ", h.Type) } @@ -94,6 +97,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error { v = &h.HysteriaOptions case C.TypeShadowTLS: v = &h.ShadowTLSOptions + case C.TypeVLESS: + v = &h.VLESSOptions default: return E.New("unknown inbound type: ", h.Type) } 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/option/vless.go b/option/vless.go index 9c6cf8f6..175c7eaf 100644 --- a/option/vless.go +++ b/option/vless.go @@ -1,9 +1,22 @@ package option +type VLESSInboundOptions struct { + ListenOptions + Users []VLESSUser `json:"users,omitempty"` + TLS *InboundTLSOptions `json:"tls,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} + +type VLESSUser struct { + Name string `json:"name"` + UUID string `json:"uuid"` +} + type VLESSOutboundOptions struct { DialerOptions ServerOptions UUID string `json:"uuid"` + Flow string `json:"flow,omitempty"` Network NetworkList `json:"network,omitempty"` TLS *OutboundTLSOptions `json:"tls,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` diff --git a/outbound/vless.go b/outbound/vless.go index 4805608a..946b5c5b 100644 --- a/outbound/vless.go +++ b/outbound/vless.go @@ -11,9 +11,9 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2ray" + "github.com/sagernet/sing-box/transport/vless" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-vmess/packetaddr" - "github.com/sagernet/sing-vmess/vless" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" @@ -67,7 +67,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg default: return nil, E.New("unknown packet encoding: ", options.PacketEncoding) } - outbound.client, err = vless.NewClient(options.UUID) + outbound.client, err = vless.NewClient(options.UUID, options.Flow) if err != nil { return nil, err } @@ -92,16 +92,12 @@ func (h *VLESS) DialContext(ctx context.Context, network string, destination M.S return nil, err } switch N.NetworkName(network) { - case N.NetworkTCP: - case N.NetworkUDP: - } - switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) - return h.client.DialEarlyConn(conn, destination), nil + return h.client.DialEarlyConn(conn, destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound packet connection to ", destination) - return h.client.DialEarlyPacketConn(conn, destination), nil + return h.client.DialEarlyPacketConn(conn, destination) default: return nil, E.Extend(N.ErrUnknownNetwork, network) } @@ -126,11 +122,15 @@ func (h *VLESS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net. return nil, err } if h.xudp { - return h.client.DialEarlyXUDPPacketConn(conn, destination), nil + return h.client.DialEarlyXUDPPacketConn(conn, destination) } else if h.packetAddr { - return dialer.NewResolvePacketConn(ctx, h.router, dns.DomainStrategyAsIS, packetaddr.NewConn(h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}), destination)), nil + conn, err := h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}) + if err != nil { + return nil, err + } + return dialer.NewResolvePacketConn(ctx, h.router, dns.DomainStrategyAsIS, packetaddr.NewConn(conn, destination)), nil } else { - return h.client.DialEarlyPacketConn(conn, destination), nil + return h.client.DialEarlyPacketConn(conn, destination) } } diff --git a/test/clash_test.go b/test/clash_test.go index 3c33bf1f..466ba5e8 100644 --- a/test/clash_test.go +++ b/test/clash_test.go @@ -13,7 +13,6 @@ import ( "testing" "time" - C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common/control" F "github.com/sagernet/sing/common/format" @@ -59,14 +58,6 @@ var allImages = []string{ var localIP = netip.MustParseAddr("127.0.0.1") func init() { - if C.IsDarwin { - var err error - localIP, err = defaultRouteIP() - if err != nil { - panic(err) - } - } - dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) diff --git a/test/config/vless-tls-server.json b/test/config/vless-tls-server.json new file mode 100644 index 00000000..1c0f3524 --- /dev/null +++ b/test/config/vless-tls-server.json @@ -0,0 +1,39 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "0.0.0.0", + "port": 1234, + "protocol": "vless", + "settings": { + "decryption": "none", + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811", + "flow": "" + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "tls", + "tlsSettings": { + "serverName": "example.org", + "certificates": [ + { + "certificateFile": "/path/to/certificate.crt", + "keyFile": "/path/to/private.key" + } + ] + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/go.mod b/test/go.mod index 7f805c9f..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 @@ -68,12 +71,12 @@ require ( github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32 // indirect github.com/sagernet/sing-dns v0.1.4 // indirect - github.com/sagernet/sing-shadowtls v0.0.0-20230221075551-c5ad05179260 // indirect + github.com/sagernet/sing-shadowtls v0.0.0-20230221123345-78e50cd7b587 // indirect github.com/sagernet/sing-tun v0.1.1 // indirect 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 0f0493af..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= @@ -146,8 +148,8 @@ github.com/sagernet/sing-dns v0.1.4 h1:7VxgeoSCiiazDSaXXQVcvrTBxFpOePPq/4XdgnUDN github.com/sagernet/sing-dns v0.1.4/go.mod h1:1+6pCa48B1AI78lD+/i/dLgpw4MwfnsSpZo0Ds8wzzk= github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9 h1:qS39eA4C7x+zhEkySbASrtmb6ebdy5v0y2M6mgkmSO0= github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9/go.mod h1:f3mHTy5shnVM9l8UocMlJgC/1G/zdj5FuEuVXhDinGU= -github.com/sagernet/sing-shadowtls v0.0.0-20230221075551-c5ad05179260 h1:RKeyBMI5kRnno3/WGsW4HrGnZkhISQQrnRxAKXbf5Vg= -github.com/sagernet/sing-shadowtls v0.0.0-20230221075551-c5ad05179260/go.mod h1:Kn1VUIprdkwCgkS6SXYaLmIpKzQbqBIKJBMY+RvBhYc= +github.com/sagernet/sing-shadowtls v0.0.0-20230221123345-78e50cd7b587 h1:OjIXlHT2bblZfp+ciupM4xY9+Ccpj9FsuHRtKRBv+Pg= +github.com/sagernet/sing-shadowtls v0.0.0-20230221123345-78e50cd7b587/go.mod h1:Kn1VUIprdkwCgkS6SXYaLmIpKzQbqBIKJBMY+RvBhYc= github.com/sagernet/sing-tun v0.1.1 h1:2Hg3GAyJWzQ7Ua1j74dE+mI06vaqSBO9yD4tkTjggn4= github.com/sagernet/sing-tun v0.1.1/go.mod h1:WzW/SkT+Nh9uJn/FIYUE2YJHYuPwfbh8sATOzU9QDGw= github.com/sagernet/sing-vmess v0.1.2 h1:RbOZNAId2LrCai8epMoQXlf0XTrou0bfcw08hNBg6lM= @@ -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 0a128c5f..988f04fa 100644 --- a/test/vless_test.go +++ b/test/vless_test.go @@ -7,6 +7,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/vless" "github.com/spyzhov/ajson" "github.com/stretchr/testify/require" @@ -28,8 +29,9 @@ func TestVLESS(t *testing.T) { startDockerContainer(t, DockerOptions{ Image: ImageV2RayCore, - Ports: []uint16{serverPort, testPort}, + Ports: []uint16{serverPort}, EntryPoint: "v2ray", + Cmd: []string{"run"}, Stdin: content, }) @@ -58,36 +60,48 @@ func TestVLESS(t *testing.T) { }, }, }) - testSuit(t, clientPort, testPort) + testTCP(t, clientPort, testPort) } func TestVLESSXRay(t *testing.T) { - testVLESSXray(t, "") + t.Run("origin", func(t *testing.T) { + testVLESSXray(t, "", "") + }) + t.Run("xudp", func(t *testing.T) { + testVLESSXray(t, "xudp", "") + }) + t.Run("vision", func(t *testing.T) { + testVLESSXray(t, "", vless.FlowVision) + }) } -func TestVLESSXUDP(t *testing.T) { - testVLESSXray(t, "xudp") -} +func testVLESSXray(t *testing.T, packetEncoding string, flow string) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") -func testVLESSXray(t *testing.T, packetEncoding string) { - content, err := os.ReadFile("config/vless-server.json") + content, err := os.ReadFile("config/vless-tls-server.json") require.NoError(t, err) config, err := ajson.Unmarshal(content) require.NoError(t, err) - user := newUUID() + userID := newUUID() inbound := config.MustKey("inbounds").MustIndex(0) inbound.MustKey("port").SetNumeric(float64(serverPort)) - inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(user.String()) + user := inbound.MustKey("settings").MustKey("clients").MustIndex(0) + user.MustKey("id").SetString(userID.String()) + user.MustKey("flow").SetString(flow) content, err = ajson.Marshal(config) require.NoError(t, err) startDockerContainer(t, DockerOptions{ Image: ImageXRayCore, - Ports: []uint16{serverPort, testPort}, + Ports: []uint16{serverPort}, EntryPoint: "xray", Stdin: content, + Bind: map[string]string{ + certPem: "/path/to/certificate.crt", + keyPem: "/path/to/private.key", + }, }) startInstance(t, option.Options{ @@ -101,17 +115,411 @@ func testVLESSXray(t *testing.T, packetEncoding string) { }, }, }, + { + Type: C.TypeTrojan, + Tag: "trojan", + TrojanOptions: option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: otherPort, + }, + Users: []option.TrojanUser{ + { + Name: "sekai", + Password: userID.String(), + }, + }, + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, }, Outbounds: []option.Outbound{ + { + Type: C.TypeTrojan, + TrojanOptions: option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "host.docker.internal", + ServerPort: otherPort, + }, + Password: userID.String(), + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + DialerOptions: option.DialerOptions{ + Detour: "vless", + }, + }, + }, { Type: C.TypeVLESS, + Tag: "vless", VLESSOptions: option.VLESSOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, - UUID: user.String(), + UUID: userID.String(), + Flow: flow, PacketEncoding: packetEncoding, + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + { + Type: C.TypeDirect, + Tag: "direct", + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"trojan"}, + Outbound: "direct", + }, + }, + }, + }, + }) + + testTCP(t, clientPort, testPort) +} + +func TestVLESSSelf(t *testing.T) { + t.Run("origin", func(t *testing.T) { + testVLESSSelf(t, "") + }) + t.Run("vision", func(t *testing.T) { + testVLESSSelf(t, vless.FlowVision) + }) + t.Run("vision-tls", func(t *testing.T) { + testVLESSSelfTLS(t, vless.FlowVision) + }) +} + +func testVLESSSelf(t *testing.T, flow string) { + _, 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: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVLESS, + Tag: "vless-out", + VLESSOptions: option.VLESSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: userUUID.String(), + Flow: flow, + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "vless-out", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func testVLESSSelfTLS(t *testing.T, flow string) { + _, 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: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + { + 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: flow, + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "trojan-out", + }, + }, + }, + }, + }) + 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", + }, }, }, }, diff --git a/transport/vless/client.go b/transport/vless/client.go new file mode 100644 index 00000000..5a9fd2ed --- /dev/null +++ b/transport/vless/client.go @@ -0,0 +1,204 @@ +package vless + +import ( + "encoding/binary" + "io" + "net" + + "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + + "github.com/gofrs/uuid" +) + +type Client struct { + key [16]byte + flow string +} + +func NewClient(userId string, flow string) (*Client, error) { + user := uuid.FromStringOrNil(userId) + if user == uuid.Nil { + user = uuid.NewV5(user, userId) + } + switch flow { + case "", "xtls-rprx-vision": + default: + return nil, E.New("unsupported flow: " + flow) + } + return &Client{user, flow}, nil +} + +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, E.Cause(err, "initialize vision") + } + conn = vConn + } + return conn, nil +} + +func (c *Client) DialConn(conn net.Conn, destination M.Socksaddr) (*Conn, error) { + vConn, err := c.prepareConn(conn) + if err != nil { + return nil, err + } + serverConn := &Conn{Conn: conn, protocolConn: vConn, key: c.key, command: vmess.CommandTCP, destination: destination, flow: c.flow} + return serverConn, common.Error(serverConn.Write(nil)) +} + +func (c *Client) DialEarlyConn(conn net.Conn, destination M.Socksaddr) (*Conn, error) { + vConn, err := c.prepareConn(conn) + if err != nil { + return nil, err + } + return &Conn{Conn: conn, protocolConn: vConn, key: c.key, command: vmess.CommandTCP, destination: destination, flow: c.flow}, nil +} + +func (c *Client) DialPacketConn(conn net.Conn, destination M.Socksaddr) (*PacketConn, error) { + serverConn := &PacketConn{Conn: conn, key: c.key, destination: destination, flow: c.flow} + return serverConn, common.Error(serverConn.Write(nil)) +} + +func (c *Client) DialEarlyPacketConn(conn net.Conn, destination M.Socksaddr) (*PacketConn, error) { + return &PacketConn{Conn: conn, key: c.key, destination: destination, flow: c.flow}, nil +} + +func (c *Client) DialXUDPPacketConn(conn net.Conn, destination M.Socksaddr) (vmess.PacketConn, error) { + serverConn := &Conn{Conn: conn, protocolConn: conn, key: c.key, command: vmess.CommandMux, destination: destination, flow: c.flow} + err := common.Error(serverConn.Write(nil)) + if err != nil { + return nil, err + } + return vmess.NewXUDPConn(serverConn, destination), nil +} + +func (c *Client) DialEarlyXUDPPacketConn(conn net.Conn, destination M.Socksaddr) (vmess.PacketConn, error) { + return vmess.NewXUDPConn(&Conn{Conn: conn, protocolConn: conn, key: c.key, command: vmess.CommandMux, destination: destination, flow: c.flow}, destination), nil +} + +type Conn struct { + net.Conn + protocolConn net.Conn + key [16]byte + command byte + destination M.Socksaddr + flow string + requestWritten bool + responseRead bool +} + +func (c *Conn) Read(b []byte) (n int, err error) { + if !c.responseRead { + err = ReadResponse(c.Conn) + if err != nil { + return + } + c.responseRead = true + } + return c.protocolConn.Read(b) +} + +func (c *Conn) Write(b []byte) (n int, err error) { + if !c.requestWritten { + request := Request{c.key, c.command, c.destination, c.flow} + if c.protocolConn != nil { + err = WriteRequest(c.Conn, request, nil) + } else { + err = WriteRequest(c.Conn, request, b) + } + if err == nil { + n = len(b) + } + c.requestWritten = true + if c.protocolConn == nil { + return + } + } + return c.protocolConn.Write(b) +} + +func (c *Conn) Upstream() any { + return c.Conn +} + +type PacketConn struct { + net.Conn + key [16]byte + destination M.Socksaddr + flow string + requestWritten bool + responseRead bool +} + +func (c *PacketConn) Read(b []byte) (n int, err error) { + if !c.responseRead { + err = ReadResponse(c.Conn) + if err != nil { + return + } + c.responseRead = true + } + var length uint16 + err = binary.Read(c.Conn, binary.BigEndian, &length) + if err != nil { + return + } + if cap(b) < int(length) { + return 0, io.ErrShortBuffer + } + return io.ReadFull(c.Conn, b[:length]) +} + +func (c *PacketConn) Write(b []byte) (n int, err error) { + if !c.requestWritten { + err = WritePacketRequest(c.Conn, Request{c.key, vmess.CommandUDP, c.destination, c.flow}, nil) + if err == nil { + n = len(b) + } + c.requestWritten = true + } + err = binary.Write(c.Conn, binary.BigEndian, uint16(len(b))) + if err != nil { + return + } + return c.Conn.Write(b) +} + +func (c *PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + defer buffer.Release() + dataLen := buffer.Len() + binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(dataLen)) + if !c.requestWritten { + err := WritePacketRequest(c.Conn, Request{c.key, vmess.CommandUDP, c.destination, c.flow}, buffer.Bytes()) + c.requestWritten = true + return err + } + return common.Error(c.Conn.Write(buffer.Bytes())) +} + +func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + n, err = c.Read(p) + if err != nil { + return + } + addr = c.destination.UDPAddr() + return +} + +func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + return c.Write(p) +} + +func (c *PacketConn) FrontHeadroom() int { + return 2 +} + +func (c *PacketConn) Upstream() any { + return c.Conn +} diff --git a/transport/vless/constant.go b/transport/vless/constant.go new file mode 100644 index 00000000..20085019 --- /dev/null +++ b/transport/vless/constant.go @@ -0,0 +1,34 @@ +package vless + +import ( + "bytes" + + "github.com/sagernet/sing/common/buf" +) + +var ( + tls13SupportedVersions = []byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04} + tlsClientHandShakeStart = []byte{0x16, 0x03} + tlsServerHandShakeStart = []byte{0x16, 0x03, 0x03} + tlsApplicationDataStart = []byte{0x17, 0x03, 0x03} +) + +var tls13CipherSuiteDic = map[uint16]string{ + 0x1301: "TLS_AES_128_GCM_SHA256", + 0x1302: "TLS_AES_256_GCM_SHA384", + 0x1303: "TLS_CHACHA20_POLY1305_SHA256", + 0x1304: "TLS_AES_128_CCM_SHA256", + 0x1305: "TLS_AES_128_CCM_8_SHA256", +} + +func reshapeBuffer(b []byte) []*buf.Buffer { + const bufferLimit = 8192 - 21 + if len(b) < bufferLimit { + return []*buf.Buffer{buf.As(b)} + } + index := int32(bytes.LastIndex(b, tlsApplicationDataStart)) + if index <= 0 { + index = 8192 / 2 + } + return []*buf.Buffer{buf.As(b[:index]), buf.As(b[index:])} +} diff --git a/transport/vless/protocol.go b/transport/vless/protocol.go new file mode 100644 index 00000000..17f18942 --- /dev/null +++ b/transport/vless/protocol.go @@ -0,0 +1,241 @@ +package vless + +import ( + "bytes" + "encoding/binary" + "io" + + "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/rw" +) + +const ( + Version = 0 + FlowVision = "xtls-rprx-vision" +) + +type Request struct { + UUID [16]byte + Command byte + Destination M.Socksaddr + Flow string +} + +func ReadRequest(reader io.Reader) (*Request, error) { + var request Request + + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return nil, err + } + if version != Version { + return nil, E.New("unknown version: ", version) + } + + _, err = io.ReadFull(reader, request.UUID[:]) + if err != nil { + return nil, err + } + + var addonsLen uint8 + err = binary.Read(reader, binary.BigEndian, &addonsLen) + if err != nil { + return nil, err + } + + if addonsLen > 0 { + addonsBytes, err := rw.ReadBytes(reader, int(addonsLen)) + if err != nil { + return nil, err + } + + addons, err := readAddons(bytes.NewReader(addonsBytes)) + if err != nil { + return nil, err + } + request.Flow = addons.Flow + } + + err = binary.Read(reader, binary.BigEndian, &request.Command) + if err != nil { + return nil, err + } + + if request.Command != vmess.CommandMux { + request.Destination, err = vmess.AddressSerializer.ReadAddrPort(reader) + if err != nil { + return nil, err + } + } + + return &request, nil +} + +type Addons struct { + Flow string + Seed string +} + +func readAddons(reader io.Reader) (*Addons, error) { + protoHeader, err := rw.ReadByte(reader) + if err != nil { + return nil, err + } + if protoHeader != 10 { + return nil, E.New("unknown protobuf message header: ", protoHeader) + } + + var addons Addons + + flowLen, err := rw.ReadUVariant(reader) + if err != nil { + if err == io.EOF { + return &addons, nil + } + return nil, err + } + flowBytes, err := rw.ReadBytes(reader, int(flowLen)) + if err != nil { + return nil, err + } + addons.Flow = string(flowBytes) + + seedLen, err := rw.ReadUVariant(reader) + if err != nil { + if err == io.EOF { + return &addons, nil + } + return nil, err + } + seedBytes, err := rw.ReadBytes(reader, int(seedLen)) + if err != nil { + return nil, err + } + addons.Seed = string(seedBytes) + + return &addons, nil +} + +func WriteRequest(writer io.Writer, request Request, payload []byte) error { + var requestLen int + requestLen += 1 // version + requestLen += 16 // uuid + requestLen += 1 // protobuf length + + var addonsLen int + if request.Flow != "" { + addonsLen += 1 // protobuf header + addonsLen += UvarintLen(uint64(len(request.Flow))) + addonsLen += len(request.Flow) + requestLen += addonsLen + } + requestLen += 1 // command + if request.Command != vmess.CommandMux { + requestLen += vmess.AddressSerializer.AddrPortLen(request.Destination) + } + requestLen += len(payload) + _buffer := buf.StackNewSize(requestLen) + defer common.KeepAlive(_buffer) + buffer := common.Dup(_buffer) + defer buffer.Release() + common.Must( + buffer.WriteByte(Version), + common.Error(buffer.Write(request.UUID[:])), + buffer.WriteByte(byte(addonsLen)), + ) + if addonsLen > 0 { + common.Must(buffer.WriteByte(10)) + binary.PutUvarint(buffer.Extend(UvarintLen(uint64(len(request.Flow)))), uint64(len(request.Flow))) + common.Must(common.Error(buffer.Write([]byte(request.Flow)))) + } + common.Must( + buffer.WriteByte(request.Command), + ) + + if request.Command != vmess.CommandMux { + common.Must(vmess.AddressSerializer.WriteAddrPort(buffer, request.Destination)) + } + + common.Must1(buffer.Write(payload)) + return common.Error(writer.Write(buffer.Bytes())) +} + +func WritePacketRequest(writer io.Writer, request Request, payload []byte) error { + var requestLen int + requestLen += 1 // version + requestLen += 16 // uuid + requestLen += 1 // protobuf length + var addonsLen int + /*if request.Flow != "" { + addonsLen += 1 // protobuf header + addonsLen += UvarintLen(uint64(len(request.Flow))) + addonsLen += len(request.Flow) + requestLen += addonsLen + }*/ + requestLen += 1 // command + requestLen += vmess.AddressSerializer.AddrPortLen(request.Destination) + if len(payload) > 0 { + requestLen += 2 + requestLen += len(payload) + } + _buffer := buf.StackNewSize(requestLen) + defer common.KeepAlive(_buffer) + buffer := common.Dup(_buffer) + defer buffer.Release() + common.Must( + buffer.WriteByte(Version), + common.Error(buffer.Write(request.UUID[:])), + buffer.WriteByte(byte(addonsLen)), + ) + + if addonsLen > 0 { + common.Must(buffer.WriteByte(10)) + binary.PutUvarint(buffer.Extend(UvarintLen(uint64(len(request.Flow)))), uint64(len(request.Flow))) + common.Must(common.Error(buffer.Write([]byte(request.Flow)))) + } + + common.Must( + buffer.WriteByte(vmess.CommandUDP), + vmess.AddressSerializer.WriteAddrPort(buffer, request.Destination), + ) + + if len(payload) > 0 { + common.Must( + binary.Write(buffer, binary.BigEndian, uint16(len(payload))), + common.Error(buffer.Write(payload)), + ) + } + + return common.Error(writer.Write(buffer.Bytes())) +} + +func ReadResponse(reader io.Reader) error { + version, err := rw.ReadByte(reader) + if err != nil { + return err + } + if version != Version { + return E.New("unknown version: ", version) + } + protobufLength, err := rw.ReadByte(reader) + if err != nil { + return err + } + if protobufLength > 0 { + err = rw.SkipN(reader, int(protobufLength)) + if err != nil { + return err + } + } + return nil +} + +func UvarintLen(value uint64) int { + var buffer [binary.MaxVarintLen64]byte + return binary.PutUvarint(buffer[:], value) +} diff --git a/transport/vless/service.go b/transport/vless/service.go new file mode 100644 index 00000000..84fd629e --- /dev/null +++ b/transport/vless/service.go @@ -0,0 +1,196 @@ +package vless + +import ( + "context" + "encoding/binary" + "io" + "net" + + "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/gofrs/uuid" +) + +type Service[T any] struct { + userMap map[[16]byte]T + handler Handler +} + +type Handler interface { + N.TCPConnectionHandler + N.UDPConnectionHandler + E.Handler +} + +func NewService[T any](handler Handler) *Service[T] { + return &Service[T]{ + handler: handler, + } +} + +func (s *Service[T]) UpdateUsers(userList []T, userUUIDList []string) { + userMap := make(map[[16]byte]T) + for i, userName := range userList { + userID := uuid.FromStringOrNil(userUUIDList[i]) + if userID == uuid.Nil { + userID = uuid.NewV5(uuid.Nil, userUUIDList[i]) + } + userMap[userID] = userName + } + s.userMap = userMap +} + +var _ N.TCPConnectionHandler = (*Service[int])(nil) + +func (s *Service[T]) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + request, err := ReadRequest(conn) + if err != nil { + return err + } + user, loaded := s.userMap[request.UUID] + if !loaded { + return E.New("unknown UUID: ", uuid.FromBytesOrNil(request.UUID[:])) + } + ctx = auth.ContextWithUser(ctx, user) + metadata.Destination = request.Destination + + protocolConn := conn + switch request.Flow { + case "": + case FlowVision: + protocolConn, err = NewVisionConn(conn, request.UUID) + if err != nil { + return E.Cause(err, "initialize vision") + } + } + + switch request.Command { + case vmess.CommandTCP: + return s.handler.NewConnection(ctx, &serverConn{Conn: protocolConn, responseWriter: conn}, metadata) + case vmess.CommandUDP: + return s.handler.NewPacketConnection(ctx, &serverPacketConn{ExtendedConn: bufio.NewExtendedConn(conn), destination: request.Destination}, metadata) + case vmess.CommandMux: + return vmess.HandleMuxConnection(ctx, &serverConn{Conn: conn}, s.handler) + default: + return E.New("unknown command: ", request.Command) + } +} + +type serverConn struct { + net.Conn + responseWriter io.Writer + responseWritten bool +} + +func (c *serverConn) Read(b []byte) (n int, err error) { + return c.Conn.Read(b) +} + +func (c *serverConn) Write(b []byte) (n int, err error) { + if !c.responseWritten { + if c.responseWriter == nil { + _, err = bufio.WriteVectorised(bufio.NewVectorisedWriter(c.Conn), [][]byte{{Version, 0}, b}) + if err == nil { + n = len(b) + } + c.responseWritten = true + return + } else { + _, err = c.responseWriter.Write([]byte{Version, 0}) + if err != nil { + return + } + c.responseWritten = true + } + } + return c.Conn.Write(b) +} + +type serverPacketConn struct { + N.ExtendedConn + responseWriter io.Writer + responseWritten bool + destination M.Socksaddr +} + +func (c *serverPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + n, err = c.ExtendedConn.Read(p) + if err != nil { + return + } + addr = c.destination.UDPAddr() + return +} + +func (c *serverPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + if !c.responseWritten { + if c.responseWriter == nil { + var packetLen [2]byte + binary.BigEndian.PutUint16(packetLen[:], uint16(len(p))) + _, err = bufio.WriteVectorised(bufio.NewVectorisedWriter(c.ExtendedConn), [][]byte{{Version, 0}, packetLen[:], p}) + if err == nil { + n = len(p) + } + c.responseWritten = true + return + } else { + _, err = c.responseWriter.Write([]byte{Version, 0}) + if err != nil { + return + } + c.responseWritten = true + } + } + return c.ExtendedConn.Write(p) +} + +func (c *serverPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { + var packetLen uint16 + err = binary.Read(c.ExtendedConn, binary.BigEndian, &packetLen) + if err != nil { + return + } + + _, err = buffer.ReadFullFrom(c.ExtendedConn, int(packetLen)) + if err != nil { + return + } + + destination = c.destination + return +} + +func (c *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + if !c.responseWritten { + if c.responseWriter == nil { + var packetLen [2]byte + binary.BigEndian.PutUint16(packetLen[:], uint16(buffer.Len())) + err := bufio.NewVectorisedWriter(c.ExtendedConn).WriteVectorised([]*buf.Buffer{buf.As([]byte{Version, 0}), buf.As(packetLen[:]), buffer}) + c.responseWritten = true + return err + } else { + _, err := c.responseWriter.Write([]byte{Version, 0}) + if err != nil { + return err + } + c.responseWritten = true + } + } + packetLen := buffer.Len() + binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(packetLen)) + return c.ExtendedConn.WriteBuffer(buffer) +} + +func (c *serverPacketConn) FrontHeadroom() int { + return 2 +} + +func (c *serverPacketConn) Upstream() any { + return c.ExtendedConn +} diff --git a/transport/vless/vision.go b/transport/vless/vision.go new file mode 100644 index 00000000..742e8355 --- /dev/null +++ b/transport/vless/vision.go @@ -0,0 +1,309 @@ +package vless + +import ( + "bytes" + "crypto/rand" + "crypto/tls" + "io" + "math/big" + "net" + "reflect" + "time" + "unsafe" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +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 + input *bytes.Reader + rawInput *bytes.Buffer + netConn net.Conn + + userUUID [16]byte + isTLS bool + numberOfPacketToFilter int + isTLS12orAbove bool + remainingServerHello int32 + cipher uint16 + enableXTLS bool + filterTlsApplicationData bool + directWrite bool + writeUUID bool + filterUUID bool + remainingContent int + remainingPadding int + currentCommand int + directRead bool + remainingReader io.Reader +} + +func NewVisionConn(conn net.Conn, userUUID [16]byte) (*VisionConn, error) { + 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") + rawInput, _ := reflectType.FieldByName("rawInput") + return &VisionConn{ + Conn: conn, + writer: bufio.NewVectorisedWriter(conn), + input: (*bytes.Reader)(unsafe.Pointer(reflectPointer + input.Offset)), + rawInput: (*bytes.Buffer)(unsafe.Pointer(reflectPointer + rawInput.Offset)), + netConn: netConn, + userUUID: userUUID, + numberOfPacketToFilter: 8, + remainingServerHello: -1, + filterTlsApplicationData: true, + writeUUID: true, + filterUUID: true, + remainingContent: -1, + remainingPadding: -1, + }, nil +} + +func (c *VisionConn) Read(p []byte) (n int, err error) { + if c.remainingReader != nil { + n, err = c.remainingReader.Read(p) + if err == io.EOF { + c.remainingReader = nil + if n > 0 { + return + } + } + } + if c.directRead { + return c.netConn.Read(p) + } + n, err = c.Conn.Read(p) + if err != nil { + return + } + buffer := p[:n] + if c.filterUUID && (c.isTLS || c.numberOfPacketToFilter > 0) { + buffers := c.unPadding(buffer) + if c.remainingContent == 0 && c.remainingPadding == 0 { + if c.currentCommand == 1 { + c.filterUUID = false + } else if c.currentCommand == 2 { + c.filterUUID = false + c.directRead = true + + inputBuffer, err := io.ReadAll(c.input) + if err != nil { + return 0, err + } + buffers = append(buffers, inputBuffer) + + rawInputBuffer, err := io.ReadAll(c.rawInput) + if err != nil { + return 0, err + } + + buffers = append(buffers, rawInputBuffer) + } else if c.currentCommand != 0 { + return 0, E.New("unknown command ", c.currentCommand) + } + } + if c.numberOfPacketToFilter > 0 { + c.filterTLS(buffers) + } + c.remainingReader = io.MultiReader(common.Map(buffers, func(it []byte) io.Reader { return bytes.NewReader(it) })...) + return c.remainingReader.Read(p) + } else { + if c.numberOfPacketToFilter > 0 { + c.filterTLS([][]byte{buffer}) + } + return + } +} + +func (c *VisionConn) Write(p []byte) (n int, err error) { + if c.numberOfPacketToFilter > 0 { + c.filterTLS([][]byte{p}) + } + if c.isTLS && c.filterTlsApplicationData { + inputLen := len(p) + buffers := reshapeBuffer(p) + var specIndex int + for i, buffer := range buffers { + if buffer.Len() > 6 && bytes.Equal(tlsApplicationDataStart, buffer.To(3)) { + var command byte = 1 + if c.enableXTLS { + c.directWrite = true + specIndex = i + command = 2 + } + c.filterTlsApplicationData = false + buffers[i] = c.padding(buffer, command) + break + } else if !c.isTLS12orAbove && c.numberOfPacketToFilter == 0 { + c.filterTlsApplicationData = false + buffers[i] = c.padding(buffer, 0x01) + break + } + buffers[i] = c.padding(buffer, 0x00) + } + if c.directWrite { + encryptedBuffer := buffers[:specIndex+1] + err = c.writer.WriteVectorised(encryptedBuffer) + if err != nil { + return + } + buffers = buffers[specIndex+1:] + c.writer = bufio.NewVectorisedWriter(c.netConn) + time.Sleep(5 * time.Millisecond) // wtf + } + err = c.writer.WriteVectorised(buffers) + if err == nil { + n = inputLen + } + return + } + if c.directWrite { + return c.netConn.Write(p) + } else { + return c.Conn.Write(p) + } +} + +func (c *VisionConn) filterTLS(buffers [][]byte) { + for _, buffer := range buffers { + c.numberOfPacketToFilter-- + if len(buffer) > 6 { + if buffer[0] == 22 && buffer[1] == 3 && buffer[2] == 3 { + c.isTLS = true + if buffer[5] == 2 { + c.isTLS12orAbove = true + c.remainingServerHello = (int32(buffer[3])<<8 | int32(buffer[4])) + 5 + if len(buffer) >= 79 && c.remainingServerHello >= 79 { + sessionIdLen := int32(buffer[43]) + cipherSuite := buffer[43+sessionIdLen+1 : 43+sessionIdLen+3] + c.cipher = uint16(cipherSuite[0])<<8 | uint16(cipherSuite[1]) + } + } + } else if bytes.Equal(tlsClientHandShakeStart, buffer[:2]) && buffer[5] == 1 { + c.isTLS = true + } + } + if c.remainingServerHello > 0 { + end := int(c.remainingServerHello) + if end > len(buffer) { + end = len(buffer) + } + c.remainingServerHello -= int32(end) + if bytes.Contains(buffer[:end], tls13SupportedVersions) { + cipher, ok := tls13CipherSuiteDic[c.cipher] + if ok && cipher != "TLS_AES_128_CCM_8_SHA256" { + c.enableXTLS = true + } + c.numberOfPacketToFilter = 0 + return + } else if c.remainingServerHello == 0 { + c.numberOfPacketToFilter = 0 + return + } + } + } +} + +func (c *VisionConn) padding(buffer *buf.Buffer, command byte) *buf.Buffer { + contentLen := 0 + paddingLen := 0 + if buffer != nil { + contentLen = buffer.Len() + } + if contentLen < 900 { + l, _ := rand.Int(rand.Reader, big.NewInt(500)) + paddingLen = int(l.Int64()) + 900 - contentLen + } + newBuffer := buf.New() + if c.writeUUID { + newBuffer.Write(c.userUUID[:]) + c.writeUUID = false + } + newBuffer.Write([]byte{command, byte(contentLen >> 8), byte(contentLen), byte(paddingLen >> 8), byte(paddingLen)}) + if buffer != nil { + newBuffer.Write(buffer.Bytes()) + buffer.Release() + } + newBuffer.Extend(paddingLen) + return newBuffer +} + +func (c *VisionConn) unPadding(buffer []byte) [][]byte { + var bufferIndex int + if c.remainingContent == -1 && c.remainingPadding == -1 { + if len(buffer) >= 21 && bytes.Equal(c.userUUID[:], buffer[:16]) { + bufferIndex = 16 + c.remainingContent = 0 + c.remainingPadding = 0 + } + } + if c.remainingContent == -1 && c.remainingPadding == -1 { + return [][]byte{buffer} + } + var buffers [][]byte + for bufferIndex < len(buffer) { + if c.remainingContent <= 0 && c.remainingPadding <= 0 { + if c.currentCommand == 1 { + buffers = append(buffers, buffer[bufferIndex:]) + break + } else { + paddingInfo := buffer[bufferIndex : bufferIndex+5] + c.currentCommand = int(paddingInfo[0]) + c.remainingContent = int(paddingInfo[1])<<8 | int(paddingInfo[2]) + c.remainingPadding = int(paddingInfo[3])<<8 | int(paddingInfo[4]) + bufferIndex += 5 + } + } else if c.remainingContent > 0 { + end := c.remainingContent + if end > len(buffer)-bufferIndex { + end = len(buffer) - bufferIndex + } + buffers = append(buffers, buffer[bufferIndex:bufferIndex+end]) + c.remainingContent -= end + bufferIndex += end + } else { + end := c.remainingPadding + if end > len(buffer)-bufferIndex { + end = len(buffer) - bufferIndex + } + c.remainingPadding -= end + bufferIndex += end + } + if bufferIndex == len(buffer) { + break + } + } + return buffers +} 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)) + }) +}