impl wsc transport and protocol

This commit is contained in:
Mobin 2025-08-31 15:45:46 +03:30
parent ed35dbe6e0
commit 1d4cd315a6
4 changed files with 236 additions and 178 deletions

14
adapter/wsc.go Normal file
View File

@ -0,0 +1,14 @@
package adapter
import (
"context"
"net"
)
type WSCServerTransport interface {
}
type WSCClientTransport interface {
DialContext(ctx context.Context, network string, endpoint string) (net.Conn, error)
Close(ctx context.Context) error
}

View File

@ -3,11 +3,7 @@ package outbound
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
@ -16,32 +12,21 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/transport/wsc"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/network"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/ws"
"github.com/sagernet/ws/wsutil"
)
var _ adapter.Outbound = &WSC{}
var _ net.Conn = &wscConn{}
type WSC struct {
myOutboundAdapter
dialer N.Dialer
serverAddr metadata.Socksaddr
auth string
path string
tlsConfig tls.Config
}
type wscConn struct {
net.Conn
reader *wsutil.Reader
mu sync.Mutex
dialer N.Dialer
tlsConfig tls.Config
client adapter.WSCClientTransport
}
func NewWSC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WSCOutboundOptions) (*WSC, error) {
@ -59,19 +44,19 @@ func NewWSC(ctx context.Context, router adapter.Router, logger log.ContextLogger
tag: tag,
dependencies: withDialerDependency(options.DialerOptions),
},
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
auth: options.Auth,
path: options.Path,
dialer: outboundDialer,
}
if outbound.auth == "" {
serverAddr := options.ServerOptions.Build()
if options.Auth == "" {
return nil, exceptions.New("Invalid Auth to use in authentications")
}
if !outbound.serverAddr.IsValid() {
if !serverAddr.IsValid() {
return nil, exceptions.New("Invalid server address")
}
if outbound.path == "" {
outbound.path = "/"
if options.Path == "" {
options.Path = "/"
}
if options.TLS != nil {
outbound.tlsConfig, err = tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
@ -80,6 +65,14 @@ func NewWSC(ctx context.Context, router adapter.Router, logger log.ContextLogger
}
}
outbound.client = &wsc.Client{
Auth: options.Auth,
Host: serverAddr.String(),
Path: options.Path,
TLS: outbound.tlsConfig,
Dialer: outbound.dialer,
}
return outbound, nil
}
@ -91,13 +84,7 @@ func (wsc *WSC) DialContext(ctx context.Context, network string, destination met
return nil, exceptions.Extend(N.ErrUnknownNetwork, network)
}
wsc.logger.InfoContext(ctx, "WSC outbound connection to ", destination)
conn, err := wsc.newWscConn(ctx, destination)
if err != nil {
return nil, err
}
return conn, nil
return wsc.client.DialContext(ctx, network, destination.String())
}
func (wsc *WSC) ListenPacket(ctx context.Context, destination metadata.Socksaddr) (net.PacketConn, error) {
@ -125,153 +112,12 @@ func (wsc *WSC) NewPacketConnection(ctx context.Context, conn network.PacketConn
// fmt.Println("wsc packet conn data is : ", dest, " | ", dest.Network(), " | ", buffer.Len())
// time.Sleep(time.Second * 10)
return NewPacketConnection(ctx, wsc.dialer, conn, metadata)
// return NewPacketConnection(ctx, wsc.dialer, conn, metadata)
return NewPacketConnection(ctx, wsc, conn, metadata)
}
func (wsc *WSC) Close() error {
return wsc.cleanup()
}
func (wsc *WSC) cleanup() error {
scheme := "http"
var tlsConfig *tls.STDConfig
if wsc.tlsConfig != nil {
scheme = "https"
var err error
tlsConfig, err = wsc.tlsConfig.Config()
if err != nil {
return err
}
}
pURL := url.URL{
Scheme: scheme,
Host: wsc.serverAddr.String(),
Path: "/cleanup",
RawQuery: "",
}
pQuery := pURL.Query()
pQuery.Set("auth", wsc.auth)
pURL.RawQuery = pQuery.Encode()
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return wsc.dialer.DialContext(ctx, network, metadata.ParseSocksaddr(addr))
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodPost, pURL.String(), nil)
if err != nil {
return err
}
response, err := client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
return nil
}
func (wsc *WSC) newWscConn(ctx context.Context, endpoint metadata.Socksaddr) (*wscConn, error) {
scheme := "ws"
if wsc.tlsConfig != nil {
scheme = "wss"
}
pURL := url.URL{
Scheme: scheme,
Host: wsc.serverAddr.String(),
Path: wsc.path,
RawQuery: "",
}
pQuery := pURL.Query()
pQuery.Set("auth", wsc.auth)
pQuery.Set("ep", endpoint.String())
pURL.RawQuery = pQuery.Encode()
dialer := ws.Dialer{
NetDial: func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := wsc.dialer.DialContext(ctx, N.NetworkTCP, metadata.ParseSocksaddr(addr))
if err != nil {
return nil, err
}
if wsc.tlsConfig != nil {
conn, err = tls.ClientHandshake(ctx, conn, wsc.tlsConfig)
if err != nil {
return nil, err
}
}
return conn, nil
},
}
wsConn, _, _, err := dialer.Dial(ctx, pURL.String())
if err != nil {
return nil, err
}
reader := wsutil.NewReader(wsConn, ws.StateClientSide)
return &wscConn{
Conn: wsConn,
reader: reader,
}, nil
}
func (cli *wscConn) Close() error {
cli.mu.Lock()
defer cli.mu.Unlock()
_ = wsutil.WriteClientMessage(cli.Conn, ws.OpClose, nil)
return cli.Conn.Close()
}
func (cli *wscConn) Read(b []byte) (n int, err error) {
err = nil
var header ws.Header
for {
n, err = cli.reader.Read(b)
if n > 0 {
return
}
if !exceptions.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) {
return
}
header, err = cli.reader.NextFrame()
if err != nil {
return
}
switch header.OpCode {
case ws.OpBinary, ws.OpText, ws.OpContinuation:
continue
case ws.OpPing:
wsutil.WriteClientMessage(cli.Conn, ws.OpPong, nil)
case ws.OpPong:
continue
case ws.OpClose:
err = io.EOF
return
default:
continue
}
}
}
func (cli *wscConn) Write(b []byte) (n int, err error) {
cli.mu.Lock()
defer cli.mu.Unlock()
if err := wsutil.WriteClientBinary(cli.Conn, b); err != nil {
return 0, err
}
return len(b), nil
return wsc.client.Close(ctx)
}

