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
|
||||
}
|
||||
62
transport/trusttunnel/icmp.go
Normal file
62
transport/trusttunnel/icmp.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package trusttunnel
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
)
|
||||
|
||||
type IcmpConn struct {
|
||||
httpConn
|
||||
}
|
||||
|
||||
func (i *IcmpConn) WritePing(id uint16, destination netip.Addr, sequenceNumber uint16, ttl uint8, size uint16) error {
|
||||
request := buf.NewSize(2 + 16 + 2 + 1 + 2)
|
||||
defer request.Release()
|
||||
must(binary.Write(request, binary.BigEndian, id))
|
||||
destinationAddress := buildPaddingIP(destination)
|
||||
must1(request.Write(destinationAddress[:]))
|
||||
must(binary.Write(request, binary.BigEndian, sequenceNumber))
|
||||
must(binary.Write(request, binary.BigEndian, ttl))
|
||||
must(binary.Write(request, binary.BigEndian, size))
|
||||
_, err := i.writeFlush(request.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *IcmpConn) ReadPing() (id uint16, sourceAddress netip.Addr, icmpType uint8, code uint8, sequenceNumber uint16, err error) {
|
||||
err = i.waitCreated()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
response := buf.NewSize(2 + 16 + 1 + 1 + 2)
|
||||
defer response.Release()
|
||||
_, err = response.ReadFullFrom(i.body, response.FreeLen())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
must(binary.Read(response, binary.BigEndian, &id))
|
||||
var sourceAddressBuffer [16]byte
|
||||
must1(response.Read(sourceAddressBuffer[:]))
|
||||
sourceAddress = parse16BytesIP(sourceAddressBuffer)
|
||||
must(binary.Read(response, binary.BigEndian, &icmpType))
|
||||
must(binary.Read(response, binary.BigEndian, &code))
|
||||
must(binary.Read(response, binary.BigEndian, &sequenceNumber))
|
||||
return
|
||||
}
|
||||
|
||||
func (i *IcmpConn) Close() error {
|
||||
return i.httpConn.Close()
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func must1[T any](_ T, err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
210
transport/trusttunnel/packet.go
Normal file
210
transport/trusttunnel/packet.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package trusttunnel
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"net"
|
||||
|
||||
"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"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
var (
|
||||
_ N.NetPacketConn = (*clientPacketConn)(nil)
|
||||
_ N.FrontHeadroom = (*clientPacketConn)(nil)
|
||||
)
|
||||
|
||||
type clientPacketConn struct {
|
||||
httpConn
|
||||
}
|
||||
|
||||
func (u *clientPacketConn) FrontHeadroom() int {
|
||||
return 4 + 16 + 2 + 16 + 2 + 1 + math.MaxUint8
|
||||
}
|
||||
|
||||
func (u *clientPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
err = u.waitCreated()
|
||||
if err != nil {
|
||||
return M.Socksaddr{}, err
|
||||
}
|
||||
return u.readPacketFromServer(buffer)
|
||||
}
|
||||
|
||||
func (u *clientPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
buffer := buf.With(p)
|
||||
destination, err := u.ReadPacket(buffer)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return buffer.Len(), destination.UDPAddr(), nil
|
||||
}
|
||||
|
||||
func (u *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
return u.writePacketToServer(buffer, destination)
|
||||
}
|
||||
|
||||
func (u *clientPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
err = u.WritePacket(buf.As(p), M.SocksaddrFromNet(addr))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (u *clientPacketConn) readPacketFromServer(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
header := buf.NewSize(4 + 16 + 2 + 16 + 2)
|
||||
defer header.Release()
|
||||
_, err = header.ReadFullFrom(u.body, header.FreeLen())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var length uint32
|
||||
common.Must(binary.Read(header, binary.BigEndian, &length))
|
||||
var sourceAddressBuffer [16]byte
|
||||
common.Must1(header.Read(sourceAddressBuffer[:]))
|
||||
destination.Addr = parse16BytesIP(sourceAddressBuffer)
|
||||
common.Must(binary.Read(header, binary.BigEndian, &destination.Port))
|
||||
common.Must(rw.SkipN(header, 16+2))
|
||||
payloadLen := int(length) - (16 + 2 + 16 + 2)
|
||||
if payloadLen < 0 {
|
||||
return M.Socksaddr{}, E.New("invalid udp length: ", length)
|
||||
}
|
||||
_, err = buffer.ReadFullFrom(u.body, payloadLen)
|
||||
return
|
||||
}
|
||||
|
||||
func (u *clientPacketConn) writePacketToServer(buffer *buf.Buffer, source M.Socksaddr) error {
|
||||
defer buffer.Release()
|
||||
if !source.IsIP() {
|
||||
return E.New("only support IP")
|
||||
}
|
||||
payloadLen := buffer.Len()
|
||||
headerLen := 4 + 16 + 2 + 16 + 2 + 1 + len(appName)
|
||||
lengthField := uint32(16 + 2 + 16 + 2 + 1 + len(appName) + payloadLen)
|
||||
destinationAddress := buildPaddingIP(source.Addr)
|
||||
header := buf.NewSize(headerLen)
|
||||
defer header.Release()
|
||||
common.Must(binary.Write(header, binary.BigEndian, lengthField))
|
||||
common.Must(header.WriteZeroN(16 + 2))
|
||||
common.Must1(header.Write(destinationAddress[:]))
|
||||
common.Must(binary.Write(header, binary.BigEndian, source.Port))
|
||||
common.Must(binary.Write(header, binary.BigEndian, uint8(len(appName))))
|
||||
common.Must1(header.WriteString(appName))
|
||||
_, err := u.writer.Write(header.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = u.writer.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.flusher != nil {
|
||||
u.flusher.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ N.NetPacketConn = (*serverPacketConn)(nil)
|
||||
_ N.FrontHeadroom = (*serverPacketConn)(nil)
|
||||
)
|
||||
|
||||
type serverPacketConn struct {
|
||||
httpConn
|
||||
}
|
||||
|
||||
func (u *serverPacketConn) FrontHeadroom() int {
|
||||
return 4 + 16 + 2 + 16 + 2
|
||||
}
|
||||
|
||||
func (u *serverPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
err = u.waitCreated()
|
||||
if err != nil {
|
||||
return M.Socksaddr{}, err
|
||||
}
|
||||
return u.readPacketFromClient(buffer)
|
||||
}
|
||||
|
||||
func (u *serverPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
buffer := buf.With(p)
|
||||
destination, err := u.ReadPacket(buffer)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return buffer.Len(), destination.UDPAddr(), nil
|
||||
}
|
||||
|
||||
func (u *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
return u.writePacketToClient(buffer, destination)
|
||||
}
|
||||
|
||||
func (u *serverPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
err = u.WritePacket(buf.As(p), M.SocksaddrFromNet(addr))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (u *serverPacketConn) readPacketFromClient(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
header := buf.NewSize(4 + 16 + 2 + 16 + 2 + 1)
|
||||
defer header.Release()
|
||||
_, err = header.ReadFullFrom(u.body, header.FreeLen())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var length uint32
|
||||
common.Must(binary.Read(header, binary.BigEndian, &length))
|
||||
common.Must(rw.SkipN(header, 16+2))
|
||||
var destinationAddressBuffer [16]byte
|
||||
common.Must1(header.Read(destinationAddressBuffer[:]))
|
||||
destination.Addr = parse16BytesIP(destinationAddressBuffer)
|
||||
common.Must(binary.Read(header, binary.BigEndian, &destination.Port))
|
||||
var appNameLen uint8
|
||||
common.Must(binary.Read(header, binary.BigEndian, &appNameLen))
|
||||
if appNameLen > 0 {
|
||||
err = rw.SkipN(u.body, int(appNameLen))
|
||||
if err != nil {
|
||||
return M.Socksaddr{}, err
|
||||
}
|
||||
}
|
||||
payloadLen := int(length) - (16 + 2 + 16 + 2 + 1 + int(appNameLen))
|
||||
if payloadLen < 0 {
|
||||
return M.Socksaddr{}, E.New("invalid udp length: ", length)
|
||||
}
|
||||
_, err = buffer.ReadFullFrom(u.body, payloadLen)
|
||||
return
|
||||
}
|
||||
|
||||
func (u *serverPacketConn) writePacketToClient(buffer *buf.Buffer, source M.Socksaddr) error {
|
||||
defer buffer.Release()
|
||||
if !source.IsIP() {
|
||||
return E.New("only support IP")
|
||||
}
|
||||
payloadLen := buffer.Len()
|
||||
headerLen := 4 + 16 + 2 + 16 + 2
|
||||
lengthField := uint32(16 + 2 + 16 + 2 + payloadLen)
|
||||
sourceAddress := buildPaddingIP(source.Addr)
|
||||
header := buf.NewSize(headerLen)
|
||||
defer header.Release()
|
||||
common.Must(binary.Write(header, binary.BigEndian, lengthField))
|
||||
common.Must1(header.Write(sourceAddress[:]))
|
||||
common.Must(binary.Write(header, binary.BigEndian, source.Port))
|
||||
common.Must(header.WriteZeroN(16 + 2))
|
||||
_, err := u.writer.Write(header.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = u.writer.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.flusher != nil {
|
||||
u.flusher.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
174
transport/trusttunnel/protocol.go
Normal file
174
transport/trusttunnel/protocol.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package trusttunnel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
UDPMagicAddress = "_udp2"
|
||||
ICMPMagicAddress = "_icmp"
|
||||
HealthCheckMagicAddress = "_check"
|
||||
DefaultConnectionTimeout = 30 * time.Second
|
||||
DefaultHealthCheckTimeout = 7 * time.Second
|
||||
DefaultSessionTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
func buildAuth(username string, password string) string {
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
|
||||
}
|
||||
|
||||
func parseBasicAuth(auth string) (username, password string, ok bool) {
|
||||
const prefix = "Basic "
|
||||
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
|
||||
return "", "", false
|
||||
}
|
||||
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
cs := string(c)
|
||||
username, password, ok = strings.Cut(cs, ":")
|
||||
return
|
||||
}
|
||||
|
||||
func parse16BytesIP(buffer [16]byte) netip.Addr {
|
||||
var zeroPrefix [12]byte
|
||||
isIPv4 := bytes.HasPrefix(buffer[:], zeroPrefix[:])
|
||||
isIPv4 = isIPv4 && !(buffer[12] == 0 && buffer[13] == 0 && buffer[14] == 0 && buffer[15] == 1)
|
||||
if isIPv4 {
|
||||
return netip.AddrFrom4([4]byte(buffer[12:16]))
|
||||
}
|
||||
return netip.AddrFrom16(buffer)
|
||||
}
|
||||
|
||||
func buildPaddingIP(addr netip.Addr) (buffer [16]byte) {
|
||||
if addr.Is6() {
|
||||
return addr.As16()
|
||||
}
|
||||
ipv4 := addr.As4()
|
||||
copy(buffer[12:16], ipv4[:])
|
||||
return buffer
|
||||
}
|
||||
|
||||
type httpConn struct {
|
||||
writer io.Writer
|
||||
flusher http.Flusher
|
||||
body io.ReadCloser
|
||||
setupOnce sync.Once
|
||||
created chan struct{}
|
||||
createErr error
|
||||
cancelFn func()
|
||||
closeFn func()
|
||||
remoteAddr net.Addr
|
||||
localAddr net.Addr
|
||||
deadline *time.Timer
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (h *httpConn) setup(body io.ReadCloser, err error) {
|
||||
h.setupOnce.Do(func() {
|
||||
h.body = body
|
||||
h.createErr = err
|
||||
close(h.created)
|
||||
})
|
||||
if h.createErr != nil && body != nil {
|
||||
_ = body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpConn) waitCreated() error {
|
||||
<-h.created
|
||||
if h.body != nil {
|
||||
return nil
|
||||
}
|
||||
return h.createErr
|
||||
}
|
||||
|
||||
func (h *httpConn) Close() error {
|
||||
h.setup(nil, net.ErrClosed)
|
||||
if closer, ok := h.writer.(io.Closer); ok {
|
||||
_ = closer.Close()
|
||||
}
|
||||
if h.body != nil {
|
||||
_ = h.body.Close()
|
||||
}
|
||||
if h.cancelFn != nil {
|
||||
h.cancelFn()
|
||||
}
|
||||
if h.closeFn != nil {
|
||||
h.closeFn()
|
||||
}
|
||||
if h.done != nil {
|
||||
select {
|
||||
case <-h.done:
|
||||
default:
|
||||
close(h.done)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpConn) writeFlush(p []byte) (n int, err error) {
|
||||
n, err = h.writer.Write(p)
|
||||
if h.flusher != nil {
|
||||
h.flusher.Flush()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (h *httpConn) RemoteAddr() net.Addr {
|
||||
if h.remoteAddr != nil {
|
||||
return h.remoteAddr
|
||||
}
|
||||
return &net.TCPAddr{}
|
||||
}
|
||||
|
||||
func (h *httpConn) LocalAddr() net.Addr {
|
||||
if h.localAddr != nil {
|
||||
return h.localAddr
|
||||
}
|
||||
return &net.TCPAddr{}
|
||||
}
|
||||
|
||||
func (h *httpConn) SetDeadline(t time.Time) error {
|
||||
if t.IsZero() {
|
||||
if h.deadline != nil {
|
||||
h.deadline.Stop()
|
||||
h.deadline = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
d := time.Until(t)
|
||||
if h.deadline != nil {
|
||||
h.deadline.Reset(d)
|
||||
return nil
|
||||
}
|
||||
h.deadline = time.AfterFunc(d, func() { h.Close() })
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpConn) SetReadDeadline(t time.Time) error { return h.SetDeadline(t) }
|
||||
func (h *httpConn) SetWriteDeadline(t time.Time) error { return h.SetDeadline(t) }
|
||||
|
||||
var _ net.Conn = (*tcpConn)(nil)
|
||||
|
||||
type tcpConn struct{ httpConn }
|
||||
|
||||
func (t *tcpConn) Read(b []byte) (n int, err error) {
|
||||
if err = t.waitCreated(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return t.body.Read(b)
|
||||
}
|
||||
|
||||
func (t *tcpConn) Write(b []byte) (int, error) {
|
||||
return t.writeFlush(b)
|
||||
}
|
||||
140
transport/trusttunnel/quic.go
Normal file
140
transport/trusttunnel/quic.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package trusttunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/congestion"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
qtls "github.com/sagernet/sing-quic"
|
||||
"github.com/sagernet/sing-quic/congestion_bbr1"
|
||||
"github.com/sagernet/sing-quic/congestion_bbr2"
|
||||
congestion_meta1 "github.com/sagernet/sing-quic/congestion_meta1"
|
||||
congestion_meta2 "github.com/sagernet/sing-quic/congestion_meta2"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
)
|
||||
|
||||
func NewCongestionControl(name string, cwnd int, bbrProfile string, timeFunc func() time.Time) (func(conn *quic.Conn) congestion.CongestionControl, error) {
|
||||
if timeFunc == nil {
|
||||
timeFunc = time.Now
|
||||
}
|
||||
if cwnd == 0 {
|
||||
cwnd = 32
|
||||
}
|
||||
switch name {
|
||||
case "", "bbr":
|
||||
return func(conn *quic.Conn) congestion.CongestionControl {
|
||||
return congestion_meta2.NewBbrSender(
|
||||
congestion_meta2.DefaultClock{TimeFunc: timeFunc},
|
||||
congestion.ByteCount(conn.Config().InitialPacketSize),
|
||||
congestion.ByteCount(cwnd)*congestion.ByteCount(conn.Config().InitialPacketSize),
|
||||
)
|
||||
}, nil
|
||||
case "bbr_standard":
|
||||
return func(conn *quic.Conn) congestion.CongestionControl {
|
||||
return congestion_bbr1.NewBbrSender(
|
||||
congestion_bbr1.DefaultClock{TimeFunc: timeFunc},
|
||||
congestion.ByteCount(conn.Config().InitialPacketSize),
|
||||
congestion_bbr1.InitialCongestionWindowPackets,
|
||||
congestion_bbr1.MaxCongestionWindowPackets,
|
||||
)
|
||||
}, nil
|
||||
case "bbr2":
|
||||
return func(conn *quic.Conn) congestion.CongestionControl {
|
||||
return congestion_bbr2.NewBBR2Sender(
|
||||
congestion_bbr2.DefaultClock{TimeFunc: timeFunc},
|
||||
congestion.ByteCount(conn.Config().InitialPacketSize),
|
||||
0,
|
||||
false,
|
||||
)
|
||||
}, nil
|
||||
case "bbr2_variant":
|
||||
return func(conn *quic.Conn) congestion.CongestionControl {
|
||||
return congestion_bbr2.NewBBR2Sender(
|
||||
congestion_bbr2.DefaultClock{TimeFunc: timeFunc},
|
||||
congestion.ByteCount(conn.Config().InitialPacketSize),
|
||||
32*congestion.ByteCount(conn.Config().InitialPacketSize),
|
||||
true,
|
||||
)
|
||||
}, nil
|
||||
case "cubic":
|
||||
return func(conn *quic.Conn) congestion.CongestionControl {
|
||||
return congestion_meta1.NewCubicSender(
|
||||
congestion_meta1.DefaultClock{TimeFunc: timeFunc},
|
||||
congestion.ByteCount(conn.Config().InitialPacketSize),
|
||||
false,
|
||||
)
|
||||
}, nil
|
||||
case "reno":
|
||||
return func(conn *quic.Conn) congestion.CongestionControl {
|
||||
return congestion_meta1.NewCubicSender(
|
||||
congestion_meta1.DefaultClock{TimeFunc: timeFunc},
|
||||
congestion.ByteCount(conn.Config().InitialPacketSize),
|
||||
true,
|
||||
)
|
||||
}, nil
|
||||
default:
|
||||
return nil, E.New("unknown congestion control: ", name)
|
||||
}
|
||||
}
|
||||
|
||||
type QUICService struct {
|
||||
service *Service
|
||||
h3Server *http3.Server
|
||||
udpConn net.PacketConn
|
||||
congestionControl string
|
||||
cwnd int
|
||||
bbrProfile string
|
||||
}
|
||||
|
||||
func NewQUICService(service *Service, congestionControl string, cwnd int, bbrProfile string) *QUICService {
|
||||
return &QUICService{
|
||||
service: service,
|
||||
congestionControl: congestionControl,
|
||||
cwnd: cwnd,
|
||||
bbrProfile: bbrProfile,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *QUICService) Start(ctx context.Context, udpConn net.PacketConn, tlsConfig tls.ServerConfig) error {
|
||||
s.udpConn = udpConn
|
||||
congestionControlFactory, err := NewCongestionControl(s.congestionControl, s.cwnd, s.bbrProfile, ntp.TimeFuncFromContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.h3Server = &http3.Server{
|
||||
Handler: s.service,
|
||||
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
|
||||
conn.SetCongestionControl(congestionControlFactory(conn))
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
quicListener, err := qtls.ListenEarly(udpConn, tlsConfig, &quic.Config{
|
||||
MaxIdleTimeout: DefaultSessionTimeout * 2,
|
||||
MaxIncomingStreams: 1 << 60,
|
||||
Allow0RTT: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
_ = s.h3Server.ServeListener(quicListener)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *QUICService) Close() error {
|
||||
var errs []error
|
||||
if s.h3Server != nil {
|
||||
errs = append(errs, s.h3Server.Close())
|
||||
}
|
||||
if s.udpConn != nil {
|
||||
errs = append(errs, s.udpConn.Close())
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
218
transport/trusttunnel/service.go
Normal file
218
transport/trusttunnel/service.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package trusttunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"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"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
N.TCPConnectionHandler
|
||||
N.UDPConnectionHandler
|
||||
}
|
||||
|
||||
type ServiceOptions struct {
|
||||
Ctx context.Context
|
||||
Logger logger.ContextLogger
|
||||
Handler Handler
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
users map[string]string
|
||||
handler Handler
|
||||
conns map[string][]io.Closer
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewService(options ServiceOptions) *Service {
|
||||
return &Service{
|
||||
ctx: options.Ctx,
|
||||
logger: options.Logger,
|
||||
handler: options.Handler,
|
||||
conns: make(map[string][]io.Closer),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) UpdateUsers(users map[string]string) {
|
||||
s.mu.Lock()
|
||||
s.users = users
|
||||
var closedConns []io.Closer
|
||||
for user, conns := range s.conns {
|
||||
if _, exists := users[user]; !exists {
|
||||
closedConns = append(closedConns, conns...)
|
||||
delete(s.conns, user)
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
for _, conn := range closedConns {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) trackConn(username string, conn io.Closer) {
|
||||
s.mu.Lock()
|
||||
s.conns[username] = append(s.conns[username], conn)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Service) untrackConn(username string, conn io.Closer) {
|
||||
s.mu.Lock()
|
||||
conns := s.conns[username]
|
||||
for i, c := range conns {
|
||||
if c == conn {
|
||||
s.conns[username] = append(conns[:i], conns[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Service) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
authorization := request.Header.Get("Proxy-Authorization")
|
||||
username, loaded := s.verify(authorization)
|
||||
if !loaded {
|
||||
writer.WriteHeader(http.StatusProxyAuthRequired)
|
||||
s.badRequest(request.Context(), request, E.New("authorization failed"))
|
||||
return
|
||||
}
|
||||
if request.Method != http.MethodConnect {
|
||||
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||
s.badRequest(request.Context(), request, E.New("unexpected HTTP method ", request.Method))
|
||||
return
|
||||
}
|
||||
ctx := request.Context()
|
||||
ctx = auth.ContextWithUser(ctx, username)
|
||||
switch request.Host {
|
||||
case UDPMagicAddress:
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
flusher, isFlusher := writer.(http.Flusher)
|
||||
if isFlusher {
|
||||
flusher.Flush()
|
||||
}
|
||||
done := make(chan struct{})
|
||||
conn := &serverPacketConn{
|
||||
httpConn: httpConn{
|
||||
writer: writer,
|
||||
flusher: flusher,
|
||||
created: make(chan struct{}),
|
||||
done: done,
|
||||
remoteAddr: parseRemoteAddr(request.RemoteAddr),
|
||||
},
|
||||
}
|
||||
conn.setup(request.Body, nil)
|
||||
firstPacket := buf.NewPacket()
|
||||
destination, err := conn.ReadPacket(firstPacket)
|
||||
if err != nil {
|
||||
firstPacket.Release()
|
||||
_ = conn.Close()
|
||||
s.logger.ErrorContext(ctx, E.Cause(err, "read first packet from ", request.RemoteAddr))
|
||||
return
|
||||
}
|
||||
destination = destination.Unwrap()
|
||||
cachedConn := bufio.NewCachedPacketConn(conn, firstPacket, destination)
|
||||
s.trackConn(username, conn)
|
||||
_ = s.handler.NewPacketConnection(ctx, cachedConn, M.Metadata{
|
||||
Protocol: "trusttunnel",
|
||||
Source: M.ParseSocksaddr(request.RemoteAddr),
|
||||
Destination: destination,
|
||||
})
|
||||
<-done
|
||||
s.untrackConn(username, conn)
|
||||
case HealthCheckMagicAddress:
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
if flusher, isFlusher := writer.(http.Flusher); isFlusher {
|
||||
flusher.Flush()
|
||||
}
|
||||
_ = request.Body.Close()
|
||||
default:
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
flusher, isFlusher := writer.(http.Flusher)
|
||||
if isFlusher {
|
||||
flusher.Flush()
|
||||
}
|
||||
done := make(chan struct{})
|
||||
conn := &tcpConn{
|
||||
httpConn{
|
||||
writer: writer,
|
||||
flusher: flusher,
|
||||
created: make(chan struct{}),
|
||||
done: done,
|
||||
remoteAddr: parseRemoteAddr(request.RemoteAddr),
|
||||
},
|
||||
}
|
||||
conn.setup(request.Body, nil)
|
||||
wrapper := &h2ConnWrapper{Conn: conn}
|
||||
s.trackConn(username, wrapper)
|
||||
_ = s.handler.NewConnection(ctx, wrapper, M.Metadata{
|
||||
Protocol: "trusttunnel",
|
||||
Source: M.ParseSocksaddr(request.RemoteAddr),
|
||||
Destination: M.ParseSocksaddr(request.Host).Unwrap(),
|
||||
})
|
||||
<-done
|
||||
s.untrackConn(username, wrapper)
|
||||
wrapper.CloseWrapper()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) verify(authorization string) (username string, loaded bool) {
|
||||
username, password, loaded := parseBasicAuth(authorization)
|
||||
if !loaded {
|
||||
return "", false
|
||||
}
|
||||
s.mu.RLock()
|
||||
recordedPassword, loaded := s.users[username]
|
||||
s.mu.RUnlock()
|
||||
if !loaded {
|
||||
return "", false
|
||||
}
|
||||
if password != recordedPassword {
|
||||
return "", false
|
||||
}
|
||||
return username, true
|
||||
}
|
||||
|
||||
func (s *Service) badRequest(ctx context.Context, request *http.Request, err error) {
|
||||
s.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr))
|
||||
}
|
||||
|
||||
func parseRemoteAddr(addr string) net.Addr {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return tcpAddr
|
||||
}
|
||||
|
||||
type h2ConnWrapper struct {
|
||||
net.Conn
|
||||
access sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (w *h2ConnWrapper) Write(p []byte) (n int, err error) {
|
||||
w.access.Lock()
|
||||
defer w.access.Unlock()
|
||||
if w.closed {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
return w.Conn.Write(p)
|
||||
}
|
||||
|
||||
func (w *h2ConnWrapper) CloseWrapper() {
|
||||
w.access.Lock()
|
||||
defer w.access.Unlock()
|
||||
w.closed = true
|
||||
}
|
||||
Reference in New Issue
Block a user