mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-05 10:47:32 +03:00
294 lines
7.1 KiB
Go
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
|
|
}
|
|
}
|