mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-05 13:25:17 +03:00
Add OpenVPN, TrustTunnel, Sudoku, inbound managers. Fixes
This commit is contained in:
323
transport/trusttunnel/client.go
Normal file
323
transport/trusttunnel/client.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package trusttunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdtls "crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
qtls "github.com/sagernet/sing-quic"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var (
|
||||
appName = "sing-box"
|
||||
appVersion = C.Version
|
||||
tcpUserAgent = runtime.GOOS + " " + appName + "/" + appVersion
|
||||
udpUserAgent = runtime.GOOS + " " + UDPMagicAddress
|
||||
icmpUserAgent = runtime.GOOS + " " + ICMPMagicAddress
|
||||
)
|
||||
|
||||
type Dialer interface {
|
||||
Dial(ctx context.Context, host string) (net.Conn, error)
|
||||
ListenPacket(ctx context.Context) (net.PacketConn, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
TLSDialer tls.Dialer
|
||||
QUICDialer N.Dialer
|
||||
QUICTLSConfig tls.Config
|
||||
Server M.Socksaddr
|
||||
Username string
|
||||
Password string
|
||||
QUIC bool
|
||||
CongestionControl string
|
||||
CWND int
|
||||
BBRProfile string
|
||||
HealthCheck bool
|
||||
MaxConnections int
|
||||
MinStreams int
|
||||
MaxStreams int
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
server M.Socksaddr
|
||||
serverString string
|
||||
auth string
|
||||
roundTripper http.RoundTripper
|
||||
startOnce sync.Once
|
||||
healthCheck bool
|
||||
healthCheckTimer *time.Timer
|
||||
count atomic.Int64
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, options ClientOptions) (*Client, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
client := &Client{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
server: options.Server,
|
||||
serverString: options.Server.String(),
|
||||
auth: buildAuth(options.Username, options.Password),
|
||||
healthCheck: options.HealthCheck,
|
||||
}
|
||||
if options.QUIC {
|
||||
congestionControlFactory, err := NewCongestionControl(options.CongestionControl, options.CWND, options.BBRProfile, ntp.TimeFuncFromContext(ctx))
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
client.roundTripper = &http3.Transport{
|
||||
QUICConfig: &quic.Config{
|
||||
MaxIdleTimeout: DefaultSessionTimeout * 2,
|
||||
KeepAlivePeriod: DefaultHealthCheckTimeout,
|
||||
},
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *stdtls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
udpConn, err := options.QUICDialer.DialContext(ctx, N.NetworkUDP, client.server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := qtls.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), options.QUICTLSConfig, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn.SetCongestionControl(congestionControlFactory(conn))
|
||||
return conn, nil
|
||||
},
|
||||
}
|
||||
} else {
|
||||
client.roundTripper = &http2.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, _ *stdtls.Config) (net.Conn, error) {
|
||||
return options.TLSDialer.DialContext(ctx, network, client.server)
|
||||
},
|
||||
AllowHTTP: true,
|
||||
}
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) start() {
|
||||
if c.healthCheck {
|
||||
c.healthCheckTimer = time.NewTimer(DefaultHealthCheckTimeout)
|
||||
go c.loopHealthCheck()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) loopHealthCheck() {
|
||||
for {
|
||||
select {
|
||||
case <-c.healthCheckTimer.C:
|
||||
case <-c.ctx.Done():
|
||||
c.healthCheckTimer.Stop()
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(c.ctx, DefaultHealthCheckTimeout)
|
||||
_ = c.HealthCheck(ctx)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) resetHealthCheckTimer() {
|
||||
if c.healthCheckTimer == nil {
|
||||
return
|
||||
}
|
||||
c.healthCheckTimer.Reset(DefaultHealthCheckTimeout)
|
||||
}
|
||||
|
||||
func (c *Client) roundTrip(request *http.Request, conn *httpConn) {
|
||||
c.startOnce.Do(c.start)
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
request.Body = pipeReader
|
||||
*conn = httpConn{writer: pipeWriter, created: make(chan struct{})}
|
||||
c.count.Add(1)
|
||||
conn.closeFn = sync.OnceFunc(func() { c.count.Add(-1) })
|
||||
ctx, cancel := context.WithCancel(c.ctx)
|
||||
conn.cancelFn = cancel
|
||||
go func() {
|
||||
timeout := time.AfterFunc(C.TCPTimeout, cancel)
|
||||
defer timeout.Stop()
|
||||
request = request.WithContext(ctx)
|
||||
response, err := c.roundTripper.RoundTrip(request)
|
||||
if err != nil {
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
_ = pipeReader.CloseWithError(err)
|
||||
conn.setup(nil, err)
|
||||
} else if response.StatusCode != http.StatusOK {
|
||||
_ = response.Body.Close()
|
||||
err = fmt.Errorf("unexpected status code: %d", response.StatusCode)
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
_ = pipeReader.CloseWithError(err)
|
||||
conn.setup(nil, err)
|
||||
} else {
|
||||
c.resetHealthCheckTimer()
|
||||
conn.setup(response.Body, nil)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *Client) newConnectRequest(host, userAgent string) *http.Request {
|
||||
return &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Scheme: "https", Host: c.serverString},
|
||||
Header: http.Header{
|
||||
"User-Agent": {userAgent},
|
||||
"Proxy-Authorization": {c.auth},
|
||||
},
|
||||
Host: host,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Dial(ctx context.Context, host string) (net.Conn, error) {
|
||||
conn := &tcpConn{}
|
||||
c.roundTrip(c.newConnectRequest(host, tcpUserAgent), &conn.httpConn)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) {
|
||||
conn := &clientPacketConn{}
|
||||
c.roundTrip(c.newConnectRequest(UDPMagicAddress, udpUserAgent), &conn.httpConn)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
c.cancel()
|
||||
if closer, ok := c.roundTripper.(io.Closer); ok {
|
||||
_ = closer.Close()
|
||||
}
|
||||
if t, ok := c.roundTripper.(*http2.Transport); ok {
|
||||
t.CloseIdleConnections()
|
||||
}
|
||||
if c.healthCheckTimer != nil {
|
||||
c.healthCheckTimer.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) HealthCheck(ctx context.Context) error {
|
||||
defer c.resetHealthCheckTimer()
|
||||
response, err := c.roundTripper.RoundTrip(c.newConnectRequest(HealthCheckMagicAddress, runtime.GOOS).WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code: %d", response.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MultiplexClient struct {
|
||||
mutex sync.Mutex
|
||||
maxConnections int
|
||||
minStreams int
|
||||
maxStreams int
|
||||
ctx context.Context
|
||||
options ClientOptions
|
||||
clients []*Client
|
||||
}
|
||||
|
||||
func NewMultiplexClient(ctx context.Context, options ClientOptions) (*MultiplexClient, error) {
|
||||
maxConnections := options.MaxConnections
|
||||
minStreams := options.MinStreams
|
||||
maxStreams := options.MaxStreams
|
||||
if maxConnections == 0 && minStreams == 0 && maxStreams == 0 {
|
||||
maxConnections = 8
|
||||
minStreams = 5
|
||||
}
|
||||
client, err := NewClient(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MultiplexClient{
|
||||
maxConnections: maxConnections,
|
||||
minStreams: minStreams,
|
||||
maxStreams: maxStreams,
|
||||
ctx: ctx,
|
||||
options: options,
|
||||
clients: []*Client{client},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *MultiplexClient) Dial(ctx context.Context, host string) (net.Conn, error) {
|
||||
t, err := c.getClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.Dial(ctx, host)
|
||||
}
|
||||
|
||||
func (c *MultiplexClient) ListenPacket(ctx context.Context) (net.PacketConn, error) {
|
||||
t, err := c.getClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.ListenPacket(ctx)
|
||||
}
|
||||
|
||||
func (c *MultiplexClient) Close() error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
var errs []error
|
||||
for _, t := range c.clients {
|
||||
if err := t.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
c.clients = nil
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (c *MultiplexClient) getClient() (*Client, error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
var transport *Client
|
||||
for _, t := range c.clients {
|
||||
if transport == nil || t.count.Load() < transport.count.Load() {
|
||||
transport = t
|
||||
}
|
||||
}
|
||||
if transport == nil {
|
||||
return c.newClientLocked()
|
||||
}
|
||||
numStreams := int(transport.count.Load())
|
||||
if numStreams == 0 {
|
||||
return transport, nil
|
||||
}
|
||||
if c.maxConnections > 0 {
|
||||
if len(c.clients) >= c.maxConnections || numStreams < c.minStreams {
|
||||
return transport, nil
|
||||
}
|
||||
} else if c.maxStreams > 0 && numStreams < c.maxStreams {
|
||||
return transport, nil
|
||||
}
|
||||
return c.newClientLocked()
|
||||
}
|
||||
|
||||
func (c *MultiplexClient) newClientLocked() (*Client, error) {
|
||||
t, err := NewClient(c.ctx, c.options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.clients = append(c.clients, t)
|
||||
return t, nil
|
||||
}
|
||||
Reference in New Issue
Block a user