Ver Fonte

Add user rule item

世界 há 3 anos atrás
pai
commit
d1e83882e5

+ 1 - 0
adapter/inbound.go

@@ -21,6 +21,7 @@ type InboundContext struct {
 	Destination M.Socksaddr
 	Domain      string
 	Protocol    string
+	User        string
 	Outbound    string
 
 	// cache

+ 3 - 100
common/settings/proxy_windows.go

@@ -1,111 +1,14 @@
 package settings
 
 import (
-	"os"
-	"syscall"
-	"unsafe"
-
 	F "github.com/sagernet/sing/common/format"
-
-	"golang.org/x/sys/windows"
-)
-
-var (
-	modwininet             = windows.NewLazySystemDLL("settings.dll")
-	procInternetSetOptionW = modwininet.NewProc("InternetSetOptionW")
-)
-
-const (
-	internetOptionPerConnectionOption  = 75
-	internetOptionSettingsChanged      = 39
-	internetOptionRefresh              = 37
-	internetOptionProxySettingsChanged = 95
-)
-
-const (
-	internetPerConnFlags                     = 1
-	internetPerConnProxyServer               = 2
-	internetPerConnProxyBypass               = 3
-	internetPerConnAutoconfigUrl             = 4
-	internetPerConnAutodiscoveryFlags        = 5
-	internetPerConnAutoconfigSecondaryUrl    = 6
-	internetPerConnAutoconfigReloadDelayMins = 7
-	internetPerConnAutoconfigLastDetectTime  = 8
-	internetPerConnAutoconfigLastDetectUrl   = 9
-	internetPerConnFlagsUi                   = 10
-	internetOptionProxyUsername              = 43
-	internetOptionProxyPassword              = 44
-)
-
-const (
-	proxyTypeDirect       = 1
-	proxyTypeProxy        = 2
-	proxyTypeAutoProxyUrl = 4
-	proxyTypeAutoDetect   = 8
+	"github.com/sagernet/sing/common/wininet"
 )
 
-type internetPerConnOptionList struct {
-	dwSize        uint32
-	pszConnection uintptr
-	dwOptionCount uint32
-	dwOptionError uint32
-	pOptions      uintptr
-}
-
-type internetPerConnOption struct {
-	dwOption uint32
-	value    [8]byte
-}
-
-func internetSetOption(option uintptr, lpBuffer uintptr, dwBufferSize uintptr) error {
-	r0, _, err := syscall.SyscallN(procInternetSetOptionW.Addr(), 0, option, lpBuffer, dwBufferSize)
-	if r0 != 1 {
-		return err
-	}
-	return nil
-}
-
-func setOptions(options ...internetPerConnOption) error {
-	var optionList internetPerConnOptionList
-	optionList.dwSize = uint32(unsafe.Sizeof(optionList))
-	optionList.dwOptionCount = uint32(len(options))
-	optionList.dwOptionError = 0
-	optionList.pOptions = uintptr(unsafe.Pointer(&options[0]))
-	err := internetSetOption(internetOptionPerConnectionOption, uintptr(unsafe.Pointer(&optionList)), uintptr(optionList.dwSize))
-	if err != nil {
-		return os.NewSyscallError("InternetSetOption(Direct)", err)
-	}
-	err = internetSetOption(internetOptionSettingsChanged, 0, 0)
-	if err != nil {
-		return os.NewSyscallError("InternetSetOption(SettingsChanged)", err)
-	}
-	err = internetSetOption(internetOptionProxySettingsChanged, 0, 0)
-	if err != nil {
-		return os.NewSyscallError("InternetSetOption(ProxySettingsChanged)", err)
-	}
-	err = internetSetOption(internetOptionRefresh, 0, 0)
-	if err != nil {
-		return os.NewSyscallError("InternetSetOption(Refresh)", err)
-	}
-	return nil
-}
-
 func ClearSystemProxy() error {
-	var flagsOption internetPerConnOption
-	flagsOption.dwOption = internetPerConnFlags
-	*((*uint32)(unsafe.Pointer(&flagsOption.value))) = proxyTypeDirect | proxyTypeAutoDetect
-	return setOptions(flagsOption)
+	return wininet.ClearSystemProxy()
 }
 
 func SetSystemProxy(port uint16, mixed bool) error {
-	var flagsOption internetPerConnOption
-	flagsOption.dwOption = internetPerConnFlags
-	*((*uint32)(unsafe.Pointer(&flagsOption.value))) = proxyTypeProxy | proxyTypeDirect
-	var proxyOption internetPerConnOption
-	proxyOption.dwOption = internetPerConnProxyServer
-	*((*uintptr)(unsafe.Pointer(&proxyOption.value))) = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(F.ToString("http://127.0.0.1:", port))))
-	var bypassOption internetPerConnOption
-	bypassOption.dwOption = internetPerConnProxyBypass
-	*((*uintptr)(unsafe.Pointer(&bypassOption.value))) = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("local")))
-	return setOptions(flagsOption, proxyOption, bypassOption)
+	return wininet.SetSystemProxy(F.ToString("http://127.0.0.1:", port), "local")
 }

