浏览代码

Add multiplexer

世界 3 年之前
父节点
当前提交
457de86819

+ 1 - 1
.golangci.yml

@@ -3,7 +3,7 @@ linters:
   enable:
     - gofumpt
     - govet
-#    - gci
+    - gci
     - staticcheck
     - paralleltest
 

+ 2 - 0
adapter/outbound.go

@@ -7,6 +7,8 @@ import (
 	N "github.com/sagernet/sing/common/network"
 )
 
+// Note: for proxy protocols, outbound creates early connections by default.
+
 type Outbound interface {
 	Type() string
 	Tag() string

+ 2 - 1
box.go

@@ -124,6 +124,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
 			tag = F.ToString(i)
 		}
 		out, err = outbound.New(
+			ctx,
 			router,
 			logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
 			outboundOptions)
@@ -133,7 +134,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
 		outbounds = append(outbounds, out)
 	}
 	err = router.Initialize(outbounds, func() adapter.Outbound {
-		out, oErr := outbound.New(router, logFactory.NewLogger("outbound/direct"), option.Outbound{Type: "direct", Tag: "default"})
+		out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), option.Outbound{Type: "direct", Tag: "default"})
 		common.Must(oErr)
 		outbounds = append(outbounds, out)
 		return out

+ 87 - 0
common/debugio/log.go

@@ -0,0 +1,87 @@
+package debugio
+
+import (
+	"net"
+
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+type LogConn struct {
+	N.ExtendedConn
+	logger log.Logger
+	prefix string
+}
+
+func NewLogConn(conn net.Conn, logger log.Logger, prefix string) N.ExtendedConn {
+	return &LogConn{bufio.NewExtendedConn(conn), logger, prefix}
+}
+
+func (c *LogConn) Read(p []byte) (n int, err error) {
+	n, err = c.ExtendedConn.Read(p)
+	if n > 0 {
+		c.logger.Debug(c.prefix, " read ", buf.EncodeHexString(p[:n]))
+	}
+	return
+}
+
+func (c *LogConn) Write(p []byte) (n int, err error) {
+	c.logger.Debug(c.prefix, " write ", buf.EncodeHexString(p))
+	return c.ExtendedConn.Write(p)
+}
+
+func (c *LogConn) ReadBuffer(buffer *buf.Buffer) error {
+	err := c.ExtendedConn.ReadBuffer(buffer)
+	if err == nil {
+		c.logger.Debug(c.prefix, " read buffer ", buf.EncodeHexString(buffer.Bytes()))
+	}
+	return err
+}
+
+func (c *LogConn) WriteBuffer(buffer *buf.Buffer) error {
+	c.logger.Debug(c.prefix, " write buffer ", buf.EncodeHexString(buffer.Bytes()))
+	return c.ExtendedConn.WriteBuffer(buffer)
+}
+
+func (c *LogConn) Upstream() any {
+	return c.ExtendedConn
+}
+
+type LogPacketConn struct {
+	N.NetPacketConn
+	logger log.Logger
+	prefix string
+}
+
+func NewLogPacketConn(conn net.PacketConn, logger log.Logger, prefix string) N.NetPacketConn {
+	return &LogPacketConn{bufio.NewPacketConn(conn), logger, prefix}
+}
+
+func (c *LogPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
+	n, addr, err = c.NetPacketConn.ReadFrom(p)
+	if n > 0 {
+		c.logger.Debug(c.prefix, " read from ", addr, " ", buf.EncodeHexString(p[:n]))
+	}
+	return
+}
+
+func (c *LogPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	c.logger.Debug(c.prefix, " write to ", addr, " ", buf.EncodeHexString(p))
+	return c.NetPacketConn.WriteTo(p, addr)
+}
+
+func (c *LogPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
+	destination, err = c.NetPacketConn.ReadPacket(buffer)
+	if err == nil {
+		c.logger.Debug(c.prefix, " read packet from ", destination, " ", buf.EncodeHexString(buffer.Bytes()))
+	}
+	return
+}
+
+func (c *LogPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	c.logger.Debug(c.prefix, " write packet to ", destination, " ", buf.EncodeHexString(buffer.Bytes()))
+	return c.NetPacketConn.WritePacket(buffer, destination)
+}

+ 48 - 0
common/debugio/race.go

@@ -0,0 +1,48 @@
+package debugio
+
+import (
+	"net"
+	"sync"
+
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	N "github.com/sagernet/sing/common/network"
+)
+
+type RaceConn struct {
+	N.ExtendedConn
+	readAccess  sync.Mutex
+	writeAccess sync.Mutex
+}
+
+func NewRaceConn(conn net.Conn) N.ExtendedConn {
+	return &RaceConn{ExtendedConn: bufio.NewExtendedConn(conn)}
+}
+
+func (c *RaceConn) Read(p []byte) (n int, err error) {
+	c.readAccess.Lock()
+	defer c.readAccess.Unlock()
+	return c.ExtendedConn.Read(p)
+}
+
+func (c *RaceConn) Write(p []byte) (n int, err error) {
+	c.writeAccess.Lock()
+	defer c.writeAccess.Unlock()
+	return c.ExtendedConn.Write(p)
+}
+
+func (c *RaceConn) ReadBuffer(buffer *buf.Buffer) error {
+	c.readAccess.Lock()
+	defer c.readAccess.Unlock()
+	return c.ExtendedConn.ReadBuffer(buffer)
+}
+
+func (c *RaceConn) WriteBuffer(buffer *buf.Buffer) error {
+	c.writeAccess.Lock()
+	defer c.writeAccess.Unlock()
+	return c.ExtendedConn.WriteBuffer(buffer)
+}
+
+func (c *RaceConn) Upstream() any {
+	return c.ExtendedConn
+}

+ 2 - 1
common/dialer/default.go

@@ -12,6 +12,7 @@ import (
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/control"
 	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
 
 	"github.com/database64128/tfo-go"
 )
@@ -124,7 +125,7 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
 }
 
 func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
