mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-08 20:24:56 +03:00
Add SSH inbound, log level. Update MTPROXY. Fixes
This commit is contained in:
@@ -22,7 +22,7 @@ func NewBondedConn(conns []net.Conn, downloadRatios, uploadRatios []uint8) *bond
|
||||
conns: conns,
|
||||
downloadRatios: downloadRatios,
|
||||
uploadRatios: uploadRatios,
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 65536)),
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 4096)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,13 +37,13 @@ type failoverConn struct {
|
||||
func NewFailoverConn(ctx context.Context, conn net.Conn, dial dial, onClose func()) *failoverConn {
|
||||
var writeBuffers [BufferSize][]byte
|
||||
for i := range BufferSize {
|
||||
writeBuffers[i] = make([]byte, 0, 1000)
|
||||
writeBuffers[i] = make([]byte, 0, 1024)
|
||||
}
|
||||
return &failoverConn{
|
||||
Conn: conn,
|
||||
ctx: ctx,
|
||||
dial: dial,
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 1000)),
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 1024)),
|
||||
writeBuffers: writeBuffers,
|
||||
onClose: onClose,
|
||||
}
|
||||
|
||||
@@ -194,8 +194,6 @@ type bwConnEntry struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
|
||||
|
||||
type ManagerBandwidthStrategy struct {
|
||||
strategies map[string]BandwidthStrategy
|
||||
conns map[string][]*bwConnEntry
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
@@ -173,8 +173,6 @@ func (h *Outbound) GetStrategy() ConnectionStrategy {
|
||||
return h.strategy
|
||||
}
|
||||
|
||||
|
||||
|
||||
func connChecker(ctx context.Context, closeFunc func() error) {
|
||||
<-ctx.Done()
|
||||
closeFunc()
|
||||
|
||||
@@ -76,8 +76,6 @@ type connEntry struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
|
||||
|
||||
type ManagerTrafficStrategy struct {
|
||||
strategies map[string]TrafficStrategy
|
||||
conns map[string][]*connEntry
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/common/cloudflare"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/transport/masque"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
@@ -136,11 +136,19 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
ctx,
|
||||
logger,
|
||||
masque.TunnelOptions{
|
||||
System: options.System,
|
||||
Name: options.Name,
|
||||
CreateDialer: func(interfaceName string) N.Dialer {
|
||||
return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{
|
||||
BindInterface: interfaceName,
|
||||
}))
|
||||
},
|
||||
Dialer: outboundDialer,
|
||||
Address: []netip.Prefix{
|
||||
netip.MustParsePrefix(appConfig.IPv4 + "/32"),
|
||||
netip.MustParsePrefix(appConfig.IPv6 + "/128"),
|
||||
},
|
||||
AllowedAddress: options.AllowedIPs,
|
||||
Endpoint: endpoint,
|
||||
TLSConfig: tlsConfig,
|
||||
UseHTTP2: options.UseHTTP2,
|
||||
|
||||
@@ -87,7 +87,7 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||
return fmt.Errorf("failed to start mieru server: %w", err)
|
||||
}
|
||||
|
||||
h.logger.Info("mieru server is started")
|
||||
h.logger.Notice("mieru server is started")
|
||||
go h.acceptLoop()
|
||||
return nil
|
||||
}
|
||||
@@ -275,14 +275,21 @@ func buildMieruServerConfig(_ context.Context, options option.MieruInboundOption
|
||||
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
|
||||
}
|
||||
|
||||
if options.ListenOptions.ListenPort == 0 {
|
||||
return nil, nil, E.New("listen_port must be set")
|
||||
if options.ListenOptions.ListenPort == 0 && len(options.ListenPorts) == 0 {
|
||||
return nil, nil, E.New("either listen_port or listen_ports must be set")
|
||||
}
|
||||
portBindings := []*mierupb.PortBinding{
|
||||
{
|
||||
var portBindings []*mierupb.PortBinding
|
||||
if options.ListenOptions.ListenPort != 0 {
|
||||
portBindings = append(portBindings, &mierupb.PortBinding{
|
||||
Port: proto.Int32(int32(options.ListenOptions.ListenPort)),
|
||||
Protocol: transportProtocol,
|
||||
},
|
||||
})
|
||||
}
|
||||
for _, pr := range options.ListenPorts {
|
||||
portBindings = append(portBindings, &mierupb.PortBinding{
|
||||
PortRange: proto.String(pr),
|
||||
Protocol: transportProtocol,
|
||||
})
|
||||
}
|
||||
|
||||
var users []*mierupb.User
|
||||
|
||||
@@ -53,7 +53,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err := c.Start(); err != nil {
|
||||
return nil, fmt.Errorf("failed to start mieru client: %w", err)
|
||||
}
|
||||
logger.InfoContext(ctx, "mieru client is started")
|
||||
logger.NoticeContext(ctx, "mieru client is started")
|
||||
|
||||
return &Outbound{
|
||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeMieru, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||
|
||||
@@ -63,7 +63,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
Secrets: secrets,
|
||||
Concurrency: options.GetConcurrency(),
|
||||
DomainFrontingPort: options.GetDomainFrontingPort(),
|
||||
DomainFrontingIP: options.DomainFrontingIP,
|
||||
DomainFrontingHost: options.DomainFrontingHost,
|
||||
DomainFrontingProxyProtocol: options.DomainFrontingProxyProtocol,
|
||||
PreferIP: options.GetPreferIP(),
|
||||
AutoUpdate: options.AutoUpdate,
|
||||
|
||||
@@ -227,7 +227,7 @@ func (h *Outbound) Start(stage adapter.StartStage) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.logger.Info("NaiveProxy started, version: ", h.client.Engine().Version())
|
||||
h.logger.Notice("NaiveProxy started, version: ", h.client.Engine().Version())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
@@ -90,10 +90,18 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
logger: logger,
|
||||
}
|
||||
tunnel, err := ovpn.NewTunnel(ctx, logger, ovpn.TunnelOptions{
|
||||
System: options.System,
|
||||
Name: options.Name,
|
||||
CreateDialer: func(interfaceName string) N.Dialer {
|
||||
return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{
|
||||
BindInterface: interfaceName,
|
||||
}))
|
||||
},
|
||||
Dialer: outboundDialer,
|
||||
Servers: options.Servers,
|
||||
TLSConfig: tlsConfig,
|
||||
Config: clientConfig,
|
||||
AllowedAddress: options.AllowedIPs,
|
||||
ReconnectDelay: time.Duration(options.ReconnectDelay),
|
||||
PingInterval: time.Duration(options.PingInterval),
|
||||
})
|
||||
|
||||
91
protocol/ssh/certificate.go
Normal file
91
protocol/ssh/certificate.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func parseCAKey(options *option.SSHCAOptions) (ssh.Signer, error) {
|
||||
var keyData []byte
|
||||
var err error
|
||||
if len(options.PrivateKey) > 0 {
|
||||
keyData = []byte(strings.Join(options.PrivateKey, "\n"))
|
||||
} else if options.PrivateKeyPath != "" {
|
||||
keyData, err = os.ReadFile(os.ExpandEnv(options.PrivateKeyPath))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read CA private key")
|
||||
}
|
||||
} else {
|
||||
return nil, E.New("missing CA private key")
|
||||
}
|
||||
if options.PrivateKeyPassphrase == "" {
|
||||
return ssh.ParsePrivateKey(keyData)
|
||||
}
|
||||
return ssh.ParsePrivateKeyWithPassphrase(keyData, []byte(options.PrivateKeyPassphrase))
|
||||
}
|
||||
|
||||
func verifyCertificate(signer ssh.Signer, metadata ssh.ConnMetadata, key ssh.PublicKey) bool {
|
||||
if signer == nil {
|
||||
return false
|
||||
}
|
||||
certificate, ok := key.(*ssh.Certificate)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
checker := &ssh.CertChecker{
|
||||
IsUserAuthority: func(auth ssh.PublicKey) bool {
|
||||
return bytes.Equal(auth.Marshal(), signer.PublicKey().Marshal())
|
||||
},
|
||||
}
|
||||
if !checker.IsUserAuthority(certificate.SignatureKey) {
|
||||
return false
|
||||
}
|
||||
return checker.CheckCert(metadata.User(), certificate) == nil
|
||||
}
|
||||
|
||||
func issueCertificate(signer ssh.Signer, user string) (ssh.Signer, error) {
|
||||
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ephemeral, err := ssh.NewSignerFromSigner(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
certificate := &ssh.Certificate{
|
||||
Key: ephemeral.PublicKey(),
|
||||
Serial: uint64(now.UnixNano()),
|
||||
CertType: ssh.UserCert,
|
||||
KeyId: user,
|
||||
ValidPrincipals: []string{user},
|
||||
ValidAfter: uint64(now.Add(-1 * time.Minute).Unix()),
|
||||
ValidBefore: uint64(now.Add(5 * time.Minute).Unix()),
|
||||
Permissions: ssh.Permissions{
|
||||
Extensions: map[string]string{
|
||||
"permit-pty": "",
|
||||
"permit-port-forwarding": "",
|
||||
"permit-agent-forwarding": "",
|
||||
"permit-X11-forwarding": "",
|
||||
"permit-user-rc": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := certificate.SignCert(rand.Reader, signer); err != nil {
|
||||
return nil, E.Cause(err, "sign certificate")
|
||||
}
|
||||
certSigner, err := ssh.NewCertSigner(certificate, ephemeral)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create certificate signer")
|
||||
}
|
||||
return certSigner, nil
|
||||
}
|
||||
322
protocol/ssh/fallback.go
Normal file
322
protocol/ssh/fallback.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var _ Service = (*Fallback)(nil)
|
||||
|
||||
type Fallback struct {
|
||||
Service
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
clientVersion string
|
||||
mainSigner ssh.Signer
|
||||
issueSigner ssh.Signer
|
||||
hostKeys []ssh.PublicKey
|
||||
keyAlgorithms []string
|
||||
pending map[string]*upstreamConn
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
type upstreamConn struct {
|
||||
conn net.Conn
|
||||
client ssh.Conn
|
||||
channels <-chan ssh.NewChannel
|
||||
requests <-chan *ssh.Request
|
||||
}
|
||||
|
||||
func NewFallback(ctx context.Context, logger logger.ContextLogger, inner Service, options *option.SSHFallbackServerOptions) (*Fallback, error) {
|
||||
serverAddr := options.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 22
|
||||
}
|
||||
if !serverAddr.Addr.IsValid() && serverAddr.Fqdn == "" {
|
||||
return nil, E.New("missing upstream server address")
|
||||
}
|
||||
upstreamDialer, err := dialer.New(ctx, options.DialerOptions, serverAddr.IsFqdn())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fallback := &Fallback{
|
||||
Service: inner,
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
dialer: upstreamDialer,
|
||||
serverAddr: serverAddr,
|
||||
clientVersion: options.ClientVersion,
|
||||
keyAlgorithms: options.HostKeyAlgorithms,
|
||||
pending: make(map[string]*upstreamConn),
|
||||
}
|
||||
if fallback.clientVersion == "" {
|
||||
fallback.clientVersion = "SSH-2.0-OpenSSH_9.6"
|
||||
}
|
||||
if options.CA != nil {
|
||||
signer, err := parseCAKey(options.CA)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse CA")
|
||||
}
|
||||
fallback.mainSigner = signer
|
||||
}
|
||||
if options.IssueCA != nil {
|
||||
signer, err := parseCAKey(options.IssueCA)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse issue CA")
|
||||
}
|
||||
fallback.issueSigner = signer
|
||||
}
|
||||
if fallback.issueSigner == nil && fallback.mainSigner != nil {
|
||||
fallback.issueSigner = fallback.mainSigner
|
||||
}
|
||||
for _, hostKey := range options.HostKey {
|
||||
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey))
|
||||
if err != nil {
|
||||
return nil, E.New("parse upstream host key ", hostKey)
|
||||
}
|
||||
fallback.hostKeys = append(fallback.hostKeys, key)
|
||||
}
|
||||
for _, hostKeyPath := range options.HostKeyPath {
|
||||
content, err := os.ReadFile(os.ExpandEnv(hostKeyPath))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read upstream host key ", hostKeyPath)
|
||||
}
|
||||
key, _, _, _, err := ssh.ParseAuthorizedKey(content)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse upstream host key ", hostKeyPath)
|
||||
}
|
||||
fallback.hostKeys = append(fallback.hostKeys, key)
|
||||
}
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
func (f *Fallback) PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
if permissions, err := f.Service.PasswordCallback(conn, password); err == nil {
|
||||
return permissions, nil
|
||||
}
|
||||
if err := f.dial(string(conn.SessionID()), conn.User(), ssh.Password(string(password))); err != nil {
|
||||
return nil, E.Cause(err, "upstream authentication failed for user ", conn.User())
|
||||
}
|
||||
return &ssh.Permissions{Extensions: map[string]string{"user": conn.User(), "fallback": "1"}}, nil
|
||||
}
|
||||
|
||||
func (f *Fallback) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
if permissions, err := f.Service.PublicKeyCallback(conn, key); err == nil {
|
||||
return permissions, nil
|
||||
}
|
||||
if verifyCertificate(f.mainSigner, conn, key) {
|
||||
signer, err := issueCertificate(f.issueSigner, conn.User())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "upstream authentication failed for user ", conn.User())
|
||||
}
|
||||
if err := f.dial(string(conn.SessionID()), conn.User(), ssh.PublicKeys(signer)); err != nil {
|
||||
return nil, E.Cause(err, "upstream authentication failed for user ", conn.User())
|
||||
}
|
||||
return &ssh.Permissions{Extensions: map[string]string{"user": conn.User(), "fallback": "1"}}, nil
|
||||
}
|
||||
return nil, E.New("public key authentication failed for user ", conn.User())
|
||||
}
|
||||
|
||||
func (f *Fallback) Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string) {
|
||||
if serverConn.Permissions == nil || serverConn.Permissions.Extensions["fallback"] != "1" {
|
||||
f.Service.Handle(ctx, serverConn, channels, requests, metadata, user)
|
||||
return
|
||||
}
|
||||
sessionID := string(serverConn.SessionID())
|
||||
f.mtx.Lock()
|
||||
upstream := f.pending[sessionID]
|
||||
delete(f.pending, sessionID)
|
||||
f.mtx.Unlock()
|
||||
if upstream == nil {
|
||||
serverConn.Close()
|
||||
return
|
||||
}
|
||||
f.logger.InfoContext(ctx, "[", user, "] forwarded SSH connection from ", metadata.Source)
|
||||
go proxyDownstreamRequests(requests, upstream.client)
|
||||
go proxyGlobalRequests(upstream.requests, serverConn)
|
||||
go func() {
|
||||
for newChannel := range upstream.channels {
|
||||
go proxyChannel(newChannel, serverConn)
|
||||
}
|
||||
}()
|
||||
var wg sync.WaitGroup
|
||||
for newChannel := range channels {
|
||||
wg.Go(func() {
|
||||
proxyChannel(newChannel, upstream.client)
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
upstream.client.Close()
|
||||
upstream.conn.Close()
|
||||
serverConn.Close()
|
||||
}
|
||||
|
||||
func (f *Fallback) Close() error {
|
||||
f.mtx.Lock()
|
||||
connections := make([]net.Conn, 0, len(f.pending))
|
||||
for id, upstream := range f.pending {
|
||||
if upstream != nil {
|
||||
connections = append(connections, upstream.conn)
|
||||
}
|
||||
delete(f.pending, id)
|
||||
}
|
||||
f.mtx.Unlock()
|
||||
for _, conn := range connections {
|
||||
conn.Close()
|
||||
}
|
||||
return f.Service.Close()
|
||||
}
|
||||
|
||||
func (f *Fallback) dial(sessionID string, user string, auth ssh.AuthMethod) error {
|
||||
f.mtx.Lock()
|
||||
if _, attempted := f.pending[sessionID]; attempted {
|
||||
f.mtx.Unlock()
|
||||
return E.New("fallback already attempted")
|
||||
}
|
||||
f.pending[sessionID] = nil
|
||||
f.mtx.Unlock()
|
||||
conn, err := f.dialer.DialContext(f.ctx, N.NetworkTCP, f.serverAddr)
|
||||
if err != nil {
|
||||
f.mtx.Lock()
|
||||
delete(f.pending, sessionID)
|
||||
f.mtx.Unlock()
|
||||
return err
|
||||
}
|
||||
conn = onclose.NewConn(conn, func() {
|
||||
f.mtx.Lock()
|
||||
delete(f.pending, sessionID)
|
||||
f.mtx.Unlock()
|
||||
})
|
||||
config := &ssh.ClientConfig{
|
||||
User: user,
|
||||
Auth: []ssh.AuthMethod{auth},
|
||||
ClientVersion: f.clientVersion,
|
||||
HostKeyAlgorithms: f.keyAlgorithms,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
if len(f.hostKeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
serverKey := key.Marshal()
|
||||
for _, hostKey := range f.hostKeys {
|
||||
if bytes.Equal(serverKey, hostKey.Marshal()) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return E.New("upstream host key mismatch, server sent ", key.Type(), " ", base64.StdEncoding.EncodeToString(serverKey))
|
||||
},
|
||||
Timeout: C.TCPTimeout,
|
||||
}
|
||||
client, channels, requests, err := ssh.NewClientConn(conn, f.serverAddr.String(), config)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
f.mtx.Lock()
|
||||
f.pending[sessionID] = &upstreamConn{conn: conn, client: client, channels: channels, requests: requests}
|
||||
f.mtx.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func proxyChannel(newChannel ssh.NewChannel, target ssh.Conn) {
|
||||
targetChannel, targetRequests, err := target.OpenChannel(newChannel.ChannelType(), newChannel.ExtraData())
|
||||
if err != nil {
|
||||
if openErr, ok := err.(*ssh.OpenChannelError); ok {
|
||||
newChannel.Reject(openErr.Reason, openErr.Message)
|
||||
} else {
|
||||
newChannel.Reject(ssh.ConnectionFailed, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
sourceChannel, sourceRequests, err := newChannel.Accept()
|
||||
if err != nil {
|
||||
targetChannel.Close()
|
||||
return
|
||||
}
|
||||
go proxyChannelRequests(sourceRequests, targetChannel)
|
||||
go io.Copy(targetChannel.Stderr(), sourceChannel.Stderr())
|
||||
go io.Copy(sourceChannel.Stderr(), targetChannel.Stderr())
|
||||
go func() {
|
||||
io.Copy(targetChannel, sourceChannel)
|
||||
targetChannel.CloseWrite()
|
||||
}()
|
||||
go func() {
|
||||
io.Copy(sourceChannel, targetChannel)
|
||||
sourceChannel.CloseWrite()
|
||||
}()
|
||||
proxyChannelRequests(targetRequests, sourceChannel)
|
||||
sourceChannel.Close()
|
||||
targetChannel.Close()
|
||||
}
|
||||
|
||||
func proxyGlobalRequests(requests <-chan *ssh.Request, target ssh.Conn) {
|
||||
for request := range requests {
|
||||
if request.Type == "hostkeys-00@openssh.com" {
|
||||
if request.WantReply {
|
||||
request.Reply(false, nil)
|
||||
}
|
||||
continue
|
||||
}
|
||||
ok, payload, err := target.SendRequest(request.Type, request.WantReply, request.Payload)
|
||||
if request.WantReply {
|
||||
if err != nil {
|
||||
request.Reply(false, nil)
|
||||
} else {
|
||||
request.Reply(ok, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func proxyDownstreamRequests(requests <-chan *ssh.Request, target ssh.Conn) {
|
||||
for request := range requests {
|
||||
switch request.Type {
|
||||
case "no-more-sessions@openssh.com", "hostkeys-prove-00@openssh.com":
|
||||
if request.WantReply {
|
||||
request.Reply(false, nil)
|
||||
}
|
||||
continue
|
||||
}
|
||||
ok, payload, err := target.SendRequest(request.Type, request.WantReply, request.Payload)
|
||||
if request.WantReply {
|
||||
if err != nil {
|
||||
request.Reply(false, nil)
|
||||
} else {
|
||||
request.Reply(ok, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func proxyChannelRequests(requests <-chan *ssh.Request, target ssh.Channel) {
|
||||
for request := range requests {
|
||||
ok, err := target.SendRequest(request.Type, request.WantReply, request.Payload)
|
||||
if request.WantReply {
|
||||
if err != nil {
|
||||
request.Reply(false, nil)
|
||||
} else {
|
||||
request.Reply(ok, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
152
protocol/ssh/inbound.go
Normal file
152
protocol/ssh/inbound.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.SSHInboundOptions](registry, C.TypeSSH, NewInbound)
|
||||
}
|
||||
|
||||
var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
|
||||
|
||||
type Inbound struct {
|
||||
inbound.Adapter
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
serverConfig *ssh.ServerConfig
|
||||
service Service
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHInboundOptions) (adapter.Inbound, error) {
|
||||
if len(options.Users) == 0 && options.Fallback == nil {
|
||||
return nil, E.New("missing users")
|
||||
}
|
||||
inbound := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeSSH, tag),
|
||||
logger: logger,
|
||||
}
|
||||
defaultService := newService(router, logger, options.Users)
|
||||
if options.Fallback != nil {
|
||||
fallback, err := NewFallback(ctx, logger, defaultService, options.Fallback)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbound.service = fallback
|
||||
} else {
|
||||
inbound.service = defaultService
|
||||
}
|
||||
serverVersion := options.ServerVersion
|
||||
if serverVersion == "" {
|
||||
serverVersion = "SSH-2.0-OpenSSH_9.6"
|
||||
}
|
||||
serverConfig := &ssh.ServerConfig{
|
||||
ServerVersion: serverVersion,
|
||||
MaxAuthTries: options.MaxAuthTries,
|
||||
PasswordCallback: inbound.service.PasswordCallback,
|
||||
PublicKeyCallback: inbound.service.PublicKeyCallback,
|
||||
}
|
||||
var hostKeys []ssh.Signer
|
||||
for _, hostKey := range options.HostKey {
|
||||
signer, err := ssh.ParsePrivateKey([]byte(hostKey))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse host key")
|
||||
}
|
||||
hostKeys = append(hostKeys, signer)
|
||||
}
|
||||
for _, hostKeyPath := range options.HostKeyPath {
|
||||
content, err := os.ReadFile(os.ExpandEnv(hostKeyPath))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read host key ", hostKeyPath)
|
||||
}
|
||||
signer, err := ssh.ParsePrivateKey(content)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse host key ", hostKeyPath)
|
||||
}
|
||||
hostKeys = append(hostKeys, signer)
|
||||
}
|
||||
if len(hostKeys) == 0 {
|
||||
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "generate host key")
|
||||
}
|
||||
signer, err := ssh.NewSignerFromSigner(privateKey)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "generate host key")
|
||||
}
|
||||
hostKeys = append(hostKeys, signer)
|
||||
}
|
||||
for _, hostKey := range hostKeys {
|
||||
serverConfig.AddHostKey(hostKey)
|
||||
}
|
||||
inbound.serverConfig = serverConfig
|
||||
inbound.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
ConnectionHandler: inbound,
|
||||
})
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return E.Errors(h.service.Close(), h.listener.Close())
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []option.SSHUser) {
|
||||
h.service.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
metadata.Inbound = h.Tag()
|
||||
metadata.InboundType = h.Type()
|
||||
serverConn, channels, requests, err := ssh.NewServerConn(conn, h.serverConfig)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
h.logger.DebugContext(ctx, "connection closed: ", err)
|
||||
} else {
|
||||
h.logger.DebugContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
|
||||
}
|
||||
return
|
||||
}
|
||||
var user string
|
||||
if serverConn.Permissions != nil {
|
||||
user = serverConn.Permissions.Extensions["user"]
|
||||
}
|
||||
if user == "" {
|
||||
user = serverConn.User()
|
||||
}
|
||||
if user != "" {
|
||||
metadata.User = user
|
||||
}
|
||||
go func() {
|
||||
serverConn.Wait()
|
||||
conn.Close()
|
||||
}()
|
||||
h.service.Handle(ctx, serverConn, channels, requests, metadata, user)
|
||||
}
|
||||
165
protocol/ssh/service.go
Normal file
165
protocol/ssh/service.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/bufio/deadline"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error)
|
||||
PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
|
||||
UpdateUsers(users []option.SSHUser)
|
||||
Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string)
|
||||
Close() error
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
users []option.SSHUser
|
||||
mtx sync.RWMutex
|
||||
}
|
||||
|
||||
func newService(router adapter.ConnectionRouterEx, logger logger.ContextLogger, users []option.SSHUser) *service {
|
||||
return &service{
|
||||
router: router,
|
||||
logger: logger,
|
||||
users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *service) PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
h.mtx.RLock()
|
||||
users := h.users
|
||||
h.mtx.RUnlock()
|
||||
for _, user := range users {
|
||||
if user.Name != "" && user.Name != conn.User() {
|
||||
continue
|
||||
}
|
||||
if user.Password != "" && user.Password == string(password) {
|
||||
return &ssh.Permissions{Extensions: map[string]string{"user": user.Name}}, nil
|
||||
}
|
||||
}
|
||||
return nil, E.New("password authentication failed for user ", conn.User())
|
||||
}
|
||||
|
||||
func (h *service) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
h.mtx.RLock()
|
||||
users := h.users
|
||||
h.mtx.RUnlock()
|
||||
for _, user := range users {
|
||||
if user.Name != "" && user.Name != conn.User() {
|
||||
continue
|
||||
}
|
||||
for _, authorizedKey := range user.AuthorizedKeys {
|
||||
parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(authorizedKey))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(parsed.Marshal(), key.Marshal()) {
|
||||
return &ssh.Permissions{Extensions: map[string]string{"user": user.Name}}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, E.New("public key authentication failed for user ", conn.User())
|
||||
}
|
||||
|
||||
func (h *service) UpdateUsers(users []option.SSHUser) {
|
||||
h.mtx.Lock()
|
||||
h.users = users
|
||||
h.mtx.Unlock()
|
||||
}
|
||||
|
||||
func (h *service) Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string) {
|
||||
h.logger.InfoContext(ctx, "[", user, "] authenticated SSH connection from ", metadata.Source)
|
||||
go ssh.DiscardRequests(requests)
|
||||
for newChannel := range channels {
|
||||
switch newChannel.ChannelType() {
|
||||
case "direct-tcpip":
|
||||
go h.handleDirectChannel(ctx, metadata, newChannel)
|
||||
default:
|
||||
newChannel.Reject(ssh.UnknownChannelType, "only direct-tcpip is supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *service) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *service) handleDirectChannel(ctx context.Context, metadata adapter.InboundContext, newChannel ssh.NewChannel) {
|
||||
var payload directTCPIPData
|
||||
if err := ssh.Unmarshal(newChannel.ExtraData(), &payload); err != nil {
|
||||
newChannel.Reject(ssh.ConnectionFailed, "invalid direct-tcpip payload")
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "parse direct-tcpip payload"))
|
||||
return
|
||||
}
|
||||
channel, requests, err := newChannel.Accept()
|
||||
if err != nil {
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "accept direct-tcpip channel"))
|
||||
return
|
||||
}
|
||||
go ssh.DiscardRequests(requests)
|
||||
connMetadata := metadata
|
||||
connMetadata.Destination = M.ParseSocksaddrHostPort(payload.HostToConnect, uint16(payload.PortToConnect))
|
||||
conn := deadline.NewConn(&channelConn{
|
||||
Channel: channel,
|
||||
localAddr: metadata.OriginDestination.TCPAddr(),
|
||||
remoteAddr: metadata.Source.TCPAddr(),
|
||||
})
|
||||
h.logger.InfoContext(ctx, "[", metadata.User, "] inbound connection to ", connMetadata.Destination)
|
||||
h.router.RouteConnectionEx(ctx, conn, connMetadata, N.OnceClose(func(it error) {
|
||||
channel.Close()
|
||||
}))
|
||||
}
|
||||
|
||||
type directTCPIPData struct {
|
||||
HostToConnect string
|
||||
PortToConnect uint32
|
||||
OriginatorAddress string
|
||||
OriginatorPort uint32
|
||||
}
|
||||
|
||||
type channelConn struct {
|
||||
ssh.Channel
|
||||
localAddr net.Addr
|
||||
remoteAddr net.Addr
|
||||
}
|
||||
|
||||
func (c *channelConn) LocalAddr() net.Addr {
|
||||
return c.localAddr
|
||||
}
|
||||
|
||||
func (c *channelConn) RemoteAddr() net.Addr {
|
||||
return c.remoteAddr
|
||||
}
|
||||
|
||||
func (c *channelConn) SetDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *channelConn) SetReadDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *channelConn) SetWriteDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
@@ -151,10 +151,10 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
|
||||
}
|
||||
|
||||
if len(defaultResolvers) > 0 {
|
||||
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ",
|
||||
t.logger.Notice("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ",
|
||||
strings.Join(common.Map(dnsConfig.DefaultResolvers, func(it *dnstype.Resolver) string { return it.Addr }), " "))
|
||||
} else {
|
||||
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts")
|
||||
t.logger.Notice("updated ", len(routes), " routes, ", len(hosts), " hosts")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -435,7 +435,7 @@ func (t *Endpoint) watchState() {
|
||||
}
|
||||
authURL := localBackend.StatusWithoutPeers().AuthURL
|
||||
if authURL != "" {
|
||||
t.logger.Info("Waiting for authentication: ", authURL)
|
||||
t.logger.Notice("Waiting for authentication: ", authURL)
|
||||
if t.platformInterface != nil {
|
||||
err := t.platformInterface.SendNotification(&adapter.Notification{
|
||||
Identifier: "tailscale-authentication",
|
||||
|
||||
@@ -388,7 +388,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
|
||||
return err
|
||||
}
|
||||
t.tunStack = tunStack
|
||||
t.logger.Info("started at ", t.tunOptions.Name)
|
||||
t.logger.Notice("started at ", t.tunOptions.Name)
|
||||
case adapter.StartStatePostStart:
|
||||
monitor := taskmonitor.New(t.logger, C.StartTimeout)
|
||||
monitor.Start("starting tun stack")
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
stdtls "crypto/tls"
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -96,7 +95,10 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
|
||||
}
|
||||
}
|
||||
// Parse encryption configuration
|
||||
muxOpts := common.PtrValueOrDefault(options.Multiplex)
|
||||
if muxOpts.Enabled {
|
||||
options.Flow = ""
|
||||
}
|
||||
if options.Encryption != "" && options.Encryption != "none" {
|
||||
encryptionConfig, err := parseClientEncryption(options.Encryption)
|
||||
if err != nil {
|
||||
@@ -109,10 +111,6 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
logger.Debug("encryption initialized: keys=", len(encryptionConfig.keys), " xorMode=", encryptionConfig.xorMode, " seconds=", encryptionConfig.seconds, " padding=", encryptionConfig.padding)
|
||||
}
|
||||
|
||||
muxOpts := common.PtrValueOrDefault(options.Multiplex)
|
||||
if muxOpts.Enabled {
|
||||
options.Flow = ""
|
||||
}
|
||||
outbound.client, err = vless.NewClient(options.UUID, options.Flow, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -191,7 +189,6 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
conn, err = h.transport.DialContext(ctx)
|
||||
if err == nil && h.vision {
|
||||
if baseConn == nil {
|
||||
// Only set baseConn if the transport delivered a TLS-capable connection
|
||||
if isVisionTLSConn(conn) {
|
||||
h.logger.Warn("Vision enabled but hook was not called by transport, using fallback")
|
||||
baseConn = conn
|
||||
@@ -210,7 +207,6 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply encryption if configured
|
||||
if h.encryption != nil {
|
||||
conn, err = h.encryption.Handshake(conn)
|
||||
if err != nil {
|
||||
@@ -218,36 +214,12 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
}
|
||||
}
|
||||
|
||||
// For Vision: wrap the connection to expose the TLS/encryption connection for vless client
|
||||
var visionBaseConn net.Conn // The connection to pass to Vision (TLS or encryption layer)
|
||||
var visionBaseConn net.Conn
|
||||
var visionCanSplice bool
|
||||
if h.vision {
|
||||
isRAWTransport := h.transport == nil
|
||||
|
||||
if baseConn != nil && !isVisionTLSConn(baseConn) {
|
||||
baseConn = nil
|
||||
}
|
||||
if baseConn != nil {
|
||||
// Has TLS/Reality: use baseConn (TLS connection)
|
||||
visionBaseConn = baseConn
|
||||
visionCanSplice = isRAWTransport
|
||||
conn = newVisionConnWrapper(conn, baseConn)
|
||||
} else if h.encryption != nil {
|
||||
// Only has encryption (no TLS/Reality): use encryption layer itself
|
||||
encConn := findEncryptionLayer(conn)
|
||||
if encConn != nil {
|
||||
visionBaseConn = encConn
|
||||
if h.encryption.IsFullRandomXorMode() {
|
||||
visionCanSplice = false
|
||||
} else {
|
||||
visionCanSplice = isRAWTransport
|
||||
}
|
||||
conn = newVisionConnWrapper(conn, encConn)
|
||||
} else {
|
||||
return nil, E.New("Vision: failed to find encryption layer")
|
||||
}
|
||||
} else {
|
||||
return nil, E.New("Vision requires either TLS/Reality or Encryption")
|
||||
conn, visionBaseConn, visionCanSplice, err = h.setupVision(conn, baseConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,8 +227,6 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
case N.NetworkTCP:
|
||||
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||
if h.vision && visionBaseConn != nil {
|
||||
// For Vision, we need to pass the base connection (TLS or encryption layer)
|
||||
// to prepareConn so it can properly initialize VisionConn
|
||||
return h.client.DialEarlyConnWithOptions(conn, visionBaseConn, destination, visionCanSplice)
|
||||
}
|
||||
return h.client.DialEarlyConn(conn, destination)
|
||||
@@ -281,6 +251,29 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
}
|
||||
}
|
||||
|
||||
func (h *vlessDialer) setupVision(conn net.Conn, baseConn net.Conn) (net.Conn, net.Conn, bool, error) {
|
||||
isRAWTransport := h.transport == nil
|
||||
|
||||
if baseConn != nil && !isVisionTLSConn(baseConn) {
|
||||
baseConn = nil
|
||||
}
|
||||
|
||||
if baseConn != nil {
|
||||
return newVisionConnWrapper(conn, baseConn), baseConn, isRAWTransport, nil
|
||||
}
|
||||
|
||||
if h.encryption != nil {
|
||||
encConn := findEncryptionLayer(conn)
|
||||
if encConn == nil {
|
||||
return nil, nil, false, E.New("Vision: failed to find encryption layer")
|
||||
}
|
||||
canSplice := isRAWTransport && !h.encryption.IsFullRandomXorMode()
|
||||
return newVisionConnWrapper(conn, encConn), encConn, canSplice, nil
|
||||
}
|
||||
|
||||
return nil, nil, false, E.New("Vision requires either TLS/Reality or Encryption")
|
||||
}
|
||||
|
||||
func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
@@ -299,7 +292,6 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
|
||||
common.Close(conn)
|
||||
return nil, err
|
||||
}
|
||||
// Apply encryption if configured
|
||||
if h.encryption != nil {
|
||||
conn, err = h.encryption.Handshake(conn)
|
||||
if err != nil {
|
||||
@@ -362,7 +354,6 @@ func (c *visionConnWrapper) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// isVisionTLSConn returns true when the provided connection exposes TLS semantics Vision expects.
|
||||
func isVisionTLSConn(conn net.Conn) bool {
|
||||
if conn == nil {
|
||||
return false
|
||||
@@ -373,16 +364,6 @@ func isVisionTLSConn(conn net.Conn) bool {
|
||||
if _, ok := conn.(interface{ Handshake() error }); ok {
|
||||
return true
|
||||
}
|
||||
connType := reflect.TypeOf(conn)
|
||||
if connType == nil {
|
||||
return false
|
||||
}
|
||||
if connType.Kind() == reflect.Ptr {
|
||||
pkgPath := connType.Elem().PkgPath()
|
||||
if pkgPath == "crypto/tls" || strings.Contains(pkgPath, "utls") || strings.Contains(pkgPath, "shadowtls") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user