diff --git a/adapter/handler.go b/adapter/handler.go index 226f76fb..d4d76c33 100644 --- a/adapter/handler.go +++ b/adapter/handler.go @@ -80,6 +80,20 @@ func (c *MetadataContext) Value(key any) any { return c.Context.Value(key) } +func ContextWithMetadata(ctx context.Context, metadata InboundContext) context.Context { + return &MetadataContext{ + Context: ctx, + Metadata: metadata, + } +} + +func UpstreamMetadata(metadata InboundContext) M.Metadata { + return M.Metadata{ + Source: metadata.Source, + Destination: metadata.Destination, + } +} + type myUpstreamContextHandlerWrapper struct { connectionHandler ConnectionHandlerFunc packetHandler PacketConnectionHandlerFunc @@ -100,14 +114,12 @@ func NewUpstreamContextHandler( func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { myCtx := ctx.Value(myContextType).(*MetadataContext) - ctx = myCtx.Context myCtx.Metadata.Destination = metadata.Destination return w.connectionHandler(ctx, conn, myCtx.Metadata) } func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { myCtx := ctx.Value(myContextType).(*MetadataContext) - ctx = myCtx.Context myCtx.Metadata.Destination = metadata.Destination return w.packetHandler(ctx, conn, myCtx.Metadata) } diff --git a/common/badjson/array.go b/common/badjson/array.go index 516c383c..11cee88a 100644 --- a/common/badjson/array.go +++ b/common/badjson/array.go @@ -41,6 +41,7 @@ func (a *JSONArray[T]) decodeJSON(decoder *json.Decoder) error { if err != nil { return err } + *a = append(*a, item) } return nil } diff --git a/inbound/default.go b/inbound/default.go index f17a5983..4d56aea7 100644 --- a/inbound/default.go +++ b/inbound/default.go @@ -54,7 +54,6 @@ func (a *myInboundAdapter) Tag() string { func (a *myInboundAdapter) Start() error { bindAddr := M.SocksaddrFromAddrPort(netip.Addr(a.listenOptions.Listen), a.listenOptions.Port) - var listenAddr net.Addr if common.Contains(a.network, C.NetworkTCP) { var tcpListener *net.TCPListener var err error @@ -68,7 +67,7 @@ func (a *myInboundAdapter) Start() error { } a.tcpListener = tcpListener go a.loopTCPIn() - listenAddr = tcpListener.Addr() + 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()) @@ -85,11 +84,8 @@ func (a *myInboundAdapter) Start() error { go a.loopUDPInThreadSafe() } go a.loopUDPOut() - if listenAddr == nil { - listenAddr = udpConn.LocalAddr() - } + a.logger.Info("udp server started at ", udpConn.LocalAddr()) } - a.logger.Info("server started at ", listenAddr) return nil } @@ -229,7 +225,7 @@ func (a *myInboundAdapter) NewError(ctx context.Context, err error) { a.logger.WithContext(ctx).Debug("connection closed") return } - a.logger.Error(err) + a.logger.WithContext(ctx).Error(err) } func (a *myInboundAdapter) writePacket(buffer *buf.Buffer, destination M.Socksaddr) error { diff --git a/inbound/direct.go b/inbound/direct.go index 082a4918..ddfe7ac3 100644 --- a/inbound/direct.go +++ b/inbound/direct.go @@ -78,9 +78,6 @@ func (d *Direct) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.B case 3: metadata.Destination.Port = d.overrideDestination.Port } - var upstreamMetadata M.Metadata - upstreamMetadata.Source = metadata.Source - upstreamMetadata.Destination = metadata.Destination - d.udpNat.NewPacketDirect(&adapter.MetadataContext{Context: log.ContextWithID(ctx), Metadata: metadata}, metadata.Source.AddrPort(), conn, buffer, upstreamMetadata) + d.udpNat.NewPacketDirect(adapter.ContextWithMetadata(log.ContextWithID(ctx), metadata), metadata.Source.AddrPort(), conn, buffer, adapter.UpstreamMetadata(metadata)) return nil } diff --git a/inbound/shadowsocks.go b/inbound/shadowsocks.go index 77f8e54e..549b93fc 100644 --- a/inbound/shadowsocks.go +++ b/inbound/shadowsocks.go @@ -14,10 +14,22 @@ import ( "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" ) +func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { + if len(options.Users) > 0 && len(options.Destinations) > 0 { + return nil, E.New("users and destinations options must not be combined") + } + if len(options.Users) > 0 { + return newShadowsocksMulti(ctx, router, logger, tag, options) + } else if len(options.Destinations) > 0 { + return newShadowsocksRelay(ctx, router, logger, tag, options) + } else { + return newShadowsocks(ctx, router, logger, tag, options) + } +} + var _ adapter.Inbound = (*Shadowsocks)(nil) type Shadowsocks struct { @@ -25,7 +37,7 @@ type Shadowsocks struct { service shadowsocks.Service } -func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.ShadowsocksInboundOptions) (*Shadowsocks, error) { +func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.ShadowsocksInboundOptions) (*Shadowsocks, error) { inbound := &Shadowsocks{ myInboundAdapter: myInboundAdapter{ protocol: C.TypeShadowsocks, @@ -61,14 +73,9 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Logge } func (h *Shadowsocks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return h.service.NewConnection(&adapter.MetadataContext{Context: ctx, Metadata: metadata}, conn, M.Metadata{ - Source: metadata.Source, - }) + return h.service.NewConnection(adapter.ContextWithMetadata(log.ContextWithID(ctx), metadata), conn, adapter.UpstreamMetadata(metadata)) } func (h *Shadowsocks) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { - h.logger.Trace("inbound packet from ", metadata.Source) - return h.service.NewPacket(&adapter.MetadataContext{Context: ctx, Metadata: metadata}, conn, buffer, M.Metadata{ - Source: metadata.Source, - }) + return h.service.NewPacket(adapter.ContextWithMetadata(log.ContextWithID(ctx), metadata), conn, buffer, adapter.UpstreamMetadata(metadata)) } diff --git a/inbound/shadowsocks_multi.go b/inbound/shadowsocks_multi.go new file mode 100644 index 00000000..b2fa7e60 --- /dev/null +++ b/inbound/shadowsocks_multi.go @@ -0,0 +1,98 @@ +package inbound + +import ( + "context" + "net" + + "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/buf" + F "github.com/sagernet/sing/common/format" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.Inbound = (*ShadowsocksMulti)(nil) + +type ShadowsocksMulti struct { + myInboundAdapter + service *shadowaead_2022.MultiService[int] + users []option.ShadowsocksUser +} + +func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.ShadowsocksInboundOptions) (*ShadowsocksMulti, error) { + inbound := &ShadowsocksMulti{ + myInboundAdapter: myInboundAdapter{ + protocol: C.TypeShadowsocks, + network: options.Network.Build(), + ctx: ctx, + router: router, + logger: logger, + tag: tag, + listenOptions: options.ListenOptions, + }, + users: options.Users, + } + inbound.connHandler = inbound + inbound.packetHandler = inbound + var udpTimeout int64 + if options.UDPTimeout != 0 { + udpTimeout = options.UDPTimeout + } else { + udpTimeout = 300 + } + service, err := shadowaead_2022.NewMultiServiceWithPassword[int]( + options.Method, + options.Password, + udpTimeout, + adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound), + ) + if err != nil { + return nil, err + } + err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int { + return index + }), common.Map(options.Users, func(user option.ShadowsocksUser) string { + return user.Password + })) + if err != nil { + return nil, err + } + inbound.service = service + inbound.packetUpstream = service + return inbound, err +} + +func (h *ShadowsocksMulti) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + return h.service.NewConnection(adapter.ContextWithMetadata(log.ContextWithID(ctx), metadata), conn, adapter.UpstreamMetadata(metadata)) +} + +func (h *ShadowsocksMulti) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { + return h.service.NewPacket(adapter.ContextWithMetadata(log.ContextWithID(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 + if user == "" { + user = F.ToString(userCtx.User) + } + h.logger.WithContext(ctx).Info("[", 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 + if user == "" { + user = F.ToString(userCtx.User) + } + ctx = log.ContextWithID(ctx) + h.logger.WithContext(ctx).Info("[", user, "] inbound packet connection from ", metadata.Source) + h.logger.WithContext(ctx).Info("[", user, "] inbound packet connection to ", metadata.Destination) + return h.router.RoutePacketConnection(ctx, conn, metadata) +} diff --git a/inbound/shadowsocks_relay.go b/inbound/shadowsocks_relay.go new file mode 100644 index 00000000..f673a149 --- /dev/null +++ b/inbound/shadowsocks_relay.go @@ -0,0 +1,98 @@ +package inbound + +import ( + "context" + "net" + + "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/buf" + F "github.com/sagernet/sing/common/format" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.Inbound = (*ShadowsocksMulti)(nil) + +type ShadowsocksRelay struct { + myInboundAdapter + service *shadowaead_2022.RelayService[int] + destinations []option.ShadowsocksDestination +} + +func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.ShadowsocksInboundOptions) (*ShadowsocksRelay, error) { + inbound := &ShadowsocksRelay{ + myInboundAdapter: myInboundAdapter{ + protocol: C.TypeShadowsocks, + network: options.Network.Build(), + ctx: ctx, + router: router, + logger: logger, + tag: tag, + listenOptions: options.ListenOptions, + }, + destinations: options.Destinations, + } + inbound.connHandler = inbound + inbound.packetHandler = inbound + var udpTimeout int64 + if options.UDPTimeout != 0 { + udpTimeout = options.UDPTimeout + } else { + udpTimeout = 300 + } + service, err := shadowaead_2022.NewRelayServiceWithPassword[int]( + options.Method, + options.Password, + udpTimeout, + adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound), + ) + if err != nil { + return nil, err + } + err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Destinations, func(index int, user option.ShadowsocksDestination) int { + return index + }), common.Map(options.Destinations, func(user option.ShadowsocksDestination) string { + return user.Password + }), common.Map(options.Destinations, option.ShadowsocksDestination.Build)) + if err != nil { + return nil, err + } + inbound.service = service + inbound.packetUpstream = service + return inbound, err +} + +func (h *ShadowsocksRelay) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + return h.service.NewConnection(adapter.ContextWithMetadata(log.ContextWithID(ctx), metadata), conn, adapter.UpstreamMetadata(metadata)) +} + +func (h *ShadowsocksRelay) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { + return h.service.NewPacket(adapter.ContextWithMetadata(log.ContextWithID(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 + if destination == "" { + destination = F.ToString(userCtx.User) + } + h.logger.WithContext(ctx).Info("[", 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 + if destination == "" { + destination = F.ToString(userCtx.User) + } + ctx = log.ContextWithID(ctx) + h.logger.WithContext(ctx).Info("[", destination, "] inbound packet connection from ", metadata.Source) + h.logger.WithContext(ctx).Info("[", destination, "] inbound packet connection to ", metadata.Destination) + return h.router.RoutePacketConnection(ctx, conn, metadata) +} diff --git a/option/inbound.go b/option/inbound.go index f897355b..79ef23e4 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -27,7 +27,7 @@ func (h Inbound) Equals(other Inbound) bool { h.SocksOptions.Equals(other.SocksOptions) && h.HTTPOptions.Equals(other.HTTPOptions) && h.MixedOptions.Equals(other.MixedOptions) && - h.ShadowsocksOptions == other.ShadowsocksOptions + h.ShadowsocksOptions.Equals(other.ShadowsocksOptions) } func (h Inbound) MarshalJSON() ([]byte, error) { @@ -102,7 +102,29 @@ type DirectInboundOptions struct { type ShadowsocksInboundOptions struct { ListenOptions - Network NetworkList `json:"network,omitempty"` - Method string `json:"method"` - Password string `json:"password"` + Network NetworkList `json:"network,omitempty"` + Method string `json:"method"` + Password string `json:"password"` + Users []ShadowsocksUser `json:"users,omitempty"` + Destinations []ShadowsocksDestination `json:"destinations,omitempty"` +} + +func (o ShadowsocksInboundOptions) Equals(other ShadowsocksInboundOptions) bool { + return o.ListenOptions == other.ListenOptions && + o.Network == other.Network && + o.Method == other.Method && + o.Password == other.Password && + common.ComparableSliceEquals(o.Users, other.Users) && + common.ComparableSliceEquals(o.Destinations, other.Destinations) +} + +type ShadowsocksUser struct { + Name string `json:"name"` + Password string `json:"password"` +} + +type ShadowsocksDestination struct { + Name string `json:"name"` + Password string `json:"password"` + ServerOptions } diff --git a/option/outbound.go b/option/outbound.go index 8ffe7b41..91ccaf7d 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -8,8 +8,8 @@ import ( ) type _Outbound struct { + Type string `json:"type"` Tag string `json:"tag,omitempty"` - Type string `json:"type,omitempty"` DirectOptions DirectOutboundOptions `json:"-"` SocksOptions SocksOutboundOptions `json:"-"` HTTPOptions HTTPOutboundOptions `json:"-"`