+ 12 - 0
docs/configuration/dns/rule.md

@@ -9,6 +9,10 @@
           "mixed-in"
         ],
         "network": "tcp",
+        "user": [
+          "usera",
+          "userb"
+        ],
         "protocol": [
           "tls",
           "http",
@@ -80,6 +84,14 @@ Tags of [inbound](../inbound).
 
 `tcp` or `udp`.
 
+#### user
+
+Username, see each inbound for details.
+
+#### protocol
+
+Sniffed protocol, see [Sniff](/configuration/route/sniff/) for details.
+
 #### domain
 
 Match full domain.

+ 1 - 3
docs/configuration/inbound/direct.md

@@ -47,9 +47,7 @@ Enable tcp fast open for listener.
 
 Enable sniffing.
 
-Reads domain names for routing, supports HTTP TLS for TCP, QUIC for UDP.
-
-This does not break zero copy, like splice.
+See [Sniff](/configuration/route/sniff/) for details.
 
 #### sniff_override_destination
 

+ 1 - 3
docs/configuration/inbound/http.md

@@ -51,9 +51,7 @@ Enable tcp fast open for listener.
 
 Enable sniffing.
 
-Reads domain names for routing, supports HTTP TLS for TCP, QUIC for UDP.
-
-This does not break zero copy, like splice.
+See [Sniff](/configuration/route/sniff/) for details.
 
 #### sniff_override_destination
 

+ 1 - 3
docs/configuration/inbound/mixed.md

@@ -51,9 +51,7 @@ Enable tcp fast open for listener.
 
 Enable sniffing.
 
-Reads domain names for routing, supports HTTP TLS for TCP, QUIC for UDP.
-
-This does not break zero copy, like splice.
+See [Sniff](/configuration/route/sniff/) for details.
 
 #### sniff_override_destination
 

+ 2 - 3
docs/configuration/inbound/redirect.md

@@ -8,6 +8,7 @@
     {
       "type": "redirect",
       "tag": "redirect-in",
+      
       "listen": "::",
       "listen_port": 5353,
       "sniff": false,
@@ -36,9 +37,7 @@ Listen port.
 
 Enable sniffing.
 
-Reads domain names for routing, supports HTTP TLS for TCP, QUIC for UDP.
-
-This does not break zero copy, like splice.
+See [Sniff](/configuration/route/sniff/) for details.
 
 #### sniff_override_destination
 

+ 1 - 3
docs/configuration/inbound/shadowsocks.md

@@ -47,9 +47,7 @@ Enable tcp fast open for listener.
 
 Enable sniffing.
 
-Reads domain names for routing, supports HTTP TLS for TCP, QUIC for UDP.
-
-This does not break zero copy, like splice.
+See [Sniff](/configuration/route/sniff/) for details.
 
 #### sniff_override_destination
 

+ 1 - 3
docs/configuration/inbound/socks.md

@@ -49,9 +49,7 @@ Enable tcp fast open for listener.
 
 Enable sniffing.
 
-Reads domain names for routing, supports HTTP TLS for TCP, QUIC for UDP.
-
-This does not break zero copy, like splice.
+See [Sniff](/configuration/route/sniff/) for details.
 
 #### sniff_override_destination
 

+ 1 - 3
docs/configuration/inbound/tproxy.md

@@ -40,9 +40,7 @@ Listen port.
 
 Enable sniffing.
 
-Reads domain names for routing, supports HTTP TLS for TCP, QUIC for UDP.
-
-This does not break zero copy, like splice.
+See [Sniff](/configuration/route/sniff/) for details.
 
 #### sniff_override_destination
 

+ 1 - 3
docs/configuration/inbound/tun.md

@@ -59,9 +59,7 @@ Hijack TCP/UDP DNS requests to the built-in DNS adapter.
 
 Enable sniffing.
 
-Reads domain names for routing, supports HTTP TLS for TCP, QUIC for UDP.
-
-This does not break zero copy, like splice.
+See [Sniff](/configuration/route/sniff/) for details.
 
 #### sniff_override_destination
 

+ 12 - 0
docs/configuration/route/rule.md

@@ -9,6 +9,10 @@
           "mixed-in"
         ],
         "network": "tcp",
+        "user": [
+          "usera",
+          "userb"
+        ],
         "protocol": [
           "tls",
           "http",
@@ -79,6 +83,14 @@
 
 Tags of [inbound](../inbound).
 
+#### user
+
+Username, see each inbound for details.
+
+#### protocol
+
+Sniffed protocol, see [Sniff](/configuration/route/sniff/) for details.
+
 #### network
 
 `tcp` or `udp`.

+ 10 - 0
docs/configuration/route/sniff.md

@@ -0,0 +1,10 @@
+If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed.
+
+#### Supported Protocols
+
+|  Network  |  Protocol  |  Domain Name  |
+|:---------:|:----------:|:-------------:|
+|    TCP    |    HTTP    |     Host      |
+|    TCP    |    TLS     |  Server Name  |
+|    UDP    |    QUIC    |  Server Name  |
+|    UDP    |    STUN    |       /       |

+ 2 - 2
go.mod

@@ -7,9 +7,9 @@ require (
 	github.com/goccy/go-json v0.9.10
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/oschwald/maxminddb-golang v1.9.0
-	github.com/sagernet/sing v0.0.0-20220716021830-bd79d31e3b10
+	github.com/sagernet/sing v0.0.0-20220717063925-00f98eb6bc34
 	github.com/sagernet/sing-dns v0.0.0-20220711062726-c64e938e4619
-	github.com/sagernet/sing-shadowsocks v0.0.0-20220716012931-952ae62e05d7
+	github.com/sagernet/sing-shadowsocks v0.0.0-20220717063942-45a2ad9cd41f
 	github.com/sagernet/sing-tun v0.0.0-20220717030718-f53aabff275f
 	github.com/spf13/cobra v1.5.0
 	github.com/stretchr/testify v1.8.0

+ 4 - 4
go.sum

@@ -25,12 +25,12 @@ github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sagernet/sing v0.0.0-20220716021830-bd79d31e3b10 h1:CQSsVgvVT6KcYNQASP4jnPTg7epSxHGI3MS011LIXkA=
-github.com/sagernet/sing v0.0.0-20220716021830-bd79d31e3b10/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
+github.com/sagernet/sing v0.0.0-20220717063925-00f98eb6bc34 h1:1kFruA2QzuH2R6txJXEDSasfdxzsjNyzC4Z1kZjMkHg=
+github.com/sagernet/sing v0.0.0-20220717063925-00f98eb6bc34/go.mod h1:GbtQfZSpmtD3cXeD1qX2LCMwY8dH+bnnInDTqd92IsM=
 github.com/sagernet/sing-dns v0.0.0-20220711062726-c64e938e4619 h1:oHbOmq1WS0XaZmXp6WpxzyB2xeyRIA1/L8EJKuNntfY=
 github.com/sagernet/sing-dns v0.0.0-20220711062726-c64e938e4619/go.mod h1:y2fpvoxukw3G7eApIZwkcpcG/NE4AB8pCQI0Qd8rMqk=
-github.com/sagernet/sing-shadowsocks v0.0.0-20220716012931-952ae62e05d7 h1:7xQvlMSxNWphQ4t+7fHfR4OnkH23GukLIjImnM1CMLA=
-github.com/sagernet/sing-shadowsocks v0.0.0-20220716012931-952ae62e05d7/go.mod h1:NtHwPOk1wEOPdjjsjtrYoaQuXtlDCrx0mrcWBrNE0sA=
+github.com/sagernet/sing-shadowsocks v0.0.0-20220717063942-45a2ad9cd41f h1:F6yiuKbBoXgWiuoP7R0YA14pDEl3emxA1mL7M16Q7gc=
+github.com/sagernet/sing-shadowsocks v0.0.0-20220717063942-45a2ad9cd41f/go.mod h1:cDrLwa3zwY8AaW6a4sjipn4xgdIr3CT8TPqSW6iFOi0=
 github.com/sagernet/sing-tun v0.0.0-20220717030718-f53aabff275f h1:o3YN4sFC7lQznAwutagPqBb23hal7MkgVq/VEvd7Vug=
 github.com/sagernet/sing-tun v0.0.0-20220717030718-f53aabff275f/go.mod h1:p7QbUBs2ejf6UQsiHyy1xGAWOk9JWQjZTHy8pOmkWmo=
 github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=

+ 22 - 1
inbound/http.go

@@ -11,6 +11,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/http"
 )
 
@@ -40,5 +41,25 @@ func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogge
 }
 
 func (h *HTTP) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return http.HandleConnection(ctx, conn, std_bufio.NewReader(conn), h.authenticator, h.upstreamHandler(metadata), M.Metadata{})
+	return http.HandleConnection(ctx, conn, std_bufio.NewReader(conn), h.authenticator, h.upstreamUserHandler(metadata), M.Metadata{})
+}
+
+func (a *myInboundAdapter) upstreamUserHandler(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapter {
+	return adapter.NewUpstreamHandler(metadata, a.newUserConnection, a.streamUserPacketConnection, a)
+}
+
+func (a *myInboundAdapter) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	user, loaded := auth.UserFromContext[string](ctx)
+	if !loaded {
+		a.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+		return a.router.RouteConnection(ctx, conn, metadata)
+	}
+	metadata.User = user
+	a.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
+	return a.router.RouteConnection(ctx, conn, metadata)
+}
+
+func (a *myInboundAdapter) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
+	return a.router.RoutePacketConnection(ctx, conn, metadata)
 }