76
transport/wsc/client.go Normal file
View File

@ -0,0 +1,76 @@
package wsc
import (
"context"
"net"
"net/http"
"net/url"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
var _ adapter.WSCClientTransport = &Client{}
type Client struct {
Auth string
Host string
Path string
TLS tls.Config
Dialer N.Dialer
}
func (cli *Client) DialContext(ctx context.Context, network string, endpoint string) (net.Conn, error) {
return cli.newConn(ctx, network, endpoint)
}
func (cli *Client) Close(ctx context.Context) error {
return cli.cleanup(ctx)
}
func (cli *Client) cleanup(ctx context.Context) error {
scheme := "http"
var tlsConfig *tls.STDConfig
if cli.TLS != nil {
scheme = "https"
var err error
tlsConfig, err = cli.TLS.Config()
if err != nil {
return err
}
}
pURL := url.URL{
Scheme: scheme,
Host: cli.Host,
Path: "/cleanup",
RawQuery: "",
}
pQuery := pURL.Query()
pQuery.Set("auth", cli.Auth)
pURL.RawQuery = pQuery.Encode()
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return cli.Dialer.DialContext(ctx, network, metadata.ParseSocksaddr(addr))
},
},
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, pURL.String(), nil)
if err != nil {
return err
}
response, err := client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
return nil
}

View File

@ -0,0 +1,122 @@
package wsc
import (
"context"
"io"
"net"
"net/url"
"sync"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/ws"
"github.com/sagernet/ws/wsutil"
)
var _ net.Conn = &clientConn{}
type clientConn struct {
net.Conn
reader *wsutil.Reader
mu sync.Mutex
}
func (cli *Client) newConn(ctx context.Context, network string, endpoint string) (*clientConn, error) {
scheme := "ws"
if cli.TLS != nil {
scheme = "wss"
}
pURL := url.URL{
Scheme: scheme,
Host: cli.Host,
Path: cli.Path,
RawQuery: "",
}
pQuery := pURL.Query()
pQuery.Set("auth", cli.Auth)
pQuery.Set("ep", endpoint)
pQuery.Set("net", network)
pURL.RawQuery = pQuery.Encode()
dialer := ws.Dialer{
NetDial: func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := cli.Dialer.DialContext(ctx, N.NetworkTCP, metadata.ParseSocksaddr(addr))
if err != nil {
return nil, err
}
if cli.TLS != nil {
conn, err = tls.ClientHandshake(ctx, conn, cli.TLS)
if err != nil {
return nil, err
}
}
return conn, nil
},
}
conn, _, _, err := dialer.Dial(ctx, pURL.String())
if err != nil {
return nil, err
}
reader := wsutil.NewReader(conn, ws.StateClientSide)
return &clientConn{
Conn: conn,
reader: reader,
}, nil
}
func (conn *clientConn) Close() error {
conn.mu.Lock()
defer conn.mu.Unlock()
_ = wsutil.WriteClientMessage(conn.Conn, ws.OpClose, nil)
return conn.Conn.Close()
}
func (conn *clientConn) Read(b []byte) (n int, err error) {
err = nil
var header ws.Header
for {
n, err = conn.reader.Read(b)
if n > 0 {
return
}
if !exceptions.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) {
return
}
header, err = conn.reader.NextFrame()
if err != nil {
return
}
switch header.OpCode {
case ws.OpBinary, ws.OpText, ws.OpContinuation:
continue
case ws.OpPing:
wsutil.WriteClientMessage(conn.Conn, ws.OpPong, nil)
case ws.OpPong:
continue
case ws.OpClose:
err = io.EOF
return
default:
continue
}
}
}
func (conn *clientConn) Write(b []byte) (n int, err error) {
conn.mu.Lock()
defer conn.mu.Unlock()
if err := wsutil.WriteClientBinary(conn.Conn, b); err != nil {
return 0, err
}
return len(b), nil
}