瀏覽代碼

Inbound rule support

世界 3 年之前
父節點
當前提交
7c57eb70e8

+ 3 - 5
adapter/inbound.go

@@ -1,8 +1,6 @@
 package adapter
 
 import (
-	"net/netip"
-
 	M "github.com/sagernet/sing/common/metadata"
 )
 
@@ -13,10 +11,10 @@ type Inbound interface {
 }
 
 type InboundContext struct {
-	Source      netip.AddrPort
-	Destination M.Socksaddr
 	Inbound     string
 	Network     string
-	Protocol    string
+	Source      M.Socksaddr
+	Destination M.Socksaddr
 	Domain      string
+	Protocol    string
 }

+ 35 - 0
adapter/inbound/builder.go

@@ -0,0 +1,35 @@
+package inbound
+
+import (
+	"context"
+
+	"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"
+	F "github.com/sagernet/sing/common/format"
+)
+
+func New(ctx context.Context, router adapter.Router, logger log.Logger, index int, options option.Inbound) (adapter.Inbound, error) {
+	var tag string
+	if options.Tag != "" {
+		tag = options.Tag
+	} else {
+		tag = F.ToString(index)
+	}
+	inboundLogger := logger.WithPrefix(F.ToString("inbound/", options.Type, "[", tag, "]: "))
+	switch options.Type {
+	case C.TypeDirect:
+		return NewDirect(ctx, router, inboundLogger, options.Tag, options.DirectOptions), nil
+	case C.TypeSocks:
+		return NewSocks(ctx, router, inboundLogger, options.Tag, options.SocksOptions), nil
+	case C.TypeHTTP:
+		return NewHTTP(ctx, router, inboundLogger, options.Tag, options.HTTPOptions), nil
+	case C.TypeMixed:
+		return NewMixed(ctx, router, inboundLogger, options.Tag, options.MixedOptions), nil
+	case C.TypeShadowsocks:
+		return NewShadowsocks(ctx, router, inboundLogger, options.Tag, options.ShadowsocksOptions)
+	default:
+		panic(F.ToString("unknown inbound type: ", options.Type))
+	}
+}

+ 27 - 20
adapter/inbound/default.go