+ 2 - 2
inbound/mixed.go

@@ -52,8 +52,8 @@ func (h *Mixed) NewConnection(ctx context.Context, conn net.Conn, metadata adapt
 	}
 	switch headerType {
 	case socks4.Version, socks5.Version:
-		return socks.HandleConnection0(ctx, conn, headerType, h.authenticator, h.upstreamHandler(metadata), M.Metadata{})
+		return socks.HandleConnection0(ctx, conn, headerType, h.authenticator, h.upstreamUserHandler(metadata), M.Metadata{})
 	}
 	reader := std_bufio.NewReader(bufio.NewCachedReader(conn, buf.As([]byte{headerType})))
-	return http.HandleConnection(ctx, conn, reader, h.authenticator, h.upstreamHandler(metadata), M.Metadata{})
+	return http.HandleConnection(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), M.Metadata{})
 }

+ 13 - 1
inbound/shadowsocks.go

@@ -77,5 +77,17 @@ func (h *Shadowsocks) NewConnection(ctx context.Context, conn net.Conn, metadata
 }
 
 func (h *Shadowsocks) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error {
-	return h.service.NewPacket(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, buffer, adapter.UpstreamMetadata(metadata))
+	return h.service.NewPacket(adapter.WithContext(ctx, &metadata), conn, buffer, adapter.UpstreamMetadata(metadata))
+}
+
+func (h *Shadowsocks) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+	return h.router.RouteConnection(ctx, conn, metadata)
+}
+
+func (h *Shadowsocks) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	ctx = log.ContextWithNewID(ctx)
+	h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
+	h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
+	return h.router.RoutePacketConnection(ctx, conn, metadata)
 }

