Files
sing-box-extended/transport/sudoku/client.go
2026-06-04 07:54:26 +03:00

294 lines
7.1 KiB
Go

package sudoku
import (
"context"
"fmt"
"net"
"strings"
"sync"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/transport/sudoku/obfs/httpmask"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type ClientOptions struct {
Dialer N.Dialer
TLSConfig tls.Config
Config ProtocolConfig
}
type Client struct {
dialer N.Dialer
tlsConfig tls.Config
baseConf ProtocolConfig
muxMu sync.Mutex
muxClient *MultiplexClient
httpMaskMu sync.Mutex
httpMaskClient *httpmask.TunnelClient
httpMaskKey string
}
func NewClient(options ClientOptions) *Client {
return &Client{
dialer: options.Dialer,
tlsConfig: options.TLSConfig,
baseConf: options.Config,
}
}
func (c *Client) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
cfg := c.baseConf
cfg.TargetAddress = destination.String()
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
if muxMode == "on" && !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
stream, err := c.dialMultiplex(ctx, cfg.TargetAddress)
if err == nil {
return stream, nil
}
return nil, err
}
conn, err := c.dialAndHandshake(ctx, &cfg)
if err != nil {
return nil, err
}
switch N.NetworkName(network) {
case N.NetworkUDP:
if err = WriteKIPMessage(conn, KIPTypeStartUoT, nil); err != nil {
conn.Close()
return nil, E.Cause(err, "start uot")
}
return conn, nil
default:
addrBuf, err := EncodeAddress(cfg.TargetAddress)
if err != nil {
conn.Close()
return nil, E.Cause(err, "encode target address")
}
if err = WriteKIPMessage(conn, KIPTypeOpenTCP, addrBuf); err != nil {
conn.Close()
return nil, E.Cause(err, "send target address")
}
return conn, nil
}
}
func (c *Client) Close() {
c.resetMuxClient()
c.resetHTTPMaskClient()
}
func (c *Client) dialAndHandshake(ctx context.Context, cfg *ProtocolConfig) (net.Conn, error) {
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask && httpTunnelModeEnabled(handshakeCfg.HTTPMaskMode) {
handshakeCfg.DisableHTTPMask = true
}
upgrade := func(raw net.Conn) (net.Conn, error) {
return ClientHandshake(raw, &handshakeCfg)
}
var conn net.Conn
var err error
var handshakeDone bool
if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
if muxMode == "auto" && strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) != "ws" {
if client, cerr := c.getOrCreateHTTPMaskClient(cfg); cerr == nil && client != nil {
conn, err = client.DialTunnel(ctx, httpmask.TunnelDialOptions{
Mode: cfg.HTTPMaskMode,
TLSConfig: c.httpMaskTLSConfig(),
HostOverride: cfg.HTTPMaskHost,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ClientAEADSeed(cfg.Key),
Upgrade: upgrade,
Multiplex: cfg.HTTPMaskMultiplex,
DialContext: c.dialRaw,
})
if err != nil {
c.resetHTTPMaskClient()
}
}
}
if conn == nil && err == nil {
conn, err = c.dialHTTPMaskTunnel(ctx, cfg, upgrade)
}
if err == nil && conn != nil {
handshakeDone = true
}
}
if conn == nil && err == nil {
conn, err = c.dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(cfg.ServerAddress))
}
if err != nil {
return nil, E.Cause(err, "connect to ", cfg.ServerAddress)
}
if !handshakeDone {
conn, err = ClientHandshake(conn, &handshakeCfg)
if err != nil {
common.Close(conn)
return nil, err
}
}
return conn, nil
}
func (c *Client) dialRaw(ctx context.Context, network, addr string) (net.Conn, error) {
return c.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
}
func (c *Client) dialHTTPMaskTunnel(ctx context.Context, cfg *ProtocolConfig, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
var earlyHandshake *httpmask.ClientEarlyHandshake
var err error
if upgrade != nil {
earlyHandshake, err = newClientHTTPMaskEarlyHandshake(cfg)
if err != nil {
return nil, err
}
}
return httpmask.DialTunnel(ctx, cfg.ServerAddress, httpmask.TunnelDialOptions{
Mode: cfg.HTTPMaskMode,
TLSConfig: c.httpMaskTLSConfig(),
HostOverride: cfg.HTTPMaskHost,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ClientAEADSeed(cfg.Key),
EarlyHandshake: earlyHandshake,
Upgrade: upgrade,
Multiplex: cfg.HTTPMaskMultiplex,
DialContext: c.dialRaw,
})
}
func (c *Client) httpMaskTLSConfig() tls.Config {
if c.tlsConfig == nil {
return nil
}
return c.tlsConfig
}
func (c *Client) dialMultiplex(ctx context.Context, targetAddress string) (net.Conn, error) {
for attempt := 0; attempt < 2; attempt++ {
client, err := c.getOrCreateMuxClient(ctx)
if err != nil {
return nil, err
}
stream, err := client.Dial(ctx, targetAddress)
if err != nil {
c.resetMuxClient()
continue
}
return stream, nil
}
return nil, fmt.Errorf("multiplex open stream failed")
}
func (c *Client) getOrCreateMuxClient(ctx context.Context) (*MultiplexClient, error) {
c.muxMu.Lock()
defer c.muxMu.Unlock()
if c.muxClient != nil && !c.muxClient.IsClosed() {
return c.muxClient, nil
}
baseCfg := c.baseConf
baseConn, err := c.dialAndHandshake(ctx, &baseCfg)
if err != nil {
return nil, err
}
client, err := StartMultiplexClient(baseConn)
if err != nil {
baseConn.Close()
return nil, err
}
c.muxClient = client
return client, nil
}
func (c *Client) resetMuxClient() {
c.muxMu.Lock()
defer c.muxMu.Unlock()
if c.muxClient != nil {
c.muxClient.Close()
c.muxClient = nil
}
}
func (c *Client) getOrCreateHTTPMaskClient(cfg *ProtocolConfig) (*httpmask.TunnelClient, error) {
key := cfg.ServerAddress + "|" + fmt.Sprint(c.tlsConfig != nil) + "|" + strings.TrimSpace(cfg.HTTPMaskHost)
c.httpMaskMu.Lock()
if c.httpMaskClient != nil && c.httpMaskKey == key {
client := c.httpMaskClient
c.httpMaskMu.Unlock()
return client, nil
}
c.httpMaskMu.Unlock()
client, err := httpmask.NewTunnelClient(cfg.ServerAddress, httpmask.TunnelClientOptions{
TLSConfig: c.httpMaskTLSConfig(),
HostOverride: cfg.HTTPMaskHost,
DialContext: c.dialRaw,
MaxIdleConns: 32,
})
if err != nil {
return nil, err
}
c.httpMaskMu.Lock()
defer c.httpMaskMu.Unlock()
if c.httpMaskClient != nil && c.httpMaskKey == key {
client.CloseIdleConnections()
return c.httpMaskClient, nil
}
if c.httpMaskClient != nil {
c.httpMaskClient.CloseIdleConnections()
}
c.httpMaskClient = client
c.httpMaskKey = key
return client, nil
}
func (c *Client) resetHTTPMaskClient() {
c.httpMaskMu.Lock()
defer c.httpMaskMu.Unlock()
if c.httpMaskClient != nil {
c.httpMaskClient.CloseIdleConnections()
c.httpMaskClient = nil
c.httpMaskKey = ""
}
}
func normalizeHTTPMaskMultiplex(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "", "off":
return "off"
case "auto":
return "auto"
case "on":
return "on"
default:
return "off"
}
}
func httpTunnelModeEnabled(mode string) bool {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "stream", "poll", "auto", "ws":
return true
default:
return false
}
}