@@ -10,9 +10,9 @@ import (
 
 	"github.com/database64128/tfo-go"
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/config"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/buf"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -29,7 +29,7 @@ type myInboundAdapter struct {
 	router         adapter.Router
 	logger         log.Logger
 	tag            string
-	listenOptions  config.ListenOptions
+	listenOptions  option.ListenOptions
 	connHandler    adapter.ConnectionHandler
 	packetHandler  adapter.PacketHandler
 	packetUpstream any
@@ -101,7 +101,7 @@ func (a *myInboundAdapter) Close() error {
 }
 
 func (a *myInboundAdapter) upstreamHandler(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapter {
-	return adapter.NewUpstreamHandler(metadata, a.newConnection, a.newPacketConnection, a)
+	return adapter.NewUpstreamHandler(metadata, a.newConnection, a.streamPacketConnection, a)
 }
 
 func (a *myInboundAdapter) upstreamContextHandler() adapter.UpstreamHandlerAdapter {
@@ -113,7 +113,14 @@ func (a *myInboundAdapter) newConnection(ctx context.Context, conn net.Conn, met
 	return a.router.RouteConnection(ctx, conn, metadata)
 }
 
+func (a *myInboundAdapter) streamPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	a.logger.WithContext(ctx).Info("inbound packet connection to ", metadata.Destination)
+	return a.router.RoutePacketConnection(ctx, conn, metadata)
+}
+
 func (a *myInboundAdapter) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	ctx = log.ContextWithID(ctx)
+	a.logger.WithContext(ctx).Info("inbound packet connection from ", metadata.Source)
 	a.logger.WithContext(ctx).Info("inbound packet connection to ", metadata.Destination)
 	return a.router.RoutePacketConnection(ctx, conn, metadata)
 }
@@ -125,16 +132,16 @@ func (a *myInboundAdapter) loopTCPIn() {
 		if err != nil {
 			return
 		}
-		var metadata adapter.InboundContext
-		metadata.Inbound = a.tag
-		metadata.Source = M.AddrPortFromNet(conn.RemoteAddr())
 		go func() {
-			metadata.Network = "tcp"
 			ctx := log.ContextWithID(a.ctx)
-			a.logger.WithContext(ctx).Info("inbound connection from ", conn.RemoteAddr())
+			var metadata adapter.InboundContext
+			metadata.Inbound = a.tag
+			metadata.Network = "tcp"
+			metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr())
+			a.logger.WithContext(ctx).Info("inbound connection from ", metadata.Source)
 			hErr := a.connHandler.NewConnection(ctx, conn, metadata)
 			if hErr != nil {
-				a.NewError(ctx, E.Cause(hErr, "process connection from ", conn.RemoteAddr()))
+				a.NewError(ctx, E.Cause(hErr, "process connection from ", metadata.Source))
 			}
 		}()
 	}
@@ -149,9 +156,6 @@ func (a *myInboundAdapter) loopUDPIn() {
 	buffer.IncRef()
 	defer buffer.DecRef()
 	packetService := (*myInboundPacketAdapter)(a)
-	var metadata adapter.InboundContext
-	metadata.Inbound = a.tag
-	metadata.Network = "udp"
 	for {
 		buffer.Reset()
 		n, addr, err := a.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
@@ -159,10 +163,13 @@ func (a *myInboundAdapter) loopUDPIn() {
 			return
 		}
 		buffer.Truncate(n)
-		metadata.Source = addr
+		var metadata adapter.InboundContext
+		metadata.Inbound = a.tag
+		metadata.Network = "udp"
+		metadata.Source = M.SocksaddrFromNetIP(addr)
 		err = a.packetHandler.NewPacket(a.ctx, packetService, buffer, metadata)
 		if err != nil {
-			a.newError(E.Cause(err, "process packet from ", addr))
+			a.newError(E.Cause(err, "process packet from ", metadata.Source))
 		}
 	}
 }
@@ -170,9 +177,6 @@ func (a *myInboundAdapter) loopUDPIn() {
 func (a *myInboundAdapter) loopUDPInThreadSafe() {
 	defer close(a.packetOutboundClosed)
 	packetService := (*myInboundPacketAdapter)(a)
-	var metadata adapter.InboundContext
-	metadata.Inbound = a.tag
-	metadata.Network = "udp"
 	for {
 		buffer := buf.NewPacket()
 		n, addr, err := a.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
@@ -180,11 +184,14 @@ func (a *myInboundAdapter) loopUDPInThreadSafe() {
 			return
 		}
 		buffer.Truncate(n)
-		metadata.Source = addr
+		var metadata adapter.InboundContext
+		metadata.Inbound = a.tag
+		metadata.Network = "udp"
+		metadata.Source = M.SocksaddrFromNetIP(addr)
 		err = a.packetHandler.NewPacket(a.ctx, packetService, buffer, metadata)
 		if err != nil {
 			buffer.Release()
-			a.newError(E.Cause(err, "process packet from ", addr))
+			a.newError(E.Cause(err, "process packet from ", metadata.Source))
 		}
 	}
 }
@@ -212,7 +219,7 @@ func (a *myInboundAdapter) loopUDPOut() {
 }
 
 func (a *myInboundAdapter) newError(err error) {
-	a.logger.Warn(err)
+	a.logger.Error(err)
 }
 
 func (a *myInboundAdapter) NewError(ctx context.Context, err error) {

+ 10 - 10
adapter/inbound/direct.go

@@ -6,9 +6,9 @@ import (
 	"net/netip"
 
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/config"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common/buf"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
@@ -24,7 +24,7 @@ type Direct struct {
 	overrideDestination M.Socksaddr
 }
 
-func NewDirect(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *config.DirectInboundOptions) *Direct {
+func NewDirect(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *option.DirectInboundOptions) *Direct {
 	inbound := &Direct{
 		myInboundAdapter: myInboundAdapter{
 			protocol:      C.TypeDirect,
@@ -54,13 +54,13 @@ func NewDirect(ctx context.Context, router adapter.Router, logger log.Logger, ta
 
 func (d *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	switch d.overrideOption {
-	case 0:
-		metadata.Destination = d.overrideDestination
 	case 1:
+		metadata.Destination = d.overrideDestination
+	case 2:
 		destination := d.overrideDestination
 		destination.Port = metadata.Destination.Port
 		metadata.Destination = destination
-	case 2:
+	case 3:
 		metadata.Destination.Port = d.overrideDestination.Port
 	}
 	d.logger.WithContext(ctx).Info("inbound connection to ", metadata.Destination)
@@ -69,18 +69,18 @@ func (d *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adap
 
 func (d *Direct) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error {
 	switch d.overrideOption {
-	case 0:
-		metadata.Destination = d.overrideDestination
 	case 1:
+		metadata.Destination = d.overrideDestination
+	case 2:
 		destination := d.overrideDestination
 		destination.Port = metadata.Destination.Port
 		metadata.Destination = destination
-	case 2:
+	case 3:
 		metadata.Destination.Port = d.overrideDestination.Port
 	}
 	var upstreamMetadata M.Metadata
-	upstreamMetadata.Source = M.SocksaddrFromNetIP(metadata.Source)
+	upstreamMetadata.Source = metadata.Source
 	upstreamMetadata.Destination = metadata.Destination
-	d.udpNat.NewPacketDirect(&adapter.MetadataContext{Context: ctx, Metadata: metadata}, metadata.Source, conn, buffer, upstreamMetadata)
+	d.udpNat.NewPacketDirect(&adapter.MetadataContext{Context: log.ContextWithID(ctx), Metadata: metadata}, metadata.Source.AddrPort(), conn, buffer, upstreamMetadata)
 	return nil
 }

+ 2 - 2
adapter/inbound/http.go

@@ -6,9 +6,9 @@ import (
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/config"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common/auth"
 	M "github.com/sagernet/sing/common/metadata"
 	"github.com/sagernet/sing/protocol/http"
@@ -21,7 +21,7 @@ type HTTP struct {
 	authenticator auth.Authenticator
 }
 
-func NewHTTP(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *config.SimpleInboundOptions) *HTTP {
+func NewHTTP(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *option.SimpleInboundOptions) *HTTP {
 	inbound := &HTTP{
 		myInboundAdapter{
 			protocol:      C.TypeHTTP,

+ 2 - 2
adapter/inbound/mixed.go

@@ -6,9 +6,9 @@ import (
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/config"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common/auth"
 	"github.com/sagernet/sing/common/buf"
 	"github.com/sagernet/sing/common/bufio"
@@ -27,7 +27,7 @@ type Mixed struct {
 	authenticator auth.Authenticator
 }
 
-func NewMixed(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *config.SimpleInboundOptions) *Mixed {
+func NewMixed(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *option.SimpleInboundOptions) *Mixed {
 	inbound := &Mixed{
 		myInboundAdapter{
 			protocol:      C.TypeMixed,

+ 6 - 5
adapter/inbound/shadowsocks.go

@@ -5,9 +5,9 @@ import (
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/config"
 	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"
 	"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
@@ -25,7 +25,7 @@ type Shadowsocks struct {
 	service shadowsocks.Service
 }
 
-func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *config.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,
@@ -39,7 +39,6 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Logge
 	}
 	inbound.connHandler = inbound
 	inbound.packetHandler = inbound
-	inbound.packetUpstream = true
 	var udpTimeout int64
 	if options.UDPTimeout != 0 {
 		udpTimeout = options.UDPTimeout
@@ -57,17 +56,19 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Logge
 	default:
 		err = E.New("shadowsocks: unsupported method: ", options.Method)
 	}
+	inbound.packetUpstream = inbound.service
 	return inbound, err
 }
 
 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: M.SocksaddrFromNetIP(metadata.Source),
+		Source: metadata.Source,
 	})
 }
 
 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: M.SocksaddrFromNetIP(metadata.Source),
+		Source: metadata.Source,
 	})
 }

+ 2 - 2
adapter/inbound/socks.go

@@ -5,9 +5,9 @@ import (
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/config"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common/auth"
 	M "github.com/sagernet/sing/common/metadata"
 	"github.com/sagernet/sing/protocol/socks"
@@ -20,7 +20,7 @@ type Socks struct {
 	authenticator auth.Authenticator
 }
 
-func NewSocks(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *config.SimpleInboundOptions) *Socks {
+func NewSocks(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *option.SimpleInboundOptions) *Socks {
 	inbound := &Socks{
 		myInboundAdapter{
 			protocol:      C.TypeSocks,

+ 27 - 0
adapter/outbound/builder.go

@@ -0,0 +1,27 @@
+package outbound
+
+import (
+	"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"
+	F "github.com/sagernet/sing/common/format"
+)
+
+func New(router adapter.Router, logger log.Logger, index int, options option.Outbound) (adapter.Outbound, error) {
+	var tag string
+	if options.Tag != "" {
+		tag = options.Tag
+	} else {
+		tag = F.ToString(index)
+	}
+	outboundLogger := logger.WithPrefix(F.ToString("outbound/", options.Type, "[", tag, "]: "))
+	switch options.Type {
+	case C.TypeDirect:
+		return NewDirect(router, outboundLogger, options.Tag, options.DirectOptions), nil
+	case C.TypeShadowsocks:
+		return NewShadowsocks(router, outboundLogger, options.Tag, options.ShadowsocksOptions)
+	default:
+		panic(F.ToString("unknown outbound type: ", options.Type))
+	}
+}

+ 4 - 4
adapter/outbound/default.go

@@ -9,8 +9,8 @@ import (
 
 	"github.com/database64128/tfo-go"
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/config"
 	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/buf"
 	"github.com/sagernet/sing/common/bufio"
@@ -48,7 +48,7 @@ func (d *defaultDialer) ListenPacket(ctx context.Context) (net.PacketConn, error
 	return d.ListenConfig.ListenPacket(ctx, "udp", "")
 }
 
-func newDialer(options config.DialerOptions) N.Dialer {
+func newDialer(options option.DialerOptions) N.Dialer {
 	var dialer net.Dialer
 	var listener net.ListenConfig
 	if options.BindInterface != "" {
@@ -70,13 +70,13 @@ func newDialer(options config.DialerOptions) N.Dialer {
 
 type lazyDialer struct {
 	router   adapter.Router
-	options  config.DialerOptions
+	options  option.DialerOptions
 	dialer   N.Dialer
 	initOnce sync.Once
 	initErr  error
 }
 
-func NewDialer(router adapter.Router, options config.DialerOptions) N.Dialer {
+func NewDialer(router adapter.Router, options option.DialerOptions) N.Dialer {
 	if options.Detour == "" {
 		return newDialer(options)
 	}

+ 8 - 8
adapter/outbound/direct.go

@@ -5,9 +5,9 @@ import (
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/config"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common/bufio"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
@@ -21,7 +21,7 @@ type Direct struct {
 	overrideDestination M.Socksaddr
 }
 
-func NewDirect(router adapter.Router, logger log.Logger, tag string, options *config.DirectOutboundOptions) *Direct {
+func NewDirect(router adapter.Router, logger log.Logger, tag string, options *option.DirectOutboundOptions) *Direct {
 	outbound := &Direct{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol: C.TypeDirect,
@@ -45,26 +45,26 @@ func NewDirect(router adapter.Router, logger log.Logger, tag string, options *co
 
 func (d *Direct) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	switch d.overrideOption {
-	case 0:
-		destination = d.overrideDestination
 	case 1:
+		destination = d.overrideDestination
+	case 2:
 		newDestination := d.overrideDestination
 		newDestination.Port = destination.Port
 		destination = newDestination
-	case 2:
+	case 3:
 		destination.Port = d.overrideDestination.Port
 	}
 	switch network {
 	case C.NetworkTCP:
-		d.logger.WithContext(ctx).Debug("outbound connection to ", destination)
+		d.logger.WithContext(ctx).Info("outbound connection to ", destination)
 	case C.NetworkUDP:
-		d.logger.WithContext(ctx).Debug("outbound packet connection to ", destination)
+		d.logger.WithContext(ctx).Info("outbound packet connection to ", destination)
 	}
 	return d.dialer.DialContext(ctx, network, destination)
 }
 
 func (d *Direct) ListenPacket(ctx context.Context) (net.PacketConn, error) {
-	d.logger.WithContext(ctx).Debug("outbound packet connection")
+	d.logger.WithContext(ctx).Info("outbound packet connection")
 	return d.dialer.ListenPacket(ctx)
 }
 

+ 5 - 5
adapter/outbound/shadowsocks.go

@@ -5,9 +5,9 @@ import (
 	"net"
 
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/config"
 	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/bufio"
@@ -24,7 +24,7 @@ type Shadowsocks struct {
 	serverAddr M.Socksaddr
 }
 
-func NewShadowsocks(router adapter.Router, logger log.Logger, tag string, options *config.ShadowsocksOutboundOptions) (*Shadowsocks, error) {
+func NewShadowsocks(router adapter.Router, logger log.Logger, tag string, options *option.ShadowsocksOutboundOptions) (*Shadowsocks, error) {
 	outbound := &Shadowsocks{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol: C.TypeDirect,
@@ -66,14 +66,14 @@ func (o *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn
 func (o *Shadowsocks) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	switch network {
 	case C.NetworkTCP:
-		o.logger.WithContext(ctx).Debug("outbound connection to ", destination)
+		o.logger.WithContext(ctx).Info("outbound connection to ", destination)
 		outConn, err := o.dialer.DialContext(ctx, "tcp", o.serverAddr)
 		if err != nil {
 			return nil, err
 		}
 		return o.method.DialEarlyConn(outConn, destination), nil
 	case C.NetworkUDP:
-		o.logger.WithContext(ctx).Debug("outbound packet connection to ", destination)
+		o.logger.WithContext(ctx).Info("outbound packet connection to ", destination)
 		outConn, err := o.dialer.DialContext(ctx, "udp", o.serverAddr)
 		if err != nil {
 			return nil, err
@@ -85,7 +85,7 @@ func (o *Shadowsocks) DialContext(ctx context.Context, network string, destinati
 }
 
 func (o *Shadowsocks) ListenPacket(ctx context.Context) (net.PacketConn, error) {
-	o.logger.WithContext(ctx).Debug("outbound packet connection to ", o.serverAddr)
+	o.logger.WithContext(ctx).Info("outbound packet connection to ", o.serverAddr)
 	outConn, err := o.dialer.ListenPacket(ctx)
 	if err != nil {
 		return nil, err

+ 59 - 14
adapter/route/router.go

@@ -4,8 +4,12 @@ import (
 	"context"
 	"net"
 
+	"github.com/oschwald/geoip2-golang"
 	"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/common"
 	N "github.com/sagernet/sing/common/network"
 )
 
@@ -15,24 +19,18 @@ type Router struct {
 	logger          log.Logger
 	defaultOutbound adapter.Outbound
 	outboundByTag   map[string]adapter.Outbound
+
+	rules     []adapter.Rule
+	geoReader *geoip2.Reader
 }
 
 func NewRouter(logger log.Logger) *Router {
 	return &Router{
-		logger:        logger,
+		logger:        logger.WithPrefix("router: "),
 		outboundByTag: make(map[string]adapter.Outbound),
 	}
 }
 
-func (r *Router) AddOutbound(outbound adapter.Outbound) {
-	if outbound.Tag() != "" {
-		r.outboundByTag[outbound.Tag()] = outbound
-	}
-	if r.defaultOutbound == nil {
-		r.defaultOutbound = outbound
-	}
-}
-
 func (r *Router) DefaultOutbound() adapter.Outbound {
 	if r.defaultOutbound == nil {
 		panic("missing default outbound")
@@ -46,13 +44,60 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
 }
 
 func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	r.logger.WithContext(ctx).Debug("no match")
-	r.logger.WithContext(ctx).Debug("route connection to default outbound")
+	for _, rule := range r.rules {
+		if rule.Match(metadata) {
+			r.logger.WithContext(ctx).Info("match ", rule.String())
+			if outbound, loaded := r.Outbound(rule.Outbound()); loaded {
+				return outbound.NewConnection(ctx, conn, metadata.Destination)
+			}
+			r.logger.WithContext(ctx).Error("outbound ", rule.Outbound(), " not found")
+		}
+	}
+	r.logger.WithContext(ctx).Info("no match => ", r.defaultOutbound.Tag())
 	return r.defaultOutbound.NewConnection(ctx, conn, metadata.Destination)
 }
 
 func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	r.logger.WithContext(ctx).Debug("no match")
-	r.logger.WithContext(ctx).Debug("route packet connection to default outbound")
+	for _, rule := range r.rules {
+		if rule.Match(metadata) {
+			r.logger.WithContext(ctx).Info("match ", rule.String())
+			if outbound, loaded := r.Outbound(rule.Outbound()); loaded {
+				return outbound.NewPacketConnection(ctx, conn, metadata.Destination)
+			}
+			r.logger.WithContext(ctx).Error("outbound ", rule.Outbound(), " not found")
+		}
+	}
+	r.logger.WithContext(ctx).Info("no match => ", r.defaultOutbound.Tag())
 	return r.defaultOutbound.NewPacketConnection(ctx, conn, metadata.Destination)
 }
+
+func (r *Router) Close() error {
+	return common.Close(
+		common.PtrOrNil(r.geoReader),
+	)
+}
+
+func (r *Router) UpdateOutbounds(outbounds []adapter.Outbound) {
+	var defaultOutbound adapter.Outbound
+	outboundByTag := make(map[string]adapter.Outbound)
+	if len(outbounds) > 0 {
+		defaultOutbound = outbounds[0]
+	}
+	for _, outbound := range outbounds {
+		outboundByTag[outbound.Tag()] = outbound
+	}
+	r.defaultOutbound = defaultOutbound
+	r.outboundByTag = outboundByTag
+}
+
+func (r *Router) UpdateRules(options []option.Rule) error {
+	rules := make([]adapter.Rule, 0, len(options))
+	for i, rule := range options {
+		switch rule.Type {
+		case "", C.RuleTypeDefault:
+			rules = append(rules, NewDefaultRule(i, rule.DefaultOptions))
+		}
+	}
+	r.rules = rules
+	return nil
+}

+ 55 - 0
adapter/route/rule.go

@@ -0,0 +1,55 @@
+package route
+
+import (
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/option"
+	F "github.com/sagernet/sing/common/format"
+)
+
+var _ adapter.Rule = (*DefaultRule)(nil)
+
+type DefaultRule struct {
+	index    int
+	outbound string
+	items    []RuleItem
+}
+
+type RuleItem interface {
+	Match(metadata adapter.InboundContext) bool
+	String() string
+}
+
+func NewDefaultRule(index int, options option.DefaultRule) *DefaultRule {
+	rule := &DefaultRule{
+		index:    index,
+		outbound: options.Outbound,
+	}
+	if len(options.Inbound) > 0 {
+		rule.items = append(rule.items, NewInboundRule(options.Inbound))
+	}
+	return rule
+}
+
+func (r *DefaultRule) Match(metadata adapter.InboundContext) bool {
+	for _, item := range r.items {
+		if item.Match(metadata) {
+			return true
+		}
+	}
+	return false
+}
+
+func (r *DefaultRule) Outbound() string {
+	return r.outbound
+}
+
+func (r *DefaultRule) String() string {
+	var description string
+	description = F.ToString("[", r.index, "]")
+	for _, item := range r.items {
+		description += " "
+		description += item.String()
+	}
+	description += " => " + r.outbound
+	return description
+}

+ 35 - 0
adapter/route/rule_inbound.go

@@ -0,0 +1,35 @@
+package route
+
+import (
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	F "github.com/sagernet/sing/common/format"
+)
+
+var _ RuleItem = (*InboundRule)(nil)
+
+type InboundRule struct {
+	inbounds   []string
+	inboundMap map[string]bool
+}
+
+func NewInboundRule(inbounds []string) RuleItem {
+	rule := &InboundRule{inbounds, make(map[string]bool)}
+	for _, inbound := range inbounds {
+		rule.inboundMap[inbound] = true
+	}
+	return rule
+}
+
+func (r *InboundRule) Match(metadata adapter.InboundContext) bool {
+	return r.inboundMap[metadata.Inbound]
+}
+
+func (r *InboundRule) String() string {
+	if len(r.inbounds) == 1 {
+		return F.ToString("inbound=", r.inbounds[0])
+	} else {
+		return F.ToString("inbound=[", strings.Join(r.inbounds, " "), "]")
+	}
+}

+ 7 - 0
adapter/router.go

@@ -12,4 +12,11 @@ type Router interface {
 	Outbound(tag string) (Outbound, bool)
 	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
 	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
+	Close() error
+}
+
+type Rule interface {
+	Match(metadata InboundContext) bool
+	Outbound() string
+	String() string
 }

+ 5 - 4
cmd/sing-box/main.go

@@ -8,7 +8,7 @@ import (
 	"syscall"
 
 	"github.com/sagernet/sing-box"
-	"github.com/sagernet/sing-box/config"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 )
@@ -36,13 +36,14 @@ func run(cmd *cobra.Command, args []string) {
 	if err != nil {
 		logrus.Fatal("read config: ", err)
 	}
-	var boxConfig config.Config
-	err = json.Unmarshal(configContent, &boxConfig)
+	var options option.Options
+	err = json.Unmarshal(configContent, &options)
 	if err != nil {
 		logrus.Fatal("parse config: ", err)
 	}
+
 	ctx, cancel := context.WithCancel(context.Background())
-	service, err := box.NewService(ctx, &boxConfig)
+	service, err := box.NewService(ctx, &options)
 	if err != nil {
 		logrus.Fatal("create service: ", err)
 	}

+ 0 - 50
config/address.go

@@ -1,50 +0,0 @@
-package config
-
-import (
-	"encoding/json"
-	"net/netip"
-
-	E "github.com/sagernet/sing/common/exceptions"
-	M "github.com/sagernet/sing/common/metadata"
-)
-
-type ListenAddress netip.Addr
-
-func (a *ListenAddress) MarshalJSON() ([]byte, error) {
-	value := netip.Addr(*a).String()
-	return json.Marshal(value)
-}
-
-func (a *ListenAddress) UnmarshalJSON(bytes []byte) error {
-	var value string
-	err := json.Unmarshal(bytes, &value)
-	if err != nil {
-		return err
-	}
-	addr, err := netip.ParseAddr(value)
-	if err != nil {
-		return err
-	}
-	*a = ListenAddress(addr)
-	return nil
-}
-
-type ServerAddress M.Socksaddr
-
-func (a *ServerAddress) MarshalJSON() ([]byte, error) {
-	value := M.Socksaddr(*a).String()
-	return json.Marshal(value)
-}
-
-func (a *ServerAddress) UnmarshalJSON(bytes []byte) error {
-	var value string
-	err := json.Unmarshal(bytes, &value)
-	if err != nil {
-		return err
-	}
-	if value == "" {
-		return E.New("empty server address")
-	}
-	*a = ServerAddress(M.ParseSocksaddr(value))
-	return nil
-}

+ 0 - 12
config/config.go

@@ -1,12 +0,0 @@
-package config
-
-type Config struct {
-	Log       *LogConfig `json:"log"`
-	Inbounds  []Inbound  `json:"inbounds,omitempty"`
-	Outbounds []Outbound `json:"outbounds,omitempty"`
-	Routes    []Route    `json:"routes,omitempty"`
-}
-
-type LogConfig struct {
-	Level string `json:"level,omitempty"`
-}

+ 0 - 14
config/route.go

@@ -1,14 +0,0 @@
-package config
-
-type Route struct {
-	Type string `json:"type"`
-}
-
-type SimpleRule struct {
-	Inbound   []string `json:"inbound,omitempty"`
-	IPVersion []int    `json:"ip_version,omitempty"`
-	Network   []string `json:"network,omitempty"`
-	Protocol  []string `json:"protocol,omitempty"`
-	Domain    []string `json:"domain,omitempty"`
-	Outbound  string   `json:"outbound,omitempty"`
-}

+ 6 - 0
constant/rule.go

@@ -0,0 +1,6 @@
+package constant
+
+const (
+	RuleTypeDefault = "default"
+	RuleTypeLogical = "logical"
+)

+ 2 - 0
go.mod

@@ -5,6 +5,7 @@ go 1.18
 require (
 	github.com/database64128/tfo-go v1.0.4
 	github.com/logrusorgru/aurora v2.0.3+incompatible
+	github.com/oschwald/geoip2-golang v1.7.0
 	github.com/sagernet/sing v0.0.0-20220701084654-2a0502dd664e
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649
 	github.com/sirupsen/logrus v1.8.1
@@ -14,6 +15,7 @@ require (
 require (
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.0.12 // indirect
+	github.com/oschwald/maxminddb-golang v1.9.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
 	golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect

+ 6 - 1
go.sum

@@ -10,6 +10,10 @@ github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0
 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
 github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
 github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
+github.com/oschwald/geoip2-golang v1.7.0 h1:JW1r5AKi+vv2ujSxjKthySK3jo8w8oKWPyXsw+Qs/S8=
+github.com/oschwald/geoip2-golang v1.7.0/go.mod h1:mdI/C7iK7NVMcIDDtf4bCKMJ7r0o7UwGeCo9eiitCMQ=
+github.com/oschwald/maxminddb-golang v1.9.0 h1:tIk4nv6VT9OiPyrnDAfJS1s1xKDQMZOsGojab6EjC1Y=
+github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm82Cp5HyvYbt8K3zLY=
 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=
@@ -23,8 +27,8 @@ github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
 github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -32,5 +36,6 @@ golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/
 golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
 lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=

+ 20 - 3
log/log.go

@@ -3,10 +3,27 @@ package log
 import (
 	"context"
 
-	"github.com/sirupsen/logrus"
+	"github.com/sagernet/sing-box/option"
 )
 
 type Logger interface {
-	logrus.FieldLogger
-	WithContext(ctx context.Context) *logrus.Entry
+	Trace(args ...interface{})
+	Debug(args ...interface{})
+	Info(args ...interface{})
+	Print(args ...interface{})
+	Warn(args ...interface{})
+	Warning(args ...interface{})
+	Error(args ...interface{})
+	Fatal(args ...interface{})
+	Panic(args ...interface{})
+	WithContext(ctx context.Context) Logger
+	WithPrefix(prefix string) Logger
+	Close() error
+}
+
+func NewLogger(options option.LogOption) (Logger, error) {
+	if options.Disabled {
+		return NewNopLogger(), nil
+	}
+	return NewLogrusLogger(options)
 }

+ 65 - 0
log/logrus.go

@@ -0,0 +1,65 @@
+package log
+
+import (
+	"context"
+	"os"
+
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+	F "github.com/sagernet/sing/common/format"
+	"github.com/sirupsen/logrus"
+)
+
+var _ Logger = (*logrusLogger)(nil)
+
+type logrusLogger struct {
+	abstractLogrusLogger
+	output *os.File
+}
+
+type abstractLogrusLogger interface {
+	logrus.Ext1FieldLogger
+	WithContext(ctx context.Context) *logrus.Entry
+}
+
+func NewLogrusLogger(options option.LogOption) (*logrusLogger, error) {
+	logger := logrus.New()
+	logger.SetLevel(logrus.TraceLevel)
+	logger.Formatter.(*logrus.TextFormatter).ForceColors = true
+	logger.AddHook(new(logrusHook))
+	var output *os.File
+	var err error
+	if options.Level != "" {
+		logger.Level, err = logrus.ParseLevel(options.Level)
+		if err != nil {
+			return nil, err
+		}
+	}
+	if options.Output != "" {
+		output, err = os.OpenFile(options.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+		if err != nil {
+			return nil, E.Extend(err, "open log output")
+		}
+		logger.SetOutput(output)
+	}
+	return &logrusLogger{logger, output}, nil
+}
+
+func (l *logrusLogger) WithContext(ctx context.Context) Logger {
+	return &logrusLogger{l.abstractLogrusLogger.WithContext(ctx), nil}
+}
+
+func (l *logrusLogger) WithPrefix(prefix string) Logger {
+	if entry, isEntry := l.abstractLogrusLogger.(*logrus.Entry); isEntry {
+		loadedPrefix := entry.Data["prefix"]
+		if loadedPrefix != "" {
+			prefix = F.ToString(loadedPrefix, prefix)
+		}
+	}
+	return &logrusLogger{l.WithField("prefix", prefix), nil}
+}
+
+func (l *logrusLogger) Close() error {
+	return common.Close(common.PtrOrNil(l.output))
+}

+ 4 - 4
log/hook.go → log/logrus_hook.go

@@ -6,13 +6,13 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-type Hook struct{}
+type logrusHook struct{}
 
-func (h *Hook) Levels() []logrus.Level {
+func (h *logrusHook) Levels() []logrus.Level {
 	return logrus.AllLevels
 }
 
-func (h *Hook) Fire(entry *logrus.Entry) error {
+func (h *logrusHook) Fire(entry *logrus.Entry) error {
 	if prefix, loaded := entry.Data["prefix"]; loaded {
 		prefixStr := prefix.(string)
 		delete(entry.Data, "prefix")
@@ -20,7 +20,7 @@ func (h *Hook) Fire(entry *logrus.Entry) error {
 	}
 	var idCtx *idContext
 	if entry.Context != nil {
-		idCtx = entry.Context.Value(idType).(*idContext)
+		idCtx, _ = entry.Context.Value(idType).(*idContext)
 	}
 	if idCtx != nil {
 		var color aurora.Color

+ 50 - 0
log/nop.go

@@ -0,0 +1,50 @@
+package log
+
+import "context"
+
+var _ Logger = (*nopLogger)(nil)
+
+type nopLogger struct{}
+
+func NewNopLogger() Logger {
+	return (*nopLogger)(nil)
+}
+
+func (l *nopLogger) Trace(args ...interface{}) {
+}
+
+func (l *nopLogger) Debug(args ...interface{}) {
+}
+
+func (l *nopLogger) Info(args ...interface{}) {
+}
+
+func (l *nopLogger) Print(args ...interface{}) {
+}
+
+func (l *nopLogger) Warn(args ...interface{}) {
+}
+
+func (l *nopLogger) Warning(args ...interface{}) {
+}
+
+func (l *nopLogger) Error(args ...interface{}) {
+}
+
+func (l *nopLogger) Fatal(args ...interface{}) {
+}
+
+func (l *nopLogger) Panic(args ...interface{}) {
+}
+
+func (l *nopLogger) WithContext(ctx context.Context) Logger {
+	return l
+}
+
+func (l *nopLogger) WithPrefix(prefix string) Logger {
+	return l
+}
+
+func (l *nopLogger) Close() error {
+	return nil
+}

+ 27 - 0
option/address.go

@@ -0,0 +1,27 @@
+package option
+
+import (
+	"encoding/json"
+	"net/netip"
+)
+
+type ListenAddress netip.Addr
+
+func (a *ListenAddress) MarshalJSON() ([]byte, error) {
+	value := netip.Addr(*a).String()
+	return json.Marshal(value)
+}
+
+func (a *ListenAddress) UnmarshalJSON(bytes []byte) error {
+	var value string
+	err := json.Unmarshal(bytes, &value)
+	if err != nil {
+		return err
+	}
+	addr, err := netip.ParseAddr(value)
+	if err != nil {
+		return err
+	}
+	*a = ListenAddress(addr)
+	return nil
+}

+ 14 - 0
option/config.go

@@ -0,0 +1,14 @@
+package option
+
+type Options struct {
+	Log       *LogOption `json:"log"`
+	Inbounds  []Inbound  `json:"inbounds,omitempty"`
+	Outbounds []Outbound `json:"outbounds,omitempty"`
+	Routes    []Rule     `json:"routes,omitempty"`
+}
+
+type LogOption struct {
+	Disabled bool   `json:"disabled,omitempty"`
+	Level    string `json:"level,omitempty"`
+	Output   string `json:"output,omitempty"`
+}

+ 1 - 1
config/inbound.go → option/inbound.go

@@ -1,4 +1,4 @@
-package config
+package option
 
 import (
 	"encoding/json"

+ 1 - 1
config/network.go → option/network.go

@@ -1,4 +1,4 @@
-package config
+package option
 
 import (
 	"encoding/json"

+ 14 - 5
config/outbound.go → option/outbound.go

@@ -1,9 +1,10 @@
-package config
+package option
 
 import (
 	"encoding/json"
 
 	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
 )
 
 var ErrUnknownOutboundType = E.New("unknown outbound type")
@@ -81,10 +82,18 @@ type DirectOutboundOptions struct {
 	OverridePort    uint16 `json:"override_port,omitempty"`
 }
 
-type ShadowsocksOutboundOptions struct {
-	DialerOptions
+type ServerOptions struct {
 	Server     string `json:"server"`
 	ServerPort uint16 `json:"server_port"`
-	Method     string `json:"method"`
-	Password   string `json:"password"`
+}
+
+func (o ServerOptions) Build() M.Socksaddr {
+	return M.ParseSocksaddrHostPort(o.Server, o.ServerPort)
+}
+
+type ShadowsocksOutboundOptions struct {
+	DialerOptions
+	ServerOptions
+	Method   string `json:"method"`
+	Password string `json:"password"`
 }

+ 80 - 0
option/route.go

@@ -0,0 +1,80 @@
+package option
+
+import (
+	"encoding/json"
+
+	C "github.com/sagernet/sing-box/constant"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+var ErrUnknownRuleType = E.New("unknown rule type")
+
+type _Rule struct {
+	Type           string      `json:"type"`
+	DefaultOptions DefaultRule `json:"default_options,omitempty"`
+	LogicalOptions LogicalRule `json:"logical_options,omitempty"`
+}
+
+type Rule _Rule
+
+func (r *Rule) MarshalJSON() ([]byte, error) {
+	var content map[string]any
+	switch r.Type {
+	case "", C.RuleTypeDefault:
+		return json.Marshal(r.DefaultOptions)
+	case C.RuleTypeLogical:
+		options, err := json.Marshal(r.LogicalOptions)
+		if err != nil {
+			return nil, err
+		}
+		err = json.Unmarshal(options, &content)
+		if err != nil {
+			return nil, err
+		}
+		content["type"] = r.Type
+		return json.Marshal(content)
+	default:
+		return nil, E.Extend(ErrUnknownRuleType, r.Type)
+	}
+}
+
+func (r *Rule) UnmarshalJSON(bytes []byte) error {
+	err := json.Unmarshal(bytes, (*_Rule)(r))
+	if err != nil {
+		return err
+	}
+	switch r.Type {
+	case "", C.RuleTypeDefault:
+		err = json.Unmarshal(bytes, &r.DefaultOptions)
+	case C.RuleTypeLogical:
+		err = json.Unmarshal(bytes, &r.LogicalOptions)
+	default:
+		err = E.Extend(ErrUnknownRuleType, r.Type)
+	}
+	return err
+}
+
+type DefaultRule struct {
+	Inbound       []string `json:"inbound,omitempty"`
+	IPVersion     []int    `json:"ip_version,omitempty"`
+	Network       []string `json:"network,omitempty"`
+	Protocol      []string `json:"protocol,omitempty"`
+	Domain        []string `json:"domain,omitempty"`
+	DomainSuffix  []string `json:"domain_suffix,omitempty"`
+	DomainKeyword []string `json:"domain_keyword,omitempty"`
+	SourceGeoIP   []string `json:"source_geoip,omitempty"`
+	GeoIP         []string `json:"geoip,omitempty"`
+	SourceIPCIDR  []string `json:"source_ipcidr,omitempty"`
+	SourcePort    []string `json:"source_port,omitempty"`
+	IPCIDR        []string `json:"destination_ipcidr,omitempty"`
+	Port          []string `json:"destination_port,omitempty"`
+	ProcessName   []string `json:"process_name,omitempty"`
+	ProcessPath   []string `json:"process_path,omitempty"`
+	Outbound      string   `json:"outbound,omitempty"`
+}
+
+type LogicalRule struct {
+	Mode     string        `json:"mode"`
+	Rules    []DefaultRule `json:"rules,omitempty"`
+	Outbound string        `json:"outbound,omitempty"`
+}

+ 40 - 75
service.go

@@ -7,106 +7,69 @@ import (
 	"github.com/sagernet/sing-box/adapter/inbound"
 	"github.com/sagernet/sing-box/adapter/outbound"
 	"github.com/sagernet/sing-box/adapter/route"
-	"github.com/sagernet/sing-box/config"
-	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
-	E "github.com/sagernet/sing/common/exceptions"
-	F "github.com/sagernet/sing/common/format"
-	"github.com/sirupsen/logrus"
 )
 
 var _ adapter.Service = (*Service)(nil)
 
 type Service struct {
-	logger    *logrus.Logger
+	router    adapter.Router
+	logger    log.Logger
 	inbounds  []adapter.Inbound
 	outbounds []adapter.Outbound
-	router    *route.Router
 }
 
-func NewService(ctx context.Context, options *config.Config) (service *Service, err error) {
-	logger := logrus.New()
-	logger.SetLevel(logrus.TraceLevel)
-	logger.Formatter.(*logrus.TextFormatter).ForceColors = true
-	logger.AddHook(new(log.Hook))
+func NewService(ctx context.Context, options *option.Options) (*Service, error) {
+	var logOptions option.LogOption
 	if options.Log != nil {
-		if options.Log.Level != "" {
-			logger.Level, err = logrus.ParseLevel(options.Log.Level)
-			if err != nil {
-				return
-			}
-		}
+		logOptions = *options.Log
 	}
-	service = &Service{
-		logger: logger,
-		router: route.NewRouter(logrus.NewEntry(logger).WithFields(logrus.Fields{"prefix": "router: "})),
+	logger, err := log.NewLogger(logOptions)
+	if err != nil {
+		return nil, err
 	}
+	router := route.NewRouter(logger)
+	var inbounds []adapter.Inbound
+	var outbounds []adapter.Outbound
 	if len(options.Inbounds) > 0 {
 		for i, inboundOptions := range options.Inbounds {
-			var prefix string
-			if inboundOptions.Tag != "" {
-				prefix = inboundOptions.Tag
-			} else {
-				prefix = F.ToString(i)
-			}
-			prefix = F.ToString("inbound/", inboundOptions.Type, "[", prefix, "]: ")
-			inboundLogger := logrus.NewEntry(logger).WithFields(logrus.Fields{"prefix": prefix})
 			var inboundService adapter.Inbound
-			switch inboundOptions.Type {
-			case C.TypeDirect:
-				inboundService = inbound.NewDirect(ctx, service.router, inboundLogger, inboundOptions.Tag, inboundOptions.DirectOptions)
-			case C.TypeSocks:
-				inboundService = inbound.NewSocks(ctx, service.router, inboundLogger, inboundOptions.Tag, inboundOptions.SocksOptions)
-			case C.TypeHTTP:
-				inboundService = inbound.NewHTTP(ctx, service.router, inboundLogger, inboundOptions.Tag, inboundOptions.HTTPOptions)
-			case C.TypeMixed:
-				inboundService = inbound.NewMixed(ctx, service.router, inboundLogger, inboundOptions.Tag, inboundOptions.MixedOptions)
-			case C.TypeShadowsocks:
-				inboundService, err = inbound.NewShadowsocks(ctx, service.router, inboundLogger, inboundOptions.Tag, inboundOptions.ShadowsocksOptions)
-			default:
-				err = E.New("unknown inbound type: " + inboundOptions.Type)
-			}
+			inboundService, err = inbound.New(ctx, router, logger, i, inboundOptions)
 			if err != nil {
-				return
+				return nil, err
 			}
-			service.inbounds = append(service.inbounds, inboundService)
+			inbounds = append(inbounds, inboundService)
 		}
 	}
 	for i, outboundOptions := range options.Outbounds {
-		var prefix string
-		if outboundOptions.Tag != "" {
-			prefix = outboundOptions.Tag
-		} else {
-			prefix = F.ToString(i)
-		}
-		prefix = F.ToString("outbound/", outboundOptions.Type, "[", prefix, "]: ")
-		outboundLogger := logrus.NewEntry(logger).WithFields(logrus.Fields{"prefix": prefix})
-		var outboundHandler adapter.Outbound
-		switch outboundOptions.Type {
-		case C.TypeDirect:
-			outboundHandler = outbound.NewDirect(service.router, outboundLogger, outboundOptions.Tag, outboundOptions.DirectOptions)
-		case C.TypeShadowsocks:
-			outboundHandler, err = outbound.NewShadowsocks(service.router, outboundLogger, outboundOptions.Tag, outboundOptions.ShadowsocksOptions)
-		default:
-			err = E.New("unknown outbound type: " + outboundOptions.Type)
-		}
+		var outboundService adapter.Outbound
+		outboundService, err = outbound.New(router, logger, i, outboundOptions)
 		if err != nil {
-			return
+			return nil, err
 		}
-		service.outbounds = append(service.outbounds, outboundHandler)
-		service.router.AddOutbound(outboundHandler)
+		outbounds = append(outbounds, outboundService)
+	}
+	if len(outbounds) == 0 {
+		outbounds = append(outbounds, outbound.NewDirect(nil, logger, "direct", &option.DirectOutboundOptions{}))
 	}
-	if len(service.outbounds) == 0 {
-		service.outbounds = append(service.outbounds, outbound.NewDirect(nil, logger, "direct", &config.DirectOutboundOptions{}))
-		service.router.AddOutbound(service.outbounds[0])
+	router.UpdateOutbounds(outbounds)
+	err = router.UpdateRules(options.Routes)
+	if err != nil {
+		return nil, err
 	}
-	return
+	return &Service{
+		router:    router,
+		logger:    logger,
+		inbounds:  inbounds,
+		outbounds: outbounds,
+	}, nil
 }
 
 func (s *Service) Start() error {
-	for _, inbound := range s.inbounds {
-		err := inbound.Start()
+	for _, in := range s.inbounds {
+		err := in.Start()
 		if err != nil {
 			return err
 		}
@@ -115,11 +78,13 @@ func (s *Service) Start() error {
 }
 
 func (s *Service) Close() error {
-	for _, inbound := range s.inbounds {
-		inbound.Close()
+	for _, in := range s.inbounds {
+		in.Close()
 	}
-	for _, outbound := range s.outbounds {
-		common.Close(outbound)
+	for _, out := range s.outbounds {
+		common.Close(out)
 	}
+	s.logger.Close()
+	s.router.Close()
 	return nil
 }