+ 19 - 8
inbound/shadowsocks_multi.go

@@ -3,14 +3,15 @@ package inbound
 import (
 	"context"
 	"net"
+	"os"
 
 	"github.com/sagernet/sing-box/adapter"
 	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/shadowaead_2022"
 	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/auth"
 	"github.com/sagernet/sing/common/buf"
 	F "github.com/sagernet/sing/common/format"
 	N "github.com/sagernet/sing/common/network"
@@ -72,24 +73,34 @@ func (h *ShadowsocksMulti) NewConnection(ctx context.Context, conn net.Conn, met
 }
 
 func (h *ShadowsocksMulti) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error {
-	return h.service.NewPacket(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, buffer, adapter.UpstreamMetadata(metadata))
+	return h.service.NewPacket(adapter.WithContext(ctx, &metadata), conn, buffer, adapter.UpstreamMetadata(metadata))
 }
 
 func (h *ShadowsocksMulti) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	userCtx := ctx.(*shadowsocks.UserContext[int])
-	user := h.users[userCtx.User].Name
+	userIndex, loaded := auth.UserFromContext[int](ctx)
+	if !loaded {
+		return os.ErrInvalid
+	}
+	user := h.users[userIndex].Name
 	if user == "" {
-		user = F.ToString(userCtx.User)
+		user = F.ToString(userIndex)
+	} else {
+		metadata.User = user
 	}
 	h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
 func (h *ShadowsocksMulti) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	userCtx := ctx.(*shadowsocks.UserContext[int])
-	user := h.users[userCtx.User].Name
+	userIndex, loaded := auth.UserFromContext[int](ctx)
+	if !loaded {
+		return os.ErrInvalid
+	}
+	user := h.users[userIndex].Name
 	if user == "" {
-		user = F.ToString(userCtx.User)
+		user = F.ToString(userIndex)
+	} else {
+		metadata.User = user
 	}
 	ctx = log.ContextWithNewID(ctx)
 	h.logger.InfoContext(ctx, "[", user, "] inbound packet connection from ", metadata.Source)

+ 19 - 8
inbound/shadowsocks_relay.go

@@ -3,14 +3,15 @@ package inbound
 import (
 	"context"
 	"net"
+	"os"
 
 	"github.com/sagernet/sing-box/adapter"
 	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/shadowaead_2022"
 	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/auth"
 	"github.com/sagernet/sing/common/buf"
 	F "github.com/sagernet/sing/common/format"
 	N "github.com/sagernet/sing/common/network"
@@ -72,24 +73,34 @@ func (h *ShadowsocksRelay) NewConnection(ctx context.Context, conn net.Conn, met
 }
 
 func (h *ShadowsocksRelay) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error {
-	return h.service.NewPacket(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, buffer, adapter.UpstreamMetadata(metadata))
+	return h.service.NewPacket(adapter.WithContext(ctx, &metadata), conn, buffer, adapter.UpstreamMetadata(metadata))
 }
 
 func (h *ShadowsocksRelay) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	userCtx := ctx.(*shadowsocks.UserContext[int])
-	destination := h.destinations[userCtx.User].Name
+	destinationIndex, loaded := auth.UserFromContext[int](ctx)
+	if !loaded {
+		return os.ErrInvalid
+	}
+	destination := h.destinations[destinationIndex].Name
 	if destination == "" {
-		destination = F.ToString(userCtx.User)
+		destination = F.ToString(destinationIndex)
+	} else {
+		metadata.User = destination
 	}
 	h.logger.InfoContext(ctx, "[", destination, "] inbound connection to ", metadata.Destination)
 	return h.router.RouteConnection(ctx, conn, metadata)
 }
 
 func (h *ShadowsocksRelay) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	userCtx := ctx.(*shadowsocks.UserContext[int])
-	destination := h.destinations[userCtx.User].Name
+	destinationIndex, loaded := auth.UserFromContext[int](ctx)
+	if !loaded {
+		return os.ErrInvalid
+	}
+	destination := h.destinations[destinationIndex].Name
 	if destination == "" {
-		destination = F.ToString(userCtx.User)
+		destination = F.ToString(destinationIndex)
+	} else {
+		metadata.User = destination
 	}
 	ctx = log.ContextWithNewID(ctx)
 	h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection from ", metadata.Source)

+ 1 - 1
inbound/socks.go

@@ -38,5 +38,5 @@ func NewSocks(ctx context.Context, router adapter.Router, logger log.ContextLogg
 }
 
 func (h *Socks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return socks.HandleConnection(ctx, conn, h.authenticator, h.upstreamHandler(metadata), M.Metadata{})
+	return socks.HandleConnection(ctx, conn, h.authenticator, h.upstreamUserHandler(metadata), M.Metadata{})
 }

+ 1 - 0
mkdocs.yml

@@ -61,6 +61,7 @@ nav:
           - GeoIP: configuration/route/geoip.md
           - Geosite: configuration/route/geosite.md
           - Route Rule: configuration/route/rule.md
+          - Protocol Sniff: configuration/route/sniff.md
   - Examples:
       - examples/index.md
       - Shadowsocks Server: examples/ss-server.md

+ 2 - 0
option/dns.go

@@ -91,6 +91,7 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
 type DefaultDNSRule struct {
 	Inbound       Listable[string] `json:"inbound,omitempty"`
 	Network       string           `json:"network,omitempty"`
+	User          Listable[string] `json:"user,omitempty"`
 	Protocol      Listable[string] `json:"protocol,omitempty"`
 	Domain        Listable[string] `json:"domain,omitempty"`
 	DomainSuffix  Listable[string] `json:"domain_suffix,omitempty"`
@@ -114,6 +115,7 @@ func (r DefaultDNSRule) IsValid() bool {
 func (r DefaultDNSRule) Equals(other DefaultDNSRule) bool {
 	return common.ComparableSliceEquals(r.Inbound, other.Inbound) &&
 		r.Network == other.Network &&
+		common.ComparableSliceEquals(r.User, other.User) &&
 		common.ComparableSliceEquals(r.Protocol, other.Protocol) &&
 		common.ComparableSliceEquals(r.Domain, other.Domain) &&
 		common.ComparableSliceEquals(r.DomainSuffix, other.DomainSuffix) &&

+ 2 - 0
option/route.go

@@ -89,6 +89,7 @@ type DefaultRule struct {
 	Inbound       Listable[string] `json:"inbound,omitempty"`
 	IPVersion     int              `json:"ip_version,omitempty"`
 	Network       string           `json:"network,omitempty"`
+	User          Listable[string] `json:"user,omitempty"`
 	Protocol      Listable[string] `json:"protocol,omitempty"`
 	Domain        Listable[string] `json:"domain,omitempty"`
 	DomainSuffix  Listable[string] `json:"domain_suffix,omitempty"`
@@ -114,6 +115,7 @@ func (r DefaultRule) Equals(other DefaultRule) bool {
 	return common.ComparableSliceEquals(r.Inbound, other.Inbound) &&
 		r.IPVersion == other.IPVersion &&
 		r.Network == other.Network &&
+		common.ComparableSliceEquals(r.User, other.User) &&
 		common.ComparableSliceEquals(r.Protocol, other.Protocol) &&
 		common.ComparableSliceEquals(r.Domain, other.Domain) &&
 		common.ComparableSliceEquals(r.DomainSuffix, other.DomainSuffix) &&

+ 6 - 1
route/rule.go

@@ -82,8 +82,13 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
 			return nil, E.New("invalid network: ", options.Network)
 		}
 	}
+	if len(options.User) > 0 {
+		item := NewUserItem(options.User)
+		rule.items = append(rule.items, item)
+		rule.allItems = append(rule.allItems, item)
+	}
 	if len(options.Protocol) > 0 {
-		item := NewProtocolItem(options.Protocol)
+		item := NewUserItem(options.Protocol)
 		rule.items = append(rule.items, item)
 		rule.allItems = append(rule.allItems, item)
 	}

+ 6 - 1
route/rule_dns.go

@@ -66,8 +66,13 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
 			return nil, E.New("invalid network: ", options.Network)
 		}
 	}
+	if len(options.User) > 0 {
+		item := NewUserItem(options.User)
+		rule.items = append(rule.items, item)
+		rule.allItems = append(rule.allItems, item)
+	}
 	if len(options.Protocol) > 0 {
-		item := NewProtocolItem(options.Protocol)
+		item := NewUserItem(options.Protocol)
 		rule.items = append(rule.items, item)
 		rule.allItems = append(rule.allItems, item)
 	}

+ 37 - 0
route/rule_user.go

@@ -0,0 +1,37 @@
+package route
+
+import (
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	F "github.com/sagernet/sing/common/format"
+)
+
+var _ RuleItem = (*UserItem)(nil)
+
+type UserItem struct {
+	users   []string
+	userMap map[string]bool
+}
+
+func NewUserItem(users []string) *UserItem {
+	userMap := make(map[string]bool)
+	for _, protocol := range users {
+		userMap[protocol] = true
+	}
+	return &UserItem{
+		users:   users,
+		userMap: userMap,
+	}
+}
+
+func (r *UserItem) Match(metadata *adapter.InboundContext) bool {
+	return r.userMap[metadata.User]
+}
+
+func (r *UserItem) String() string {
+	if len(r.users) == 1 {
+		return F.ToString("user=", r.users[0])
+	}
+	return F.ToString("user=[", strings.Join(r.users, " "), "]")
+}

+ 8 - 8
test/box_test.go

@@ -46,15 +46,15 @@ func startInstance(t *testing.T, options option.Options) {
 
 func testSuit(t *testing.T, clientPort uint16, testPort uint16) {
 	dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
-	dialTCP := func() (net.Conn, error) {
-		return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort))
+	dialTCP := func(port uint16) (net.Conn, error) {
+		return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", port))
 	}
-	dialUDP := func() (net.PacketConn, error) {
-		return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
+	dialUDP := func(port uint16) (net.PacketConn, error) {
+		return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", port))
 	}
-	require.NoError(t, testPingPongWithConn(t, testPort, dialTCP))
-	require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP))
-	require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP))
-	require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP))
+	require.NoError(t, testPingPongWithConn(t, dialTCP))
+	require.NoError(t, testLargeDataWithConn(t, dialTCP))
+	require.NoError(t, testPingPongWithPacketConn(t, dialUDP))
+	require.NoError(t, testLargeDataWithPacketConn(t, dialUDP))
 	require.NoError(t, testPacketConnTimeout(t, dialUDP))
 }