-	return d.ListenConfig.ListenPacket(ctx, C.NetworkUDP, "")
+	return d.ListenConfig.ListenPacket(ctx, N.NetworkUDP, "")
 }
 
 func (d *DefaultDialer) Upstream() any {

+ 1 - 2
common/dialer/router.go

@@ -5,7 +5,6 @@ import (
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
-	C "github.com/sagernet/sing-box/constant"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
@@ -23,7 +22,7 @@ func (d *RouterDialer) DialContext(ctx context.Context, network string, destinat
 }
 
 func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
-	return d.router.DefaultOutbound(C.NetworkUDP).ListenPacket(ctx, destination)
+	return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination)
 }
 
 func (d *RouterDialer) Upstream() any {

+ 1 - 1
common/dialer/tls.go

@@ -112,7 +112,7 @@ func NewTLS(dialer N.Dialer, serverAddress string, options option.OutboundTLSOpt
 }
 
 func (d *TLSDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
-	if network != C.NetworkTCP {
+	if network != N.NetworkTCP {
 		return nil, os.ErrInvalid
 	}
 	conn, err := d.dialer.DialContext(ctx, network, destination)

+ 476 - 0
common/mux/client.go

@@ -0,0 +1,476 @@
+package mux
+
+import (
+	"context"
+	"encoding/binary"
+	"io"
+	"net"
+	"sync"
+
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	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/x/list"
+
+	"github.com/hashicorp/yamux"
+)
+
+var _ N.Dialer = (*Client)(nil)
+
+type Client struct {
+	access         sync.Mutex
+	connections    list.List[*yamux.Session]
+	ctx            context.Context
+	dialer         N.Dialer
+	maxConnections int
+	minStreams     int
+	maxStreams     int
+}
+
+func NewClient(ctx context.Context, dialer N.Dialer, maxConnections int, minStreams int, maxStreams int) *Client {
+	return &Client{
+		ctx:            ctx,
+		dialer:         dialer,
+		maxConnections: maxConnections,
+		minStreams:     minStreams,
+		maxStreams:     maxStreams,
+	}
+}
+
+func NewClientWithOptions(ctx context.Context, dialer N.Dialer, options option.MultiplexOptions) N.Dialer {
+	if !options.Enabled {
+		return dialer
+	}
+	if options.MaxConnections == 0 && options.MaxStreams == 0 {
+		options.MinStreams = 8
+	}
+	return NewClient(ctx, dialer, options.MaxConnections, options.MinStreams, options.MaxStreams)
+}
+
+func (c *Client) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
+		stream, err := c.openStream()
+		if err != nil {
+			return nil, err
+		}
+		return &ClientConn{Conn: stream, destination: destination}, nil
+	case N.NetworkUDP:
+		stream, err := c.openStream()
+		if err != nil {
+			return nil, err
+		}
+		return bufio.NewUnbindPacketConn(&ClientPacketConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination}), nil
+	default:
+		return nil, E.Extend(N.ErrUnknownNetwork, network)
+	}
+}
+
+func (c *Client) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	stream, err := c.openStream()
+	if err != nil {
+		return nil, err
+	}
+	// return bufio.NewUnbindPacketConn(&ClientPacketConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination}), nil
+	return &ClientPacketAddrConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination}, nil
+}
+
+func (c *Client) openStream() (net.Conn, error) {
+	session, err := c.offer()
+	if err != nil {
+		return nil, err
+	}
+	conn, err := session.Open()
+	if err != nil {
+		return nil, err
+	}
+	return conn, nil
+}
+
+func (c *Client) offer() (*yamux.Session, error) {
+	c.access.Lock()
+	defer c.access.Unlock()
+
+	sessions := make([]*yamux.Session, 0, c.maxConnections)
+	for element := c.connections.Front(); element != nil; {
+		if element.Value.IsClosed() {
+			nextElement := element.Next()
+			c.connections.Remove(element)
+			element = nextElement
+			continue
+		}
+		sessions = append(sessions, element.Value)
+		element = element.Next()
+	}
+	sLen := len(sessions)
+	if sLen == 0 {
+		return c.offerNew()
+	}
+	// session := common.MinBy(sessions, yamux.Session.NumStreams)
+	session := common.MinBy(sessions, func(it *yamux.Session) int {
+		return it.NumStreams()
+	})
+	numStreams := session.NumStreams()
+	if numStreams == 0 {
+		return session, nil
+	}
+	if c.maxConnections > 0 {
+		if sLen >= c.maxConnections || numStreams < c.minStreams {
+			return session, nil
+		}
+	} else {
+		if c.maxStreams > 0 && numStreams < c.maxStreams {
+			return session, nil
+		}
+	}
+	return c.offerNew()
+}
+
+func (c *Client) offerNew() (*yamux.Session, error) {
+	conn, err := c.dialer.DialContext(c.ctx, N.NetworkTCP, Destination)
+	if err != nil {
+		return nil, err
+	}
+	session, err := yamux.Client(conn, newMuxConfig())
+	if err != nil {
+		return nil, err
+	}
+	c.connections.PushBack(session)
+	return session, nil
+}
+
+func (c *Client) Close() error {
+	c.access.Lock()
+	defer c.access.Unlock()
+	for _, session := range c.connections.Array() {
+		session.Close()
+	}
+	return nil
+}
+
+type ClientConn struct {
+	net.Conn
+	destination  M.Socksaddr
+	requestWrite bool
+	responseRead bool
+}
+
+func (c *ClientConn) readResponse() error {
+	response, err := ReadResponse(c.Conn)
+	if err != nil {
+		return err
+	}
+	if response.Status == statusError {
+		return E.New("remote error: ", response.Message)
+	}
+	return nil
+}
+
+func (c *ClientConn) Read(b []byte) (n int, err error) {
+	if !c.responseRead {
+		err = c.readResponse()
+		if err != nil {
+			return
+		}
+		c.responseRead = true
+	}
+	return c.Conn.Read(b)
+}
+
+func (c *ClientConn) Write(b []byte) (n int, err error) {
+	if c.requestWrite {
+		return c.Conn.Write(b)
+	}
+	request := Request{
+		Network:     N.NetworkTCP,
+		Destination: c.destination,
+	}
+	_buffer := buf.StackNewSize(requestLen(request) + len(b))
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	EncodeRequest(request, buffer)
+	buffer.Write(b)
+	_, err = c.Conn.Write(buffer.Bytes())
+	if err != nil {
+		return
+	}
+	c.requestWrite = true
+	return len(b), nil
+}
+
+func (c *ClientConn) ReadFrom(r io.Reader) (n int64, err error) {
+	if !c.requestWrite {
+		return bufio.ReadFrom0(c, r)
+	}
+	return bufio.Copy(c.Conn, r)
+}
+
+func (c *ClientConn) WriteTo(w io.Writer) (n int64, err error) {
+	if !c.responseRead {
+		return bufio.WriteTo0(c, w)
+	}
+	return bufio.Copy(w, c.Conn)
+}
+
+func (c *ClientConn) LocalAddr() net.Addr {
+	return c.Conn.LocalAddr()
+}
+
+func (c *ClientConn) RemoteAddr() net.Addr {
+	return c.destination.TCPAddr()
+}
+
+func (c *ClientConn) ReaderReplaceable() bool {
+	return c.responseRead
+}
+
+func (c *ClientConn) WriterReplaceable() bool {
+	return c.requestWrite
+}
+
+func (c *ClientConn) Upstream() any {
+	return c.Conn
+}
+
+type ClientPacketConn struct {
+	N.ExtendedConn
+	destination  M.Socksaddr
+	requestWrite bool
+	responseRead bool
+}
+
+func (c *ClientPacketConn) readResponse() error {
+	response, err := ReadResponse(c.ExtendedConn)
+	if err != nil {
+		return err
+	}
+	if response.Status == statusError {
+		return E.New("remote error: ", response.Message)
+	}
+	return nil
+}
+
+func (c *ClientPacketConn) Read(b []byte) (n int, err error) {
+	if !c.responseRead {
+		err = c.readResponse()
+		if err != nil {
+			return
+		}
+		c.responseRead = true
+	}
+	var length uint16
+	err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
+	if err != nil {
+		return
+	}
+	if cap(b) < int(length) {
+		return 0, io.ErrShortBuffer
+	}
+	return io.ReadFull(c.ExtendedConn, b[:length])
+}
+
+func (c *ClientPacketConn) writeRequest(payload []byte) (n int, err error) {
+	request := Request{
+		Network:     N.NetworkUDP,
+		Destination: c.destination,
+	}
+	rLen := requestLen(request)
+	if len(payload) > 0 {
+		rLen += 2 + len(payload)
+	}
+	_buffer := buf.StackNewSize(rLen)
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	EncodeRequest(request, buffer)
+	if len(payload) > 0 {
+		common.Must(
+			binary.Write(buffer, binary.BigEndian, uint16(len(payload))),
+			common.Error(buffer.Write(payload)),
+		)
+	}
+	_, err = c.ExtendedConn.Write(buffer.Bytes())
+	if err != nil {
+		return
+	}
+	c.requestWrite = true
+	return len(payload), nil
+}
+
+func (c *ClientPacketConn) Write(b []byte) (n int, err error) {
+	if !c.requestWrite {
+		return c.writeRequest(b)
+	}
+	err = binary.Write(c.ExtendedConn, binary.BigEndian, uint16(len(b)))
+	if err != nil {
+		return
+	}
+	return c.ExtendedConn.Write(b)
+}
+
+func (c *ClientPacketConn) WriteBuffer(buffer *buf.Buffer) error {
+	if !c.requestWrite {
+		defer buffer.Release()
+		return common.Error(c.writeRequest(buffer.Bytes()))
+	}
+	bLen := buffer.Len()
+	binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(bLen))
+	return c.ExtendedConn.WriteBuffer(buffer)
+}
+
+func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	return c.WriteBuffer(buffer)
+}
+
+func (c *ClientPacketConn) LocalAddr() net.Addr {
+	return c.ExtendedConn.LocalAddr()
+}
+
+func (c *ClientPacketConn) RemoteAddr() net.Addr {
+	return c.destination.UDPAddr()
+}
+
+func (c *ClientPacketConn) Upstream() any {
+	return c.ExtendedConn
+}
+
+var _ N.NetPacketConn = (*ClientPacketAddrConn)(nil)
+
+type ClientPacketAddrConn struct {
+	N.ExtendedConn
+	destination  M.Socksaddr
+	requestWrite bool
+	responseRead bool
+}
+
+func (c *ClientPacketAddrConn) readResponse() error {
+	response, err := ReadResponse(c.ExtendedConn)
+	if err != nil {
+		return err
+	}
+	if response.Status == statusError {
+		return E.New("remote error: ", response.Message)
+	}
+	return nil
+}
+
+func (c *ClientPacketAddrConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
+	if !c.responseRead {
+		err = c.readResponse()
+		if err != nil {
+			return
+		}
+		c.responseRead = true
+	}
+	destination, err := M.SocksaddrSerializer.ReadAddrPort(c.ExtendedConn)
+	if err != nil {
+		return
+	}
+	addr = destination.UDPAddr()
+	var length uint16
+	err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
+	if err != nil {
+		return
+	}
+	if cap(p) < int(length) {
+		return 0, nil, io.ErrShortBuffer
+	}
+	n, err = io.ReadFull(c.ExtendedConn, p[:length])
+	return
+}
+
+func (c *ClientPacketAddrConn) writeRequest(payload []byte, destination M.Socksaddr) (n int, err error) {
+	request := Request{
+		Network:     N.NetworkUDP,
+		Destination: c.destination,
+		PacketAddr:  true,
+	}
+	rLen := requestLen(request)
+	if len(payload) > 0 {
+		rLen += M.SocksaddrSerializer.AddrPortLen(destination) + 2 + len(payload)
+	}
+	_buffer := buf.StackNewSize(rLen)
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	EncodeRequest(request, buffer)
+	if len(payload) > 0 {
+		common.Must(
+			M.SocksaddrSerializer.WriteAddrPort(buffer, destination),
+			binary.Write(buffer, binary.BigEndian, uint16(len(payload))),
+			common.Error(buffer.Write(payload)),
+		)
+	}
+	_, err = c.ExtendedConn.Write(buffer.Bytes())
+	if err != nil {
+		return
+	}
+	c.requestWrite = true
+	return len(payload), nil
+}
+
+func (c *ClientPacketAddrConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	if !c.requestWrite {
+		return c.writeRequest(p, M.SocksaddrFromNet(addr))
+	}
+	err = M.SocksaddrSerializer.WriteAddrPort(c.ExtendedConn, M.SocksaddrFromNet(addr))
+	if err != nil {
+		return
+	}
+	err = binary.Write(c.ExtendedConn, binary.BigEndian, uint16(len(p)))
+	if err != nil {
+		return
+	}
+	return c.ExtendedConn.Write(p)
+}
+
+func (c *ClientPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
+	if !c.responseRead {
+		err = c.readResponse()
+		if err != nil {
+			return
+		}
+		c.responseRead = true
+	}
+	destination, err = M.SocksaddrSerializer.ReadAddrPort(c.ExtendedConn)
+	if err != nil {
+		return
+	}
+	var length uint16
+	err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
+	if err != nil {
+		return
+	}
+	if buffer.FreeLen() < int(length) {
+		return destination, io.ErrShortBuffer
+	}
+	_, err = io.ReadFull(c.ExtendedConn, buffer.Extend(int(length)))
+	return
+}
+
+func (c *ClientPacketAddrConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	if !c.requestWrite {
+		defer buffer.Release()
+		return common.Error(c.writeRequest(buffer.Bytes(), destination))
+	}
+	bLen := buffer.Len()
+	header := buf.With(buffer.ExtendHeader(M.SocksaddrSerializer.AddrPortLen(destination) + 2))
+	common.Must(
+		M.SocksaddrSerializer.WriteAddrPort(header, destination),
+		binary.Write(header, binary.BigEndian, uint16(bLen)),
+	)
+	return c.ExtendedConn.WriteBuffer(buffer)
+}
+
+func (c *ClientPacketAddrConn) LocalAddr() net.Addr {
+	return c.ExtendedConn.LocalAddr()
+}
+
+func (c *ClientPacketAddrConn) Upstream() any {
+	return c.ExtendedConn
+}

+ 119 - 0
common/mux/protocol.go

@@ -0,0 +1,119 @@
+package mux
+
+import (
+	"encoding/binary"
+	"io"
+
+	C "github.com/sagernet/sing-box/constant"
+	"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"
+
+	"github.com/hashicorp/yamux"
+)
+
+var Destination = M.Socksaddr{
+	Fqdn: "sp.mux.sing-box.arpa",
+	Port: 444,
+}
+
+func newMuxConfig() *yamux.Config {
+	config := yamux.DefaultConfig()
+	config.LogOutput = io.Discard
+	config.StreamCloseTimeout = C.TCPTimeout
+	config.StreamOpenTimeout = C.TCPTimeout
+	return config
+}
+
+const (
+	version0      = 0
+	flagUDP       = 1
+	flagAddr      = 2
+	statusSuccess = 0
+	statusError   = 1
+)
+
+type Request struct {
+	Network     string
+	Destination M.Socksaddr
+	PacketAddr  bool
+}
+
+func ReadRequest(reader io.Reader) (*Request, error) {
+	version, err := rw.ReadByte(reader)
+	if err != nil {
+		return nil, err
+	}
+	if version != version0 {
+		return nil, E.New("unsupported version: ", version)
+	}
+	var flags uint16
+	err = binary.Read(reader, binary.BigEndian, &flags)
+	if err != nil {
+		return nil, err
+	}
+	destination, err := M.SocksaddrSerializer.ReadAddrPort(reader)
+	if err != nil {
+		return nil, err
+	}
+	var network string
+	var udpAddr bool
+	if flags&flagUDP == 0 {
+		network = N.NetworkTCP
+	} else {
+		network = N.NetworkUDP
+		udpAddr = flags&flagAddr != 0
+	}
+	return &Request{network, destination, udpAddr}, nil
+}
+
+func requestLen(request Request) int {
+	var rLen int
+	rLen += 1 // version
+	rLen += 2 // flags
+	rLen += M.SocksaddrSerializer.AddrPortLen(request.Destination)
+	return rLen
+}
+
+func EncodeRequest(request Request, buffer *buf.Buffer) {
+	destination := request.Destination
+	var flags uint16
+	if request.Network == N.NetworkUDP {
+		flags |= flagUDP
+	}
+	if request.PacketAddr {
+		flags |= flagAddr
+		if !destination.IsValid() {
+			destination = Destination
+		}
+	}
+	common.Must(
+		buffer.WriteByte(version0),
+		binary.Write(buffer, binary.BigEndian, flags),
+		M.SocksaddrSerializer.WriteAddrPort(buffer, destination),
+	)
+}
+
+type Response struct {
+	Status  uint8
+	Message string
+}
+
+func ReadResponse(reader io.Reader) (*Response, error) {
+	var response Response
+	status, err := rw.ReadByte(reader)
+	if err != nil {
+		return nil, err
+	}
+	response.Status = status
+	if status == statusError {
+		response.Message, err = rw.ReadVString(reader)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return &response, nil
+}

+ 210 - 0
common/mux/service.go

@@ -0,0 +1,210 @@
+package mux
+
+import (
+	"context"
+	"encoding/binary"
+	"net"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	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"
+
+	"github.com/hashicorp/yamux"
+)
+
+func NewConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, conn net.Conn, metadata adapter.InboundContext) error {
+	session, err := yamux.Server(conn, newMuxConfig())
+	if err != nil {
+		return err
+	}
+	for {
+		stream, err := session.Accept()
+		if err != nil {
+			return err
+		}
+		request, err := ReadRequest(stream)
+		if err != nil {
+			return err
+		}
+		metadata.Destination = request.Destination
+		if request.Network == N.NetworkTCP {
+			go func() {
+				logger.InfoContext(ctx, "inbound multiplex connection to ", metadata.Destination)
+				hErr := router.RouteConnection(ctx, &ServerConn{ExtendedConn: bufio.NewExtendedConn(stream)}, metadata)
+				// hErr := router.RouteConnection(ctx, &ServerConn{ExtendedConn: bufio.NewExtendedConn(stream)}, metadata)
+				if hErr != nil {
+					errorHandler.NewError(ctx, hErr)
+				}
+			}()
+		} else {
+			go func() {
+				var packetConn N.PacketConn
+				if !request.PacketAddr {
+					logger.InfoContext(ctx, "inbound multiplex packet connection to ", metadata.Destination)
+					packetConn = &ServerPacketConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: request.Destination}
+				} else {
+					logger.InfoContext(ctx, "inbound multiplex packet connection")
+					packetConn = &ServerPacketAddrConn{ExtendedConn: bufio.NewExtendedConn(stream)}
+				}
+				hErr := router.RoutePacketConnection(ctx, packetConn, metadata)
+				if hErr != nil {
+					errorHandler.NewError(ctx, hErr)
+				}
+			}()
+		}
+	}
+}
+
+var _ N.HandshakeConn = (*ServerConn)(nil)
+
+type ServerConn struct {
+	N.ExtendedConn
+	responseWrite bool
+}
+
+func (c *ServerConn) HandshakeFailure(err error) error {
+	errMessage := err.Error()
+	_buffer := buf.StackNewSize(1 + rw.UVariantLen(uint64(len(errMessage))) + len(errMessage))
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	common.Must(
+		buffer.WriteByte(statusError),
+		rw.WriteVString(_buffer, errMessage),
+	)
+	return c.ExtendedConn.WriteBuffer(buffer)
+}
+
+func (c *ServerConn) Write(b []byte) (n int, err error) {
+	if c.responseWrite {
+		return c.ExtendedConn.Write(b)
+	}
+	_buffer := buf.StackNewSize(1 + len(b))
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	common.Must(
+		buffer.WriteByte(statusSuccess),
+		common.Error(buffer.Write(b)),
+	)
+	_, err = c.ExtendedConn.Write(buffer.Bytes())
+	if err != nil {
+		return
+	}
+	c.responseWrite = true
+	return len(b), nil
+}
+
+func (c *ServerConn) WriteBuffer(buffer *buf.Buffer) error {
+	if c.responseWrite {
+		return c.ExtendedConn.WriteBuffer(buffer)
+	}
+	buffer.ExtendHeader(1)[0] = statusSuccess
+	c.responseWrite = true
+	return c.ExtendedConn.WriteBuffer(buffer)
+}
+
+var (
+	_ N.HandshakeConn = (*ServerPacketConn)(nil)
+	_ N.PacketConn    = (*ServerPacketConn)(nil)
+)
+
+type ServerPacketConn struct {
+	N.ExtendedConn
+	destination   M.Socksaddr
+	responseWrite bool
+}
+
+func (c *ServerPacketConn) HandshakeFailure(err error) error {
+	errMessage := err.Error()
+	_buffer := buf.StackNewSize(1 + rw.UVariantLen(uint64(len(errMessage))) + len(errMessage))
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	common.Must(
+		buffer.WriteByte(statusError),
+		rw.WriteVString(_buffer, errMessage),
+	)
+	return c.ExtendedConn.WriteBuffer(buffer)
+}
+
+func (c *ServerPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
+	var length uint16
+	err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
+	if err != nil {
+		return
+	}
+	_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
+	if err != nil {
+		return
+	}
+	destination = c.destination
+	return
+}
+
+func (c *ServerPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	pLen := buffer.Len()
+	common.Must(binary.Write(buf.With(buffer.ExtendHeader(2)), binary.BigEndian, uint16(pLen)))
+	if !c.responseWrite {
+		buffer.ExtendHeader(1)[0] = statusSuccess
+		c.responseWrite = true
+	}
+	return c.ExtendedConn.WriteBuffer(buffer)
+}
+
+var (
+	_ N.HandshakeConn = (*ServerPacketAddrConn)(nil)
+	_ N.PacketConn    = (*ServerPacketAddrConn)(nil)
+)
+
+type ServerPacketAddrConn struct {
+	N.ExtendedConn
+	responseWrite bool
+}
+
+func (c *ServerPacketAddrConn) HandshakeFailure(err error) error {
+	errMessage := err.Error()
+	_buffer := buf.StackNewSize(1 + rw.UVariantLen(uint64(len(errMessage))) + len(errMessage))
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	common.Must(
+		buffer.WriteByte(statusError),
+		rw.WriteVString(_buffer, errMessage),
+	)
+	return c.ExtendedConn.WriteBuffer(buffer)
+}
+
+func (c *ServerPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
+	destination, err = M.SocksaddrSerializer.ReadAddrPort(c.ExtendedConn)
+	if err != nil {
+		return
+	}
+	var length uint16
+	err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
+	if err != nil {
+		return
+	}
+	_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
+	if err != nil {
+		return
+	}
+	return
+}
+
+func (c *ServerPacketAddrConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	pLen := buffer.Len()
+	common.Must(binary.Write(buf.With(buffer.ExtendHeader(2)), binary.BigEndian, uint16(pLen)))
+	common.Must(M.SocksaddrSerializer.WriteAddrPort(buf.With(buffer.ExtendHeader(M.SocksaddrSerializer.AddrPortLen(destination))), destination))
+	if !c.responseWrite {
+		buffer.ExtendHeader(1)[0] = statusSuccess
+		c.responseWrite = true
+	}
+	return c.ExtendedConn.WriteBuffer(buffer)
+}