+ 15 - 10
test/clash_test.go

@@ -152,14 +152,15 @@ func newLargeDataPair() (chan hashPair, chan hashPair, func(t *testing.T) error)
 	return pingCh, pongCh, test
 }
 
-func testPingPongWithConn(t *testing.T, port uint16, cc func() (net.Conn, error)) error {
+func testPingPongWithConn(t *testing.T, cc func(port uint16) (net.Conn, error)) error {
+	port := mkPort(t)
 	l, err := listen("tcp", ":"+F.ToString(port))
 	if err != nil {
 		return err
 	}
 	defer l.Close()
 
-	c, err := cc()
+	c, err := cc(port)
 	if err != nil {
 		return err
 	}
@@ -198,7 +199,9 @@ func testPingPongWithConn(t *testing.T, port uint16, cc func() (net.Conn, error)
 	return test(t)
 }
 
-func testPingPongWithPacketConn(t *testing.T, port uint16, pcc func() (net.PacketConn, error)) error {
+func testPingPongWithPacketConn(t *testing.T, pcc func(port uint16) (net.PacketConn, error)) error {
+	port := mkPort(t)
+
 	l, err := listenPacket("udp", ":"+F.ToString(port))
 	require.NoError(t, err)
 	defer l.Close()
@@ -219,7 +222,7 @@ func testPingPongWithPacketConn(t *testing.T, port uint16, pcc func() (net.Packe
 		}
 	}()
 
-	pc, err := pcc()
+	pc, err := pcc(port)
 	if err != nil {
 		return err
 	}
@@ -246,7 +249,8 @@ type hashPair struct {
 	recvHash map[int][]byte
 }
 
-func testLargeDataWithConn(t *testing.T, port uint16, cc func() (net.Conn, error)) error {
+func testLargeDataWithConn(t *testing.T, cc func(port uint16) (net.Conn, error)) error {
+	port := mkPort(t)
 	l, err := listen("tcp", ":"+F.ToString(port))
 	require.NoError(t, err)
 	defer l.Close()
@@ -275,7 +279,7 @@ func testLargeDataWithConn(t *testing.T, port uint16, cc func() (net.Conn, error
 		return hashMap, nil
 	}
 
-	c, err := cc()
+	c, err := cc(port)
 	if err != nil {
 		return err
 	}
@@ -343,7 +347,8 @@ func testLargeDataWithConn(t *testing.T, port uint16, cc func() (net.Conn, error
 	return test(t)
 }
 
-func testLargeDataWithPacketConn(t *testing.T, port uint16, pcc func() (net.PacketConn, error)) error {
+func testLargeDataWithPacketConn(t *testing.T, pcc func(port uint16) (net.PacketConn, error)) error {
+	port := mkPort(t)
 	l, err := listenPacket("udp", ":"+F.ToString(port))
 	require.NoError(t, err)
 	defer l.Close()
@@ -409,7 +414,7 @@ func testLargeDataWithPacketConn(t *testing.T, port uint16, pcc func() (net.Pack
 		}
 	}()
 
-	pc, err := pcc()
+	pc, err := pcc(port)
 	if err != nil {
 		return err
 	}
@@ -444,8 +449,8 @@ func testLargeDataWithPacketConn(t *testing.T, port uint16, pcc func() (net.Pack
 	return test(t)
 }
 
-func testPacketConnTimeout(t *testing.T, pcc func() (net.PacketConn, error)) error {
-	pc, err := pcc()
+func testPacketConnTimeout(t *testing.T, pcc func(port uint16) (net.PacketConn, error)) error {
+	pc, err := pcc(mkPort(t))
 	if err != nil {
 		return err
 	}

+ 2 - 2
test/go.mod

@@ -5,7 +5,7 @@ go 1.18
 require (
 	github.com/docker/docker v20.10.17+incompatible
 	github.com/docker/go-connections v0.4.0
-	github.com/sagernet/sing v0.0.0-20220716021830-bd79d31e3b10
+	github.com/sagernet/sing v0.0.0-20220717063925-00f98eb6bc34
 	github.com/sagernet/sing-box v0.0.0
 	github.com/stretchr/testify v1.8.0
 	golang.org/x/net v0.0.0-20220708220712-1185a9018129
@@ -33,7 +33,7 @@ require (
 	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-20220711062726-c64e938e4619 // indirect
-	github.com/sagernet/sing-shadowsocks v0.0.0-20220716012931-952ae62e05d7 // indirect
+	github.com/sagernet/sing-shadowsocks v0.0.0-20220717063942-45a2ad9cd41f // indirect
 	github.com/sagernet/sing-tun v0.0.0-20220717030718-f53aabff275f // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/vishvananda/netlink v1.1.0 // indirect

+ 4 - 4
test/go.sum

@@ -52,12 +52,12 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sagernet/sing v0.0.0-20220716021830-bd79d31e3b10 h1:CQSsVgvVT6KcYNQASP4jnPTg7epSxHGI3MS011LIXkA=
-github.com/sagernet/sing v0.0.0-20220716021830-bd79d31e3b10/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
+github.com/sagernet/sing v0.0.0-20220717063925-00f98eb6bc34 h1:1kFruA2QzuH2R6txJXEDSasfdxzsjNyzC4Z1kZjMkHg=
+github.com/sagernet/sing v0.0.0-20220717063925-00f98eb6bc34/go.mod h1:GbtQfZSpmtD3cXeD1qX2LCMwY8dH+bnnInDTqd92IsM=
 github.com/sagernet/sing-dns v0.0.0-20220711062726-c64e938e4619 h1:oHbOmq1WS0XaZmXp6WpxzyB2xeyRIA1/L8EJKuNntfY=
 github.com/sagernet/sing-dns v0.0.0-20220711062726-c64e938e4619/go.mod h1:y2fpvoxukw3G7eApIZwkcpcG/NE4AB8pCQI0Qd8rMqk=
-github.com/sagernet/sing-shadowsocks v0.0.0-20220716012931-952ae62e05d7 h1:7xQvlMSxNWphQ4t+7fHfR4OnkH23GukLIjImnM1CMLA=
-github.com/sagernet/sing-shadowsocks v0.0.0-20220716012931-952ae62e05d7/go.mod h1:NtHwPOk1wEOPdjjsjtrYoaQuXtlDCrx0mrcWBrNE0sA=
+github.com/sagernet/sing-shadowsocks v0.0.0-20220717063942-45a2ad9cd41f h1:F6yiuKbBoXgWiuoP7R0YA14pDEl3emxA1mL7M16Q7gc=
+github.com/sagernet/sing-shadowsocks v0.0.0-20220717063942-45a2ad9cd41f/go.mod h1:cDrLwa3zwY8AaW6a4sjipn4xgdIr3CT8TPqSW6iFOi0=
 github.com/sagernet/sing-tun v0.0.0-20220717030718-f53aabff275f h1:o3YN4sFC7lQznAwutagPqBb23hal7MkgVq/VEvd7Vug=
 github.com/sagernet/sing-tun v0.0.0-20220717030718-f53aabff275f/go.mod h1:p7QbUBs2ejf6UQsiHyy1xGAWOk9JWQjZTHy8pOmkWmo=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=