+ 4 - 4
common/process/searcher_darwin.go

@@ -8,8 +8,8 @@ import (
 	"syscall"
 	"unsafe"
 
-	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
+	N "github.com/sagernet/sing/common/network"
 
 	"golang.org/x/sys/unix"
 )
@@ -33,9 +33,9 @@ func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, sr
 func findProcessName(network string, ip netip.Addr, port int) (string, error) {
 	var spath string
 	switch network {
-	case C.NetworkTCP:
+	case N.NetworkTCP:
 		spath = "net.inet.tcp.pcblist_n"
-	case C.NetworkUDP:
+	case N.NetworkUDP:
 		spath = "net.inet.udp.pcblist_n"
 	default:
 		return "", os.ErrInvalid
@@ -55,7 +55,7 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
 	// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
 	// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
 	itemSize := 384
-	if network == C.NetworkTCP {
+	if network == N.NetworkTCP {
 		// rup8(sizeof(xtcpcb_n))
 		itemSize += 208
 	}

+ 3 - 3
common/process/searcher_linux_shared.go

@@ -15,10 +15,10 @@ import (
 	"unicode"
 	"unsafe"
 
-	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/buf"
 	E "github.com/sagernet/sing/common/exceptions"
+	N "github.com/sagernet/sing/common/network"
 )
 
 // from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
@@ -52,9 +52,9 @@ func resolveSocketByNetlink0(network string, ip netip.Addr, srcPort int) (inode
 	var protocol byte
 
 	switch network {
-	case C.NetworkTCP:
+	case N.NetworkTCP:
 		protocol = syscall.IPPROTO_TCP
-	case C.NetworkUDP:
+	case N.NetworkUDP:
 		protocol = syscall.IPPROTO_UDP
 	default:
 		return 0, 0, os.ErrInvalid

+ 4 - 4
common/process/searcher_windows.go

@@ -8,9 +8,9 @@ import (
 	"syscall"
 	"unsafe"
 
-	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	E "github.com/sagernet/sing/common/exceptions"
+	N "github.com/sagernet/sing/common/network"
 
 	"golang.org/x/sys/windows"
 )
@@ -86,10 +86,10 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (string, error)
 	var class int
 	var fn uintptr
 	switch network {
-	case C.NetworkTCP:
+	case N.NetworkTCP:
 		fn = procGetExtendedTcpTable.Addr()
 		class = tcpTablePidConn
-	case C.NetworkUDP:
+	case N.NetworkUDP:
 		fn = procGetExtendedUdpTable.Addr()
 		class = udpTablePid
 	default:
@@ -101,7 +101,7 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (string, error)
 		return "", err
 	}
 
-	s := newSearcher(family == windows.AF_INET, network == C.NetworkTCP)
+	s := newSearcher(family == windows.AF_INET, network == N.NetworkTCP)
 
 	pid, err := s.Search(buf, ip, uint16(srcPort))
 	if err != nil {

+ 0 - 6
constant/network.go

@@ -1,6 +0,0 @@
-package constant
-
-const (
-	NetworkTCP = "tcp"
-	NetworkUDP = "udp"
-)

+ 7 - 9
constant/timeout.go

@@ -3,13 +3,11 @@ package constant
 import "time"
 
 const (
-	TCPTimeout             = 5 * time.Second
-	TCPKeepAlivePeriod     = 30 * time.Second
-	ReadPayloadTimeout     = 300 * time.Millisecond
-	URLTestTimeout         = TCPTimeout
-	DefaultURLTestInterval = 1 * time.Minute
-	DNSTimeout             = 10 * time.Second
-	QUICTimeout            = 30 * time.Second
-	STUNTimeout            = 15 * time.Second
-	UDPTimeout             = 5 * time.Minute
+	TCPTimeout         = 5 * time.Second
+	TCPKeepAlivePeriod = 30 * time.Second
+	ReadPayloadTimeout = 300 * time.Millisecond
+	DNSTimeout         = 10 * time.Second
+	QUICTimeout        = 30 * time.Second
+	STUNTimeout        = 15 * time.Second
+	UDPTimeout         = 5 * time.Minute
 )

+ 3 - 2
experimental/clashapi/proxies.go

@@ -15,6 +15,7 @@ import (
 	"github.com/sagernet/sing-box/outbound"
 	"github.com/sagernet/sing/common"
 	F "github.com/sagernet/sing/common/format"
+	N "github.com/sagernet/sing/common/network"
 
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/render"
@@ -82,7 +83,7 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
 	}
 	info.Put("type", clashType)
 	info.Put("name", detour.Tag())
-	info.Put("udp", common.Contains(detour.Network(), C.NetworkUDP))
+	info.Put("udp", common.Contains(detour.Network(), N.NetworkUDP))
 	delayHistory := server.urlTestHistory.LoadURLTestHistory(adapter.OutboundTag(detour))
 	if delayHistory != nil {
 		info.Put("history", []*urltest.History{delayHistory})
@@ -114,7 +115,7 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite
 			allProxies = append(allProxies, detour.Tag())
 		}
 
-		defaultTag := router.DefaultOutbound(C.NetworkTCP).Tag()
+		defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag()
 		if defaultTag == "" {
 			defaultTag = allProxies[0]
 		}

+ 2 - 3
experimental/clashapi/trafficontrol/tracker.go

@@ -6,7 +6,6 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
-	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/buf"
 	M "github.com/sagernet/sing/common/metadata"
@@ -86,7 +85,7 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
 	var chain []string
 	var next string
 	if rule == nil {
-		next = router.DefaultOutbound(C.NetworkTCP).Tag()
+		next = router.DefaultOutbound(N.NetworkTCP).Tag()
 	} else {
 		next = rule.Outbound()
 	}
@@ -173,7 +172,7 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
 	var chain []string
 	var next string
 	if rule == nil {
-		next = router.DefaultOutbound(C.NetworkUDP).Tag()
+		next = router.DefaultOutbound(N.NetworkUDP).Tag()
 	} else {
 		next = rule.Outbound()
 	}

+ 8 - 6
go.mod

@@ -7,25 +7,27 @@ require (
 	github.com/fsnotify/fsnotify v1.5.4
 	github.com/go-chi/chi/v5 v5.0.7
 	github.com/go-chi/cors v1.2.1
-	github.com/go-chi/render v1.0.1
+	github.com/go-chi/render v1.0.2
 	github.com/gofrs/uuid v4.2.0+incompatible
 	github.com/gorilla/websocket v1.5.0
+	github.com/hashicorp/yamux v0.1.1
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/oschwald/maxminddb-golang v1.9.0
-	github.com/sagernet/sing v0.0.0-20220726034811-bc109486f14e
-	github.com/sagernet/sing-dns v0.0.0-20220726044716-2b8c696b09f5
-	github.com/sagernet/sing-shadowsocks v0.0.0-20220726034922-ebbaadcae06b
+	github.com/sagernet/sing v0.0.0-20220729120910-4376f188c512
+	github.com/sagernet/sing-dns v0.0.0-20220729120941-109c0a7aabb1
+	github.com/sagernet/sing-shadowsocks v0.0.0-20220729155919-91d2780bfc80
 	github.com/sagernet/sing-tun v0.0.0-20220726111504-b4bded886e01
 	github.com/sagernet/sing-vmess v0.0.0-20220726034841-4dae776653e5
 	github.com/spf13/cobra v1.5.0
 	github.com/stretchr/testify v1.8.0
 	go.uber.org/atomic v1.9.0
 	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
-	golang.org/x/net v0.0.0-20220725212005-46097bf591d3
-	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f
+	golang.org/x/net v0.0.0-20220728211354-c7608f3a8462
+	golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10
 )
 
 require (
+	github.com/ajg/form v1.5.1 // indirect
 	github.com/cheekybits/genny v1.0.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect

+ 16 - 12
go.sum

@@ -8,6 +8,8 @@ dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1
 dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
 git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
+github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
@@ -35,8 +37,8 @@ github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
 github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
 github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
-github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
-github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
+github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
+github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
@@ -80,6 +82,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
+github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
+github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -143,12 +147,12 @@ github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7q
 github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sagernet/sing v0.0.0-20220726034811-bc109486f14e h1:5lfrAc+vSv0iW6eHGNLyHC+a/k6BDGJvYxYxwB/68Kk=
-github.com/sagernet/sing v0.0.0-20220726034811-bc109486f14e/go.mod h1:GbtQfZSpmtD3cXeD1qX2LCMwY8dH+bnnInDTqd92IsM=
-github.com/sagernet/sing-dns v0.0.0-20220726044716-2b8c696b09f5 h1:l6ztUAFVhWhY0XOq7ISbwVBE4YLWMxfIN6HptgaOl4I=
-github.com/sagernet/sing-dns v0.0.0-20220726044716-2b8c696b09f5/go.mod h1:KL+8wZG3gqHLm+nvNI3ZNaPzCMA4T7KIwsGp7ix9a34=
-github.com/sagernet/sing-shadowsocks v0.0.0-20220726034922-ebbaadcae06b h1:6wJoJaroW3WCGjHGu7XPOSLEKP9Loi3Ox4+7A1kRTsQ=
-github.com/sagernet/sing-shadowsocks v0.0.0-20220726034922-ebbaadcae06b/go.mod h1:mH6wE4b5FZp1Q/meATe4tjiPjvQO9E7Lr0FBBwFYp4I=
+github.com/sagernet/sing v0.0.0-20220729120910-4376f188c512 h1:dCWDE55LpZu//W02FccNbGObZFlv1N2NS0yUdf2i4Mc=
+github.com/sagernet/sing v0.0.0-20220729120910-4376f188c512/go.mod h1:GbtQfZSpmtD3cXeD1qX2LCMwY8dH+bnnInDTqd92IsM=
+github.com/sagernet/sing-dns v0.0.0-20220729120941-109c0a7aabb1 h1:Gv9ow1IF98Qdxs+X8unPHJG4iwuEWoq0PE/jvlIqgqY=
+github.com/sagernet/sing-dns v0.0.0-20220729120941-109c0a7aabb1/go.mod h1:LQJDT4IpqyWI6NugkSSqxTcFfxxNBp94n+fXtHFMboQ=
+github.com/sagernet/sing-shadowsocks v0.0.0-20220729155919-91d2780bfc80 h1:gpCPZyZJQVn6ZTBCJ/XaYbPi6j43TdyTty/MI5bXhbE=
+github.com/sagernet/sing-shadowsocks v0.0.0-20220729155919-91d2780bfc80/go.mod h1:mH6wE4b5FZp1Q/meATe4tjiPjvQO9E7Lr0FBBwFYp4I=
 github.com/sagernet/sing-tun v0.0.0-20220726111504-b4bded886e01 h1:tNJn7T87sgQyA8gpEvC6LbusV4lkhZU8oi4mRujOhM8=
 github.com/sagernet/sing-tun v0.0.0-20220726111504-b4bded886e01/go.mod h1:bYHamPB16GFGt34ayYt56Pb7aN64RPY0+uuFPBSbj0U=
 github.com/sagernet/sing-vmess v0.0.0-20220726034841-4dae776653e5 h1:TNguWTPF6gxX/gR02hY3LGviUn6LGlDPofE6lpSJWeo=
@@ -238,8 +242,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.0.0-20220725212005-46097bf591d3 h1:2yWTtPWWRcISTw3/o+s/Y4UOMnQL71DWyToOANFusCg=
-golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM=
+golang.org/x/net v0.0.0-20220728211354-c7608f3a8462 h1:UreQrH7DbFXSi9ZFox6FNT3WBooWmdANpU+IfkT1T4I=
+golang.org/x/net v0.0.0-20220728211354-c7608f3a8462/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -275,8 +279,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

+ 11 - 11
inbound/default.go

@@ -62,13 +62,13 @@ func (a *myInboundAdapter) Tag() string {
 
 func (a *myInboundAdapter) Start() error {
 	bindAddr := M.SocksaddrFrom(netip.Addr(a.listenOptions.Listen), a.listenOptions.ListenPort)
-	if common.Contains(a.network, C.NetworkTCP) {
+	if common.Contains(a.network, N.NetworkTCP) {
 		var tcpListener *net.TCPListener
 		var err error
 		if !a.listenOptions.TCPFastOpen {
-			tcpListener, err = net.ListenTCP(M.NetworkFromNetAddr(C.NetworkTCP, bindAddr.Addr), bindAddr.TCPAddr())
+			tcpListener, err = net.ListenTCP(M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.TCPAddr())
 		} else {
-			tcpListener, err = tfo.ListenTCP(M.NetworkFromNetAddr(C.NetworkTCP, bindAddr.Addr), bindAddr.TCPAddr())
+			tcpListener, err = tfo.ListenTCP(M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.TCPAddr())
 		}
 		if err != nil {
 			return err
@@ -77,8 +77,8 @@ func (a *myInboundAdapter) Start() error {
 		go a.loopTCPIn()
 		a.logger.Info("tcp server started at ", tcpListener.Addr())
 	}
-	if common.Contains(a.network, C.NetworkUDP) {
-		udpConn, err := net.ListenUDP(M.NetworkFromNetAddr(C.NetworkUDP, bindAddr.Addr), bindAddr.UDPAddr())
+	if common.Contains(a.network, N.NetworkUDP) {
+		udpConn, err := net.ListenUDP(M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.UDPAddr())
 		if err != nil {
 			return err
 		}
@@ -162,7 +162,7 @@ func (a *myInboundAdapter) loopTCPIn() {
 			metadata.SniffEnabled = a.listenOptions.SniffEnabled
 			metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
 			metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
-			metadata.Network = C.NetworkTCP
+			metadata.Network = N.NetworkTCP
 			metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr())
 			a.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
 			hErr := a.connHandler.NewConnection(ctx, conn, metadata)
@@ -196,7 +196,7 @@ func (a *myInboundAdapter) loopUDPIn() {
 		metadata.SniffEnabled = a.listenOptions.SniffEnabled
 		metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
 		metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
-		metadata.Network = C.NetworkUDP
+		metadata.Network = N.NetworkUDP
 		metadata.Source = M.SocksaddrFromNetIP(addr)
 		err = a.packetHandler.NewPacket(a.ctx, packetService, buffer, metadata)
 		if err != nil {
@@ -228,7 +228,7 @@ func (a *myInboundAdapter) loopUDPOOBIn() {
 		metadata.SniffEnabled = a.listenOptions.SniffEnabled
 		metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
 		metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
-		metadata.Network = C.NetworkUDP
+		metadata.Network = N.NetworkUDP
 		metadata.Source = M.SocksaddrFromNetIP(addr)
 		err = a.oobPacketHandler.NewPacket(a.ctx, packetService, buffer, oob[:oobN], metadata)
 		if err != nil {
@@ -254,7 +254,7 @@ func (a *myInboundAdapter) loopUDPInThreadSafe() {
 		metadata.SniffEnabled = a.listenOptions.SniffEnabled
 		metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
 		metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
-		metadata.Network = C.NetworkUDP
+		metadata.Network = N.NetworkUDP
 		metadata.Source = M.SocksaddrFromNetIP(addr)
 		err = a.packetHandler.NewPacket(a.ctx, packetService, buffer, metadata)
 		if err != nil {
@@ -282,7 +282,7 @@ func (a *myInboundAdapter) loopUDPOOBInThreadSafe() {
 		metadata.SniffEnabled = a.listenOptions.SniffEnabled
 		metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
 		metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
-		metadata.Network = C.NetworkUDP
+		metadata.Network = N.NetworkUDP
 		metadata.Source = M.SocksaddrFromNetIP(addr)
 		err = a.oobPacketHandler.NewPacket(a.ctx, packetService, buffer, oob[:oobN], metadata)
 		if err != nil {
@@ -334,7 +334,7 @@ func NewError(logger log.ContextLogger, ctx context.Context, err error) {
 func (a *myInboundAdapter) writePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
 	defer buffer.Release()
 	if destination.IsFqdn() {
-		udpAddr, err := net.ResolveUDPAddr(C.NetworkUDP, destination.String())
+		udpAddr, err := net.ResolveUDPAddr(N.NetworkUDP, destination.String())
 		if err != nil {
 			return err
 		}

+ 1 - 1
inbound/http.go

@@ -29,7 +29,7 @@ func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogge
 	inbound := &HTTP{
 		myInboundAdapter: myInboundAdapter{
 			protocol:       C.TypeHTTP,
-			network:        []string{C.NetworkTCP},
+			network:        []string{N.NetworkTCP},
 			ctx:            ctx,
 			router:         router,
 			logger:         logger,

+ 2 - 1
inbound/mixed.go

@@ -13,6 +13,7 @@ import (
 	"github.com/sagernet/sing/common/buf"
 	"github.com/sagernet/sing/common/bufio"
 	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/rw"
 	"github.com/sagernet/sing/protocol/http"
 	"github.com/sagernet/sing/protocol/socks"
@@ -31,7 +32,7 @@ func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogg
 	inbound := &Mixed{
 		myInboundAdapter{
 			protocol:       C.TypeMixed,
-			network:        []string{C.NetworkTCP},
+			network:        []string{N.NetworkTCP},
 			ctx:            ctx,
 			router:         router,
 			logger:         logger,

+ 2 - 1
inbound/redirect.go

@@ -11,6 +11,7 @@ import (
 	"github.com/sagernet/sing-box/option"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
 )
 
 type Redirect struct {
@@ -21,7 +22,7 @@ func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextL
 	redirect := &Redirect{
 		myInboundAdapter{
 			protocol:      C.TypeRedirect,
-			network:       []string{C.NetworkTCP},
+			network:       []string{N.NetworkTCP},
 			ctx:           ctx,
 			router:        router,
 			logger:        logger,

+ 2 - 1
inbound/socks.go

@@ -10,6 +10,7 @@ import (
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common/auth"
 	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/protocol/socks"
 )
 
@@ -24,7 +25,7 @@ func NewSocks(ctx context.Context, router adapter.Router, logger log.ContextLogg
 	inbound := &Socks{
 		myInboundAdapter{
 			protocol:      C.TypeSocks,
-			network:       []string{C.NetworkTCP},
+			network:       []string{N.NetworkTCP},
 			ctx:           ctx,
 			router:        router,
 			logger:        logger,

+ 2 - 2
inbound/tun.go

@@ -111,7 +111,7 @@ func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata
 	var metadata adapter.InboundContext
 	metadata.Inbound = t.tag
 	metadata.InboundType = C.TypeTun
-	metadata.Network = C.NetworkTCP
+	metadata.Network = N.NetworkTCP
 	metadata.Source = upstreamMetadata.Source
 	metadata.Destination = upstreamMetadata.Destination
 	metadata.SniffEnabled = t.inboundOptions.SniffEnabled
@@ -134,7 +134,7 @@ func (t *Tun) NewPacketConnection(ctx context.Context, conn N.PacketConn, upstre
 	var metadata adapter.InboundContext
 	metadata.Inbound = t.tag
 	metadata.InboundType = C.TypeTun
-	metadata.Network = C.NetworkUDP
+	metadata.Network = N.NetworkUDP
 	metadata.Source = upstreamMetadata.Source
 	metadata.Destination = upstreamMetadata.Destination
 	metadata.SniffEnabled = t.inboundOptions.SniffEnabled

+ 1 - 1
inbound/vmess.go

@@ -30,7 +30,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
 	inbound := &VMess{
 		myInboundAdapter: myInboundAdapter{
 			protocol:      C.TypeVMess,
-			network:       []string{C.NetworkTCP},
+			network:       []string{N.NetworkTCP},
 			ctx:           ctx,
 			router:        router,
 			logger:        logger,

+ 4 - 0
log/export.go

@@ -12,6 +12,10 @@ func init() {
 	std = NewFactory(Formatter{BaseTime: time.Now()}, os.Stderr).Logger()
 }
 
+func StdLogger() ContextLogger {
+	return std
+}
+
 func Trace(args ...any) {
 	std.Trace(args...)
 }

+ 7 - 0
option/outbound.go

@@ -98,3 +98,10 @@ type ServerOptions struct {
 func (o ServerOptions) Build() M.Socksaddr {
 	return M.ParseSocksaddrHostPort(o.Server, o.ServerPort)
 }
+
+type MultiplexOptions struct {
+	Enabled        bool `json:"enabled,omitempty"`
+	MaxConnections int  `json:"max_connections,omitempty"`
+	MinStreams     int  `json:"min_streams,omitempty"`
+	MaxStreams     int  `json:"max_streams,omitempty"`
+}

+ 4 - 3
option/shadowsocks.go

@@ -24,7 +24,8 @@ type ShadowsocksDestination struct {
 type ShadowsocksOutboundOptions struct {
 	OutboundDialerOptions
 	ServerOptions
-	Method   string      `json:"method"`
-	Password string      `json:"password"`
-	Network  NetworkList `json:"network,omitempty"`
+	Method    string            `json:"method"`
+	Password  string            `json:"password"`
+	Network   NetworkList       `json:"network,omitempty"`
+	Multiplex *MultiplexOptions `json:"multiplex,omitempty"`
 }

+ 3 - 3
option/types.go

@@ -6,9 +6,9 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/common/json"
-	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-dns"
 	E "github.com/sagernet/sing/common/exceptions"
+	N "github.com/sagernet/sing/common/network"
 )
 
 type ListenAddress netip.Addr
@@ -50,7 +50,7 @@ func (v *NetworkList) UnmarshalJSON(content []byte) error {
 	}
 	for _, networkName := range networkList {
 		switch networkName {
-		case C.NetworkTCP, C.NetworkUDP:
+		case N.NetworkTCP, N.NetworkUDP:
 			break
 		default:
 			return E.New("unknown network: " + networkName)
@@ -62,7 +62,7 @@ func (v *NetworkList) UnmarshalJSON(content []byte) error {
 
 func (v NetworkList) Build() []string {
 	if v == "" {
-		return []string{C.NetworkTCP, C.NetworkUDP}
+		return []string{N.NetworkTCP, N.NetworkUDP}
 	}
 	return strings.Split(string(v), "\n")
 }

+ 1 - 1
outbound/block.go

@@ -22,7 +22,7 @@ func NewBlock(logger log.ContextLogger, tag string) *Block {
 	return &Block{
 		myOutboundAdapter{
 			protocol: C.TypeBlock,
-			network:  []string{C.NetworkTCP, C.NetworkUDP},
+			network:  []string{N.NetworkTCP, N.NetworkUDP},
 			logger:   logger,
 			tag:      tag,
 		},

+ 4 - 2
outbound/builder.go

@@ -1,6 +1,8 @@
 package outbound
 
 import (
+	"context"
+
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -8,7 +10,7 @@ import (
 	E "github.com/sagernet/sing/common/exceptions"
 )
 
-func New(router adapter.Router, logger log.ContextLogger, options option.Outbound) (adapter.Outbound, error) {
+func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.Outbound) (adapter.Outbound, error) {
 	if options.Type == "" {
 		return nil, E.New("missing outbound type")
 	}
@@ -24,7 +26,7 @@ func New(router adapter.Router, logger log.ContextLogger, options option.Outboun
 	case C.TypeHTTP:
 		return NewHTTP(router, logger, options.Tag, options.HTTPOptions)
 	case C.TypeShadowsocks:
-		return NewShadowsocks(router, logger, options.Tag, options.ShadowsocksOptions)
+		return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions)
 	case C.TypeVMess:
 		return NewVMess(router, logger, options.Tag, options.VMessOptions)
 	case C.TypeSelector:

+ 8 - 8
outbound/default.go

@@ -42,12 +42,12 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a
 	var outConn net.Conn
 	var err error
 	if len(metadata.DestinationAddresses) > 0 {
-		outConn, err = N.DialSerial(ctx, this, C.NetworkTCP, metadata.Destination, metadata.DestinationAddresses)
+		outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses)
 	} else {
-		outConn, err = this.DialContext(ctx, C.NetworkTCP, metadata.Destination)
+		outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
 	}
 	if err != nil {
-		return err
+		return N.HandshakeFailure(conn, err)
 	}
 	return bufio.CopyConn(ctx, conn, outConn)
 }
@@ -57,12 +57,12 @@ func NewEarlyConnection(ctx context.Context, this N.Dialer, conn net.Conn, metad
 	var outConn net.Conn
 	var err error
 	if len(metadata.DestinationAddresses) > 0 {
-		outConn, err = N.DialSerial(ctx, this, C.NetworkTCP, metadata.Destination, metadata.DestinationAddresses)
+		outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses)
 	} else {
-		outConn, err = this.DialContext(ctx, C.NetworkTCP, metadata.Destination)
+		outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
 	}
 	if err != nil {
-		return err
+		return N.HandshakeFailure(conn, err)
 	}
 	return CopyEarlyConn(ctx, conn, outConn)
 }
@@ -77,7 +77,7 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn,
 		outConn, err = this.ListenPacket(ctx, metadata.Destination)
 	}
 	if err != nil {
-		return err
+		return N.HandshakeFailure(conn, err)
 	}
 	if metadata.Protocol != "" {
 		switch metadata.Protocol {
@@ -120,7 +120,7 @@ func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) erro
 	}
 	_, err = serverConn.Write(payload.Bytes())
 	if err != nil {
-		return E.Cause(err, "client handshake")
+		return N.HandshakeFailure(conn, err)
 	}
 	runtime.KeepAlive(_payload)
 	return bufio.CopyConn(ctx, conn, serverConn)

+ 3 - 3
outbound/direct.go

@@ -26,7 +26,7 @@ func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, opti
 	outbound := &Direct{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol: C.TypeDirect,
-			network:  []string{C.NetworkTCP, C.NetworkUDP},
+			network:  []string{N.NetworkTCP, N.NetworkUDP},
 			router:   router,
 			logger:   logger,
 			tag:      tag,
@@ -61,9 +61,9 @@ func (h *Direct) DialContext(ctx context.Context, network string, destination M.
 		destination.Port = h.overrideDestination.Port
 	}
 	switch network {
-	case C.NetworkTCP:
+	case N.NetworkTCP:
 		h.logger.InfoContext(ctx, "outbound connection to ", destination)
-	case C.NetworkUDP:
+	case N.NetworkUDP:
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 	}
 	return h.dialer.DialContext(ctx, network, destination)

+ 1 - 1
outbound/dns.go

@@ -30,7 +30,7 @@ func NewDNS(router adapter.Router, logger log.ContextLogger, tag string) *DNS {
 	return &DNS{
 		myOutboundAdapter{
 			protocol: C.TypeDNS,
-			network:  []string{C.NetworkTCP, C.NetworkUDP},
+			network:  []string{N.NetworkTCP, N.NetworkUDP},
 			router:   router,
 			logger:   logger,
 			tag:      tag,

+ 1 - 1
outbound/http.go

@@ -31,7 +31,7 @@ func NewHTTP(router adapter.Router, logger log.ContextLogger, tag string, option
 	return &HTTP{
 		myOutboundAdapter{
 			protocol: C.TypeHTTP,
-			network:  []string{C.NetworkTCP},
+			network:  []string{N.NetworkTCP},
 			router:   router,
 			logger:   logger,
 			tag:      tag,

+ 1 - 1
outbound/selector.go

@@ -46,7 +46,7 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op
 
 func (s *Selector) Network() []string {
 	if s.selected == nil {
-		return []string{C.NetworkTCP, C.NetworkUDP}
+		return []string{N.NetworkTCP, N.NetworkUDP}
 	}
 	return s.selected.Network()
 }

+ 44 - 26
outbound/shadowsocks.go

@@ -6,12 +6,15 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/dialer"
+	"github.com/sagernet/sing-box/common/mux"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-shadowsocks"
 	"github.com/sagernet/sing-shadowsocks/shadowimpl"
+	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
+	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
@@ -20,70 +23,85 @@ var _ adapter.Outbound = (*Shadowsocks)(nil)
 
 type Shadowsocks struct {
 	myOutboundAdapter
-	dialer     N.Dialer
-	method     shadowsocks.Method
-	serverAddr M.Socksaddr
+	dialer          N.Dialer
+	method          shadowsocks.Method
+	serverAddr      M.Socksaddr
+	multiplexDialer N.Dialer
 }
 
-func NewShadowsocks(router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksOutboundOptions) (*Shadowsocks, error) {
+func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksOutboundOptions) (*Shadowsocks, error) {
 	method, err := shadowimpl.FetchMethod(options.Method, options.Password)
 	if err != nil {
 		return nil, err
 	}
-	return &Shadowsocks{
-		myOutboundAdapter{
+	outbound := &Shadowsocks{
+		myOutboundAdapter: myOutboundAdapter{
 			protocol: C.TypeShadowsocks,
 			network:  options.Network.Build(),
 			router:   router,
 			logger:   logger,
 			tag:      tag,
 		},
-		dialer.NewOutbound(router, options.OutboundDialerOptions),
-		method,
-		options.ServerOptions.Build(),
-	}, nil
+		dialer:     dialer.NewOutbound(router, options.OutboundDialerOptions),
+		method:     method,
+		serverAddr: options.ServerOptions.Build(),
+	}
+	outbound.multiplexDialer = mux.NewClientWithOptions(ctx, (*shadowsocksDialer)(outbound), common.PtrValueOrDefault(options.Multiplex))
+	return outbound, nil
 }
 
 func (h *Shadowsocks) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	return h.multiplexDialer.DialContext(ctx, network, destination)
+}
+
+func (h *Shadowsocks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	return h.multiplexDialer.ListenPacket(ctx, destination)
+}
+
+func (h *Shadowsocks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	return NewEarlyConnection(ctx, h, conn, metadata)
+}
+
+func (h *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return NewPacketConnection(ctx, h, conn, metadata)
+}
+
+var _ N.Dialer = (*shadowsocksDialer)(nil)
+
+type shadowsocksDialer Shadowsocks
+
+func (h *shadowsocksDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	ctx, metadata := adapter.AppendContext(ctx)
 	metadata.Outbound = h.tag
 	metadata.Destination = destination
-	switch network {
-	case C.NetworkTCP:
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
 		h.logger.InfoContext(ctx, "outbound connection to ", destination)
-		outConn, err := h.dialer.DialContext(ctx, C.NetworkTCP, h.serverAddr)
+		outConn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
 		if err != nil {
 			return nil, err
 		}
 		return h.method.DialEarlyConn(outConn, destination), nil
-	case C.NetworkUDP:
+	case N.NetworkUDP:
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
-		outConn, err := h.dialer.DialContext(ctx, C.NetworkUDP, h.serverAddr)
+		outConn, err := h.dialer.DialContext(ctx, N.NetworkUDP, h.serverAddr)
 		if err != nil {
 			return nil, err
 		}
 		return &bufio.BindPacketConn{PacketConn: h.method.DialPacketConn(outConn), Addr: destination}, nil
 	default:
-		panic("unknown network " + network)
+		return nil, E.Extend(N.ErrUnknownNetwork, network)
 	}
 }
 
-func (h *Shadowsocks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+func (h *shadowsocksDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
 	ctx, metadata := adapter.AppendContext(ctx)
 	metadata.Outbound = h.tag
 	metadata.Destination = destination
 	h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
-	outConn, err := h.dialer.DialContext(ctx, "udp", h.serverAddr)
+	outConn, err := h.dialer.DialContext(ctx, N.NetworkUDP, h.serverAddr)
 	if err != nil {
 		return nil, err
 	}
 	return h.method.DialPacketConn(outConn), nil
 }
-
-func (h *Shadowsocks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewEarlyConnection(ctx, h, conn, metadata)
-}
-
-func (h *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	return NewPacketConnection(ctx, h, conn, metadata)
-}

+ 5 - 4
outbound/socks.go

@@ -9,6 +9,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	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/protocol/socks"
@@ -49,13 +50,13 @@ func (h *Socks) DialContext(ctx context.Context, network string, destination M.S
 	ctx, metadata := adapter.AppendContext(ctx)
 	metadata.Outbound = h.tag
 	metadata.Destination = destination
-	switch network {
-	case C.NetworkTCP:
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
 		h.logger.InfoContext(ctx, "outbound connection to ", destination)
-	case C.NetworkUDP:
+	case N.NetworkUDP:
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 	default:
-		panic("unknown network " + network)
+		return nil, E.Extend(N.ErrUnknownNetwork, network)
 	}
 	return h.client.DialContext(ctx, network, destination)
 }

+ 8 - 7
outbound/vmess.go

@@ -11,6 +11,7 @@ import (
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-vmess"
 	"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"
 )
@@ -58,28 +59,28 @@ func (h *VMess) DialContext(ctx context.Context, network string, destination M.S
 	ctx, metadata := adapter.AppendContext(ctx)
 	metadata.Outbound = h.tag
 	metadata.Destination = destination
-	switch network {
-	case C.NetworkTCP:
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
 		h.logger.InfoContext(ctx, "outbound connection to ", destination)
-		outConn, err := h.dialer.DialContext(ctx, C.NetworkTCP, h.serverAddr)
+		outConn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
 		if err != nil {
 			return nil, err
 		}
 		return h.client.DialEarlyConn(outConn, destination), nil
-	case C.NetworkUDP:
+	case N.NetworkUDP:
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
-		outConn, err := h.dialer.DialContext(ctx, C.NetworkTCP, h.serverAddr)
+		outConn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
 		if err != nil {
 			return nil, err
 		}
 		return h.client.DialEarlyPacketConn(outConn, destination), nil
 	default:
-		panic("unknown network " + network)
+		return nil, E.Extend(N.ErrUnknownNetwork, network)
 	}
 }
 
 func (h *VMess) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
-	conn, err := h.DialContext(ctx, C.NetworkUDP, destination)
+	conn, err := h.DialContext(ctx, N.NetworkUDP, destination)
 	if err != nil {
 		return nil, err
 	}

+ 17 - 7
route/router.go

@@ -19,6 +19,7 @@ import (
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/geoip"
 	"github.com/sagernet/sing-box/common/geosite"
+	"github.com/sagernet/sing-box/common/mux"
 	"github.com/sagernet/sing-box/common/process"
 	"github.com/sagernet/sing-box/common/sniff"
 	"github.com/sagernet/sing-box/common/warning"
@@ -278,17 +279,17 @@ func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func()
 		if !loaded {
 			return E.New("default detour not found: ", r.defaultDetour)
 		}
-		if common.Contains(detour.Network(), C.NetworkTCP) {
+		if common.Contains(detour.Network(), N.NetworkTCP) {
 			defaultOutboundForConnection = detour
 		}
-		if common.Contains(detour.Network(), C.NetworkUDP) {
+		if common.Contains(detour.Network(), N.NetworkUDP) {
 			defaultOutboundForPacketConnection = detour
 		}
 	}
 	var index, packetIndex int
 	if defaultOutboundForConnection == nil {
 		for i, detour := range outbounds {
-			if common.Contains(detour.Network(), C.NetworkTCP) {
+			if common.Contains(detour.Network(), N.NetworkTCP) {
 				index = i
 				defaultOutboundForConnection = detour
 				break
@@ -297,7 +298,7 @@ func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func()
 	}
 	if defaultOutboundForPacketConnection == nil {
 		for i, detour := range outbounds {
-			if common.Contains(detour.Network(), C.NetworkUDP) {
+			if common.Contains(detour.Network(), N.NetworkUDP) {
 				packetIndex = i
 				defaultOutboundForPacketConnection = detour
 				break
@@ -478,7 +479,7 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
 }
 
 func (r *Router) DefaultOutbound(network string) adapter.Outbound {
-	if network == C.NetworkTCP {
+	if network == N.NetworkTCP {
 		return r.defaultOutboundForConnection
 	} else {
 		return r.defaultOutboundForPacketConnection
@@ -486,6 +487,10 @@ func (r *Router) DefaultOutbound(network string) adapter.Outbound {
 }
 
 func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	if metadata.Destination.Fqdn == mux.Destination.Fqdn {
+		r.logger.InfoContext(ctx, "inbound multiplex connection")
+		return mux.NewConnection(ctx, r, r, r.logger, conn, metadata)
+	}
 	if metadata.SniffEnabled {
 		_buffer := buf.StackNew()
 		defer common.KeepAlive(_buffer)
@@ -517,7 +522,7 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
 		r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]")
 	}
 	matchedRule, detour := r.match(ctx, &metadata, r.defaultOutboundForConnection)
-	if !common.Contains(detour.Network(), C.NetworkTCP) {
+	if !common.Contains(detour.Network(), N.NetworkTCP) {
 		conn.Close()
 		return E.New("missing supported outbound, closing connection")
 	}
@@ -564,7 +569,7 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
 		r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]")
 	}
 	matchedRule, detour := r.match(ctx, &metadata, r.defaultOutboundForPacketConnection)
-	if !common.Contains(detour.Network(), C.NetworkUDP) {
+	if !common.Contains(detour.Network(), N.NetworkUDP) {
 		conn.Close()
 		return E.New("missing supported outbound, closing packet connection")
 	}
@@ -927,5 +932,10 @@ func (r *Router) downloadGeositeDatabase(savePath string) error {
 }
 
 func (r *Router) NewError(ctx context.Context, err error) {
+	common.Close(err)
+	if E.IsClosedOrCanceled(err) {
+		r.logger.TraceContext(ctx, "connection closed: ", err)
+		return
+	}
 	r.logger.ErrorContext(ctx, err)
 }

+ 2 - 1
route/rule.go

@@ -10,6 +10,7 @@ import (
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
+	N "github.com/sagernet/sing/common/network"
 )
 
 func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule) (adapter.Rule, error) {
@@ -73,7 +74,7 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
 	}
 	if options.Network != "" {
 		switch options.Network {
-		case C.NetworkTCP, C.NetworkUDP:
+		case N.NetworkTCP, N.NetworkUDP:
 			item := NewNetworkItem(options.Network)
 			rule.items = append(rule.items, item)
 			rule.allItems = append(rule.allItems, item)

+ 2 - 1
route/rule_dns.go

@@ -10,6 +10,7 @@ import (
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
+	N "github.com/sagernet/sing/common/network"
 )
 
 func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule) (adapter.DNSRule, error) {
@@ -59,7 +60,7 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
 	}
 	if options.Network != "" {
 		switch options.Network {
-		case C.NetworkTCP, C.NetworkUDP:
+		case N.NetworkTCP, N.NetworkUDP:
 			item := NewNetworkItem(options.Network)
 			rule.items = append(rule.items, item)
 			rule.allItems = append(rule.allItems, item)

+ 8 - 6
test/go.mod

@@ -10,14 +10,16 @@ require (
 	github.com/docker/docker v20.10.17+incompatible
 	github.com/docker/go-connections v0.4.0
 	github.com/gofrs/uuid v4.2.0+incompatible
-	github.com/sagernet/sing v0.0.0-20220726034811-bc109486f14e
+	github.com/sagernet/sing v0.0.0-20220729120910-4376f188c512
+	github.com/sagernet/sing-shadowsocks v0.0.0-20220729155919-91d2780bfc80
 	github.com/spyzhov/ajson v0.7.1
 	github.com/stretchr/testify v1.8.0
-	golang.org/x/net v0.0.0-20220725212005-46097bf591d3
+	golang.org/x/net v0.0.0-20220728211354-c7608f3a8462
 )
 
 require (
 	github.com/Microsoft/go-winio v0.5.1 // indirect
+	github.com/ajg/form v1.5.1 // indirect
 	github.com/cheekybits/genny v1.0.0 // indirect
 	github.com/database64128/tfo-go v1.1.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
@@ -26,11 +28,12 @@ require (
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/go-chi/chi/v5 v5.0.7 // indirect
 	github.com/go-chi/cors v1.2.1 // indirect
-	github.com/go-chi/render v1.0.1 // indirect
+	github.com/go-chi/render v1.0.2 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/google/btree v1.0.1 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
+	github.com/hashicorp/yamux v0.1.1 // indirect
 	github.com/klauspost/cpuid/v2 v2.0.12 // indirect
 	github.com/kr/text v0.2.0 // indirect
 	github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
@@ -49,8 +52,7 @@ require (
 	github.com/oschwald/maxminddb-golang v1.9.0 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/sagernet/sing-dns v0.0.0-20220726044716-2b8c696b09f5 // indirect
-	github.com/sagernet/sing-shadowsocks v0.0.0-20220726034922-ebbaadcae06b // indirect
+	github.com/sagernet/sing-dns v0.0.0-20220729120941-109c0a7aabb1 // indirect
 	github.com/sagernet/sing-tun v0.0.0-20220726111504-b4bded886e01 // indirect
 	github.com/sagernet/sing-vmess v0.0.0-20220726034841-4dae776653e5 // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
@@ -59,7 +61,7 @@ require (
 	go.uber.org/atomic v1.9.0 // indirect
 	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
 	golang.org/x/mod v0.5.1 // indirect
-	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
+	golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
 	golang.org/x/tools v0.1.9 // indirect

+ 16 - 12
test/go.sum

@@ -12,6 +12,8 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg6
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
 github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
+github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
@@ -48,8 +50,8 @@ github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
 github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
 github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
-github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
-github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
+github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
+github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
@@ -96,6 +98,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
+github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
+github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -168,12 +172,12 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:
 github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
-github.com/sagernet/sing v0.0.0-20220726034811-bc109486f14e h1:5lfrAc+vSv0iW6eHGNLyHC+a/k6BDGJvYxYxwB/68Kk=
-github.com/sagernet/sing v0.0.0-20220726034811-bc109486f14e/go.mod h1:GbtQfZSpmtD3cXeD1qX2LCMwY8dH+bnnInDTqd92IsM=
-github.com/sagernet/sing-dns v0.0.0-20220726044716-2b8c696b09f5 h1:l6ztUAFVhWhY0XOq7ISbwVBE4YLWMxfIN6HptgaOl4I=
-github.com/sagernet/sing-dns v0.0.0-20220726044716-2b8c696b09f5/go.mod h1:KL+8wZG3gqHLm+nvNI3ZNaPzCMA4T7KIwsGp7ix9a34=
-github.com/sagernet/sing-shadowsocks v0.0.0-20220726034922-ebbaadcae06b h1:6wJoJaroW3WCGjHGu7XPOSLEKP9Loi3Ox4+7A1kRTsQ=
-github.com/sagernet/sing-shadowsocks v0.0.0-20220726034922-ebbaadcae06b/go.mod h1:mH6wE4b5FZp1Q/meATe4tjiPjvQO9E7Lr0FBBwFYp4I=
+github.com/sagernet/sing v0.0.0-20220729120910-4376f188c512 h1:dCWDE55LpZu//W02FccNbGObZFlv1N2NS0yUdf2i4Mc=
+github.com/sagernet/sing v0.0.0-20220729120910-4376f188c512/go.mod h1:GbtQfZSpmtD3cXeD1qX2LCMwY8dH+bnnInDTqd92IsM=
+github.com/sagernet/sing-dns v0.0.0-20220729120941-109c0a7aabb1 h1:Gv9ow1IF98Qdxs+X8unPHJG4iwuEWoq0PE/jvlIqgqY=
+github.com/sagernet/sing-dns v0.0.0-20220729120941-109c0a7aabb1/go.mod h1:LQJDT4IpqyWI6NugkSSqxTcFfxxNBp94n+fXtHFMboQ=
+github.com/sagernet/sing-shadowsocks v0.0.0-20220729155919-91d2780bfc80 h1:gpCPZyZJQVn6ZTBCJ/XaYbPi6j43TdyTty/MI5bXhbE=
+github.com/sagernet/sing-shadowsocks v0.0.0-20220729155919-91d2780bfc80/go.mod h1:mH6wE4b5FZp1Q/meATe4tjiPjvQO9E7Lr0FBBwFYp4I=
 github.com/sagernet/sing-tun v0.0.0-20220726111504-b4bded886e01 h1:tNJn7T87sgQyA8gpEvC6LbusV4lkhZU8oi4mRujOhM8=
 github.com/sagernet/sing-tun v0.0.0-20220726111504-b4bded886e01/go.mod h1:bYHamPB16GFGt34ayYt56Pb7aN64RPY0+uuFPBSbj0U=
 github.com/sagernet/sing-vmess v0.0.0-20220726034841-4dae776653e5 h1:TNguWTPF6gxX/gR02hY3LGviUn6LGlDPofE6lpSJWeo=
@@ -268,8 +272,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.0.0-20220725212005-46097bf591d3 h1:2yWTtPWWRcISTw3/o+s/Y4UOMnQL71DWyToOANFusCg=
-golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM=
+golang.org/x/net v0.0.0-20220728211354-c7608f3a8462 h1:UreQrH7DbFXSi9ZFox6FNT3WBooWmdANpU+IfkT1T4I=
+golang.org/x/net v0.0.0-20220728211354-c7608f3a8462/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -310,8 +314,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

+ 74 - 0
test/mux_test.go

@@ -0,0 +1,74 @@
+package main
+
+import (
+	"net/netip"
+	"testing"
+
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
+)
+
+func TestShadowsocksMux(t *testing.T) {
+	method := shadowaead_2022.List[0]
+	password := mkBase64(t, 16)
+	startInstance(t, option.Options{
+		Log: &option.LogOptions{
+			Level: "debug",
+		},
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeShadowsocks,
+				ShadowsocksOptions: option.ShadowsocksInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Method:   method,
+					Password: password,
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeShadowsocks,
+				Tag:  "ss-out",
+				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					Method:   method,
+					Password: password,
+					Multiplex: &option.MultiplexOptions{
+						Enabled: true,
+					},
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "ss-out",
+					},
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}