Browse Source

Invalid config check

世界 3 years ago
parent
commit
30444057bd
16 changed files with 273 additions and 159 deletions
  1. 10 6
      adapter/inbound/builder.go
  2. 7 3
      adapter/outbound/builder.go
  3. 15 0
      adapter/route/rule.go
  4. 11 1
      cmd/sing-box/main.go
  5. 1 1
      go.mod
  6. 2 2
      go.sum
  7. 2 1
      log/log.go
  8. 17 13
      log/logrus.go
  9. 8 4
      log/nop.go
  10. 9 0
      option/config.go
  11. 57 54
      option/inbound.go
  12. 40 0
      option/json.go
  13. 1 1
      option/listable.go
  14. 16 37
      option/outbound.go
  15. 60 27
      option/route.go
  16. 17 9
      service.go

+ 10 - 6
adapter/inbound/builder.go

@@ -8,10 +8,14 @@ import (
 	"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"
 )
 
 func New(ctx context.Context, router adapter.Router, logger log.Logger, index int, options option.Inbound) (adapter.Inbound, error) {
+	if common.IsEmptyByEquals(options) {
+		return nil, E.New("empty inbound config")
+	}
 	var tag string
 	if options.Tag != "" {
 		tag = options.Tag
@@ -21,16 +25,16 @@ func New(ctx context.Context, router adapter.Router, logger log.Logger, index in
 	inboundLogger := logger.WithPrefix(F.ToString("inbound/", options.Type, "[", tag, "]: "))
 	switch options.Type {
 	case C.TypeDirect:
-		return NewDirect(ctx, router, inboundLogger, options.Tag, common.PtrValueOrDefault(options.DirectOptions)), nil
+		return NewDirect(ctx, router, inboundLogger, options.Tag, options.DirectOptions), nil
 	case C.TypeSocks:
-		return NewSocks(ctx, router, inboundLogger, options.Tag, common.PtrValueOrDefault(options.SocksOptions)), nil
+		return NewSocks(ctx, router, inboundLogger, options.Tag, options.SocksOptions), nil
 	case C.TypeHTTP:
-		return NewHTTP(ctx, router, inboundLogger, options.Tag, common.PtrValueOrDefault(options.HTTPOptions)), nil
+		return NewHTTP(ctx, router, inboundLogger, options.Tag, options.HTTPOptions), nil
 	case C.TypeMixed:
-		return NewMixed(ctx, router, inboundLogger, options.Tag, common.PtrValueOrDefault(options.MixedOptions)), nil
+		return NewMixed(ctx, router, inboundLogger, options.Tag, options.MixedOptions), nil
 	case C.TypeShadowsocks:
-		return NewShadowsocks(ctx, router, inboundLogger, options.Tag, common.PtrValueOrDefault(options.ShadowsocksOptions))
+		return NewShadowsocks(ctx, router, inboundLogger, options.Tag, options.ShadowsocksOptions)
 	default:
-		panic(F.ToString("unknown inbound type: ", options.Type))
+		return nil, E.New("unknown inbound type: ", options.Type)
 	}
 }

+ 7 - 3
adapter/outbound/builder.go

@@ -6,10 +6,14 @@ import (
 	"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"
 )
 
 func New(router adapter.Router, logger log.Logger, index int, options option.Outbound) (adapter.Outbound, error) {
+	if common.IsEmpty(options) {
+		return nil, E.New("empty outbound config")
+	}
 	var tag string
 	if options.Tag != "" {
 		tag = options.Tag
@@ -19,10 +23,10 @@ func New(router adapter.Router, logger log.Logger, index int, options option.Out
 	outboundLogger := logger.WithPrefix(F.ToString("outbound/", options.Type, "[", tag, "]: "))
 	switch options.Type {
 	case C.TypeDirect:
-		return NewDirect(router, outboundLogger, options.Tag, common.PtrValueOrDefault(options.DirectOptions)), nil
+		return NewDirect(router, outboundLogger, options.Tag, options.DirectOptions), nil
 	case C.TypeShadowsocks:
-		return NewShadowsocks(router, outboundLogger, options.Tag, common.PtrValueOrDefault(options.ShadowsocksOptions))
+		return NewShadowsocks(router, outboundLogger, options.Tag, options.ShadowsocksOptions)
 	default:
-		panic(F.ToString("unknown outbound type: ", options.Type))
+		return nil, E.New("unknown outbound type: ", options.Type)
 	}
 }

+ 15 - 0
adapter/route/rule.go

@@ -13,10 +13,25 @@ import (
 )
 
 func NewRule(router adapter.Router, logger log.Logger, options option.Rule) (adapter.Rule, error) {
+	if common.IsEmptyByEquals(options) {
+		return nil, E.New("empty rule config")
+	}
 	switch options.Type {
 	case "", C.RuleTypeDefault:
+		if !options.DefaultOptions.IsValid() {
+			return nil, E.New("missing conditions")
+		}
+		if options.DefaultOptions.Outbound == "" {
+			return nil, E.New("missing outbound field")
+		}
 		return NewDefaultRule(router, logger, common.PtrValueOrDefault(options.DefaultOptions))
 	case C.RuleTypeLogical:
+		if !options.LogicalOptions.IsValid() {
+			return nil, E.New("missing conditions")
+		}
+		if options.LogicalOptions.Outbound == "" {
+			return nil, E.New("missing outbound field")
+		}
 		return NewLogicalRule(router, logger, common.PtrValueOrDefault(options.LogicalOptions))
 	default:
 		return nil, E.New("unknown rule type: ", options.Type)

+ 11 - 1
cmd/sing-box/main.go

@@ -18,7 +18,10 @@ func init() {
 	logrus.StandardLogger().Formatter.(*logrus.TextFormatter).ForceColors = true
 }
 
-var configPath string
+var (
+	configPath string
+	workingDir string
+)
 
 func main() {
 	command := &cobra.Command{
@@ -26,12 +29,19 @@ func main() {
 		Run: run,
 	}
 	command.Flags().StringVarP(&configPath, "config", "c", "config.json", "set configuration file path")
+	command.Flags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
 	if err := command.Execute(); err != nil {
 		logrus.Fatal(err)
 	}
 }
 
 func run(cmd *cobra.Command, args []string) {
+	if workingDir != "" {
+		if err := os.Chdir(workingDir); err != nil {
+			logrus.Fatal(err)
+		}
+	}
+
 	configContent, err := os.ReadFile(configPath)
 	if err != nil {
 		logrus.Fatal("read config: ", err)

+ 1 - 1
go.mod

@@ -6,7 +6,7 @@ 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-20220702141141-b3923d54845b
+	github.com/sagernet/sing v0.0.0-20220702174608-cb5bb5132de4
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649
 	github.com/sirupsen/logrus v1.8.1
 	github.com/spf13/cobra v1.5.0

+ 2 - 2
go.sum

@@ -18,8 +18,8 @@ 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-20220702141141-b3923d54845b h1:oK5RglZ0s4oXNSrIsLJkBiHbYoAUMOGLN3a0JgDNzVM=
-github.com/sagernet/sing v0.0.0-20220702141141-b3923d54845b/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
+github.com/sagernet/sing v0.0.0-20220702174608-cb5bb5132de4 h1:Ce6nW9gV6g2hq/1K/nMtlVGiTtxh86EWs0/jOzMuNa4=
+github.com/sagernet/sing v0.0.0-20220702174608-cb5bb5132de4/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=

+ 2 - 1
log/log.go

@@ -7,6 +7,8 @@ import (
 )
 
 type Logger interface {
+	Start() error
+	Close() error
 	Trace(args ...interface{})
 	Debug(args ...interface{})
 	Info(args ...interface{})
@@ -18,7 +20,6 @@ type Logger interface {
 	Panic(args ...interface{})
 	WithContext(ctx context.Context) Logger
 	WithPrefix(prefix string) Logger
-	Close() error
 }
 
 func NewLogger(options option.LogOption) (Logger, error) {

+ 17 - 13
log/logrus.go

@@ -15,7 +15,8 @@ var _ Logger = (*logrusLogger)(nil)
 
 type logrusLogger struct {
 	abstractLogrusLogger
-	output *os.File
+	outputPath string
+	output     *os.File
 }
 
 type abstractLogrusLogger interface {
@@ -28,7 +29,6 @@ func NewLogrusLogger(options option.LogOption) (*logrusLogger, error) {
 	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)
@@ -36,18 +36,26 @@ func NewLogrusLogger(options option.LogOption) (*logrusLogger, error) {
 			return nil, err
 		}
 	}
-	if options.Output != "" {
-		output, err = os.OpenFile(options.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+	return &logrusLogger{logger, options.Output, nil}, nil
+}
+
+func (l *logrusLogger) Start() error {
+	if l.outputPath != "" {
+		output, err := os.OpenFile(l.outputPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
 		if err != nil {
-			return nil, E.Extend(err, "open log output")
+			return E.Cause(err, "open log output")
 		}
-		logger.SetOutput(output)
+		l.abstractLogrusLogger.(*logrus.Logger).SetOutput(output)
 	}
-	return &logrusLogger{logger, output}, nil
+	return nil
+}
+
+func (l *logrusLogger) Close() error {
+	return common.Close(common.PtrOrNil(l.output))
 }
 
 func (l *logrusLogger) WithContext(ctx context.Context) Logger {
-	return &logrusLogger{l.abstractLogrusLogger.WithContext(ctx), nil}
+	return &logrusLogger{abstractLogrusLogger: l.abstractLogrusLogger.WithContext(ctx)}
 }
 
 func (l *logrusLogger) WithPrefix(prefix string) Logger {
@@ -57,9 +65,5 @@ func (l *logrusLogger) WithPrefix(prefix string) Logger {
 			prefix = F.ToString(loadedPrefix, prefix)
 		}
 	}
-	return &logrusLogger{l.WithField("prefix", prefix), nil}
-}
-
-func (l *logrusLogger) Close() error {
-	return common.Close(common.PtrOrNil(l.output))
+	return &logrusLogger{abstractLogrusLogger: l.WithField("prefix", prefix)}
 }

+ 8 - 4
log/nop.go

@@ -10,6 +10,14 @@ func NewNopLogger() Logger {
 	return (*nopLogger)(nil)
 }
 
+func (l *nopLogger) Start() error {
+	return nil
+}
+
+func (l *nopLogger) Close() error {
+	return nil
+}
+
 func (l *nopLogger) Trace(args ...interface{}) {
 }
 
@@ -44,7 +52,3 @@ func (l *nopLogger) WithContext(ctx context.Context) Logger {
 func (l *nopLogger) WithPrefix(prefix string) Logger {
 	return l
 }
-
-func (l *nopLogger) Close() error {
-	return nil
-}

+ 9 - 0
option/config.go

@@ -1,5 +1,7 @@
 package option
 
+import "github.com/sagernet/sing/common"
+
 type Options struct {
 	Log       *LogOption    `json:"log"`
 	Inbounds  []Inbound     `json:"inbounds,omitempty"`
@@ -7,6 +9,13 @@ type Options struct {
 	Route     *RouteOptions `json:"route,omitempty"`
 }
 
+func (o Options) Equals(other Options) bool {
+	return common.ComparablePtrEquals(o.Log, other.Log) &&
+		common.SliceEquals(o.Inbounds, other.Inbounds) &&
+		common.ComparableSliceEquals(o.Outbounds, other.Outbounds) &&
+		common.PtrEquals(o.Route, other.Route)
+}
+
 type LogOption struct {
 	Disabled bool   `json:"disabled,omitempty"`
 	Level    string `json:"level,omitempty"`

+ 57 - 54
option/inbound.go

@@ -3,52 +3,50 @@ package option
 import (
 	"encoding/json"
 
+	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
 	E "github.com/sagernet/sing/common/exceptions"
 )
 
-var ErrUnknownInboundType = E.New("unknown inbound type")
-
 type _Inbound struct {
-	Tag                string                     `json:"tag,omitempty"`
-	Type               string                     `json:"type,omitempty"`
-	DirectOptions      *DirectInboundOptions      `json:"directOptions,omitempty"`
-	SocksOptions       *SimpleInboundOptions      `json:"socksOptions,omitempty"`
-	HTTPOptions        *SimpleInboundOptions      `json:"httpOptions,omitempty"`
-	MixedOptions       *SimpleInboundOptions      `json:"mixedOptions,omitempty"`
-	ShadowsocksOptions *ShadowsocksInboundOptions `json:"shadowsocksOptions,omitempty"`
+	Tag                string                    `json:"tag,omitempty"`
+	Type               string                    `json:"type"`
+	DirectOptions      DirectInboundOptions      `json:"-"`
+	SocksOptions       SimpleInboundOptions      `json:"-"`
+	HTTPOptions        SimpleInboundOptions      `json:"-"`
+	MixedOptions       SimpleInboundOptions      `json:"-"`
+	ShadowsocksOptions ShadowsocksInboundOptions `json:"-"`
 }
 
 type Inbound _Inbound
 
+func (i Inbound) Equals(other Inbound) bool {
+	return i.Type == other.Type &&
+		i.Tag == other.Tag &&
+		common.Equals(i.DirectOptions, other.DirectOptions) &&
+		common.Equals(i.SocksOptions, other.SocksOptions) &&
+		common.Equals(i.HTTPOptions, other.HTTPOptions) &&
+		common.Equals(i.MixedOptions, other.MixedOptions) &&
+		common.Equals(i.ShadowsocksOptions, other.ShadowsocksOptions)
+}
+
 func (i *Inbound) MarshalJSON() ([]byte, error) {
-	var options []byte
-	var err error
+	var v any
 	switch i.Type {
 	case "direct":
-		options, err = json.Marshal(i.DirectOptions)
+		v = i.DirectOptions
 	case "socks":
-		options, err = json.Marshal(i.SocksOptions)
+		v = i.SocksOptions
 	case "http":
-		options, err = json.Marshal(i.HTTPOptions)
+		v = i.HTTPOptions
 	case "mixed":
-		options, err = json.Marshal(i.MixedOptions)
+		v = i.MixedOptions
 	case "shadowsocks":
-		options, err = json.Marshal(i.ShadowsocksOptions)
+		v = i.ShadowsocksOptions
 	default:
-		return nil, E.Extend(ErrUnknownInboundType, i.Type)
-	}
-	if err != nil {
-		return nil, err
-	}
-	var content map[string]any
-	err = json.Unmarshal(options, &content)
-	if err != nil {
-		return nil, err
+		return nil, E.New("unknown inbound type: ", i.Type)
 	}
-	content["tag"] = i.Tag
-	content["type"] = i.Type
-	return json.Marshal(content)
+	return MarshallObjects(i, v)
 }
 
 func (i *Inbound) UnmarshalJSON(bytes []byte) error {
@@ -56,43 +54,29 @@ func (i *Inbound) UnmarshalJSON(bytes []byte) error {
 	if err != nil {
 		return err
 	}
+	var v any
 	switch i.Type {
 	case "direct":
-		if i.DirectOptions != nil {
-			break
-		}
-		err = json.Unmarshal(bytes, &i.DirectOptions)
+		v = &i.DirectOptions
 	case "socks":
-		if i.SocksOptions != nil {
-			break
-		}
-		err = json.Unmarshal(bytes, &i.SocksOptions)
+		v = &i.SocksOptions
 	case "http":
-		if i.HTTPOptions != nil {
-			break
-		}
-		err = json.Unmarshal(bytes, &i.HTTPOptions)
+		v = &i.HTTPOptions
 	case "mixed":
-		if i.MixedOptions != nil {
-			break
-		}
-		err = json.Unmarshal(bytes, &i.MixedOptions)
+		v = &i.MixedOptions
 	case "shadowsocks":
-		if i.ShadowsocksOptions != nil {
-			break
-		}
-		err = json.Unmarshal(bytes, &i.ShadowsocksOptions)
+		v = &i.ShadowsocksOptions
 	default:
-		return E.Extend(ErrUnknownInboundType, i.Type)
+		return nil
 	}
-	return err
+	return json.Unmarshal(bytes, v)
 }
 
 type ListenOptions struct {
 	Listen      ListenAddress `json:"listen"`
 	Port        uint16        `json:"listen_port"`
-	TCPFastOpen bool          `json:"tcpFastOpen,omitempty"`
-	UDPTimeout  int64         `json:"udpTimeout,omitempty"`
+	TCPFastOpen bool          `json:"tcp_fast_open,omitempty"`
+	UDPTimeout  int64         `json:"udp_timeout,omitempty"`
 }
 
 type SimpleInboundOptions struct {
@@ -100,11 +84,23 @@ type SimpleInboundOptions struct {
 	Users []auth.User `json:"users,omitempty"`
 }
 
+func (o SimpleInboundOptions) Equals(other SimpleInboundOptions) bool {
+	return o.ListenOptions == other.ListenOptions &&
+		common.ComparableSliceEquals(o.Users, other.Users)
+}
+
 type DirectInboundOptions struct {
 	ListenOptions
 	Network         NetworkList `json:"network,omitempty"`
-	OverrideAddress string      `json:"overrideAddress,omitempty"`
-	OverridePort    uint16      `json:"overridePort,omitempty"`
+	OverrideAddress string      `json:"override_address,omitempty"`
+	OverridePort    uint16      `json:"override_port,omitempty"`
+}
+
+func (o DirectInboundOptions) Equals(other DirectInboundOptions) bool {
+	return o.ListenOptions == other.ListenOptions &&
+		common.ComparableSliceEquals(o.Network, other.Network) &&
+		o.OverrideAddress == other.OverrideAddress &&
+		o.OverridePort == other.OverridePort
 }
 
 type ShadowsocksInboundOptions struct {
@@ -113,3 +109,10 @@ type ShadowsocksInboundOptions struct {
 	Method   string      `json:"method"`
 	Password string      `json:"password"`
 }
+
+func (o ShadowsocksInboundOptions) Equals(other ShadowsocksInboundOptions) bool {
+	return o.ListenOptions == other.ListenOptions &&
+		common.ComparableSliceEquals(o.Network, other.Network) &&
+		o.Method == other.Method &&
+		o.Password == other.Password
+}

+ 40 - 0
option/json.go

@@ -0,0 +1,40 @@
+package option
+
+import (
+	"encoding/json"
+)
+
+func ToMap(v any) (map[string]any, error) {
+	bytes, err := json.Marshal(v)
+	if err != nil {
+		return nil, err
+	}
+	var content map[string]any
+	err = json.Unmarshal(bytes, &content)
+	if err != nil {
+		return nil, err
+	}
+	return content, nil
+}
+
+func MergeObjects(objects ...any) (map[string]any, error) {
+	content := make(map[string]any)
+	for _, object := range objects {
+		objectMap, err := ToMap(object)
+		if err != nil {
+			return nil, err
+		}
+		for k, v := range objectMap {
+			content[k] = v
+		}
+	}
+	return content, nil
+}
+
+func MarshallObjects(objects ...any) ([]byte, error) {
+	content, err := MergeObjects(objects...)
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(content)
+}

+ 1 - 1
option/listable.go

@@ -2,7 +2,7 @@ package option
 
 import "encoding/json"
 
-type Listable[T any] []T
+type Listable[T comparable] []T
 
 func (l *Listable[T]) MarshalJSON() ([]byte, error) {
 	arrayList := []T(*l)

+ 16 - 37
option/outbound.go

@@ -7,64 +7,43 @@ import (
 	M "github.com/sagernet/sing/common/metadata"
 )
 
-var ErrUnknownOutboundType = E.New("unknown outbound type")
-
 type _Outbound struct {
-	Tag                string                      `json:"tag,omitempty"`
-	Type               string                      `json:"type,omitempty"`
-	DirectOptions      *DirectOutboundOptions      `json:"directOptions,omitempty"`
-	ShadowsocksOptions *ShadowsocksOutboundOptions `json:"shadowsocksOptions,omitempty"`
+	Tag                string                     `json:"tag,omitempty"`
+	Type               string                     `json:"type,omitempty"`
+	DirectOptions      DirectOutboundOptions      `json:"-"`
+	ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"`
 }
 
 type Outbound _Outbound
 
 func (i *Outbound) MarshalJSON() ([]byte, error) {
-	var options []byte
-	var err error
+	var v any
 	switch i.Type {
 	case "direct":
-		options, err = json.Marshal(i.DirectOptions)
+		v = i.DirectOptions
 	case "shadowsocks":
-		options, err = json.Marshal(i.ShadowsocksOptions)
+		v = i.ShadowsocksOptions
 	default:
-		return nil, E.Extend(ErrUnknownOutboundType, i.Type)
-	}
-	if err != nil {
-		return nil, err
+		return nil, E.New("unknown outbound type: ", i.Type)
 	}
-	var content map[string]any
-	err = json.Unmarshal(options, &content)
-	if err != nil {
-		return nil, err
-	}
-	content["tag"] = i.Tag
-	content["type"] = i.Type
-	return json.Marshal(content)
+	return MarshallObjects(i, v)
 }
 
 func (i *Outbound) UnmarshalJSON(bytes []byte) error {
-	if err := json.Unmarshal(bytes, (*_Outbound)(i)); err != nil {
+	err := json.Unmarshal(bytes, (*_Outbound)(i))
+	if err != nil {
 		return err
 	}
+	var v any
 	switch i.Type {
 	case "direct":
-		if i.DirectOptions != nil {
-			break
-		}
-		if err := json.Unmarshal(bytes, &i.DirectOptions); err != nil {
-			return err
-		}
+		v = &i.DirectOptions
 	case "shadowsocks":
-		if i.ShadowsocksOptions != nil {
-			break
-		}
-		if err := json.Unmarshal(bytes, &i.ShadowsocksOptions); err != nil {
-			return err
-		}
+		v = &i.ShadowsocksOptions
 	default:
-		return E.Extend(ErrUnknownOutboundType, i.Type)
+		return nil
 	}
-	return nil
+	return json.Unmarshal(bytes, v)
 }
 
 type DialerOptions struct {

+ 60 - 27
option/route.go

@@ -4,16 +4,20 @@ import (
 	"encoding/json"
 
 	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 )
 
-var ErrUnknownRuleType = E.New("unknown rule type")
-
 type RouteOptions struct {
 	GeoIP *GeoIPOptions `json:"geoip,omitempty"`
 	Rules []Rule        `json:"rules,omitempty"`
 }
 
+func (o RouteOptions) Equals(other RouteOptions) bool {
+	return common.ComparablePtrEquals(o.GeoIP, other.GeoIP) &&
+		common.SliceEquals(o.Rules, other.Rules)
+}
+
 type GeoIPOptions struct {
 	Path           string `json:"path,omitempty"`
 	DownloadURL    string `json:"download_url,omitempty"`
@@ -28,25 +32,23 @@ type _Rule struct {
 
 type Rule _Rule
 
+func (r Rule) Equals(other Rule) bool {
+	return r.Type == other.Type &&
+		common.PtrEquals(r.DefaultOptions, other.DefaultOptions) &&
+		common.PtrEquals(r.LogicalOptions, other.LogicalOptions)
+}
+
 func (r *Rule) MarshalJSON() ([]byte, error) {
-	var content map[string]any
+	var v any
 	switch r.Type {
-	case "", C.RuleTypeDefault:
-		return json.Marshal(r.DefaultOptions)
+	case C.RuleTypeDefault:
+		v = 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)
+		v = r.LogicalOptions
 	default:
-		return nil, E.Extend(ErrUnknownRuleType, r.Type)
+		return nil, E.New("unknown rule type: " + r.Type)
 	}
+	return MarshallObjects(r, v)
 }
 
 func (r *Rule) UnmarshalJSON(bytes []byte) error {
@@ -54,21 +56,19 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error {
 	if err != nil {
 		return err
 	}
+	if r.Type == "" {
+		r.Type = C.RuleTypeDefault
+	}
+	var v any
 	switch r.Type {
-	case "", C.RuleTypeDefault:
-		if r.DefaultOptions == nil {
-			break
-		}
-		err = json.Unmarshal(bytes, r.DefaultOptions)
+	case C.RuleTypeDefault:
+		v = &r.DefaultOptions
 	case C.RuleTypeLogical:
-		if r.LogicalOptions == nil {
-			break
-		}
-		err = json.Unmarshal(bytes, r.LogicalOptions)
+		v = &r.LogicalOptions
 	default:
-		err = E.Extend(ErrUnknownRuleType, r.Type)
+		return E.New("unknown rule type: " + r.Type)
 	}
-	return err
+	return json.Unmarshal(bytes, v)
 }
 
 type DefaultRule struct {
@@ -90,8 +90,41 @@ type DefaultRule struct {
 	Outbound string `json:"outbound,omitempty"`
 }
 
+func (r DefaultRule) IsValid() bool {
+	var defaultValue DefaultRule
+	defaultValue.Outbound = r.Outbound
+	return !r.Equals(defaultValue)
+}
+
+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.Protocol, other.Protocol) &&
+		common.ComparableSliceEquals(r.Domain, other.Domain) &&
+		common.ComparableSliceEquals(r.DomainSuffix, other.DomainSuffix) &&
+		common.ComparableSliceEquals(r.DomainKeyword, other.DomainKeyword) &&
+		common.ComparableSliceEquals(r.SourceGeoIP, other.SourceGeoIP) &&
+		common.ComparableSliceEquals(r.GeoIP, other.GeoIP) &&
+		common.ComparableSliceEquals(r.SourceIPCIDR, other.SourceIPCIDR) &&
+		common.ComparableSliceEquals(r.IPCIDR, other.IPCIDR) &&
+		common.ComparableSliceEquals(r.SourcePort, other.SourcePort) &&
+		common.ComparableSliceEquals(r.Port, other.Port) &&
+		r.Outbound == other.Outbound
+}
+
 type LogicalRule struct {
 	Mode     string        `json:"mode"`
 	Rules    []DefaultRule `json:"rules,omitempty"`
 	Outbound string        `json:"outbound,omitempty"`
 }
+
+func (r LogicalRule) IsValid() bool {
+	return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid)
+}
+
+func (r LogicalRule) Equals(other LogicalRule) bool {
+	return r.Mode == other.Mode &&
+		common.SliceEquals(r.Rules, other.Rules) &&
+		r.Outbound == other.Outbound
+}

+ 17 - 9
service.go

@@ -10,6 +10,7 @@ import (
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
 )
 
 var _ adapter.Service = (*Service)(nil)
@@ -24,11 +25,11 @@ type Service struct {
 func NewService(ctx context.Context, options option.Options) (*Service, error) {
 	logger, err := log.NewLogger(common.PtrValueOrDefault(options.Log))
 	if err != nil {
-		return nil, err
+		return nil, E.Cause(err, "parse log options")
 	}
 	router, err := route.NewRouter(ctx, logger, common.PtrValueOrDefault(options.Route))
 	if err != nil {
-		return nil, err
+		return nil, E.Cause(err, "parse route options")
 	}
 	inbounds := make([]adapter.Inbound, 0, len(options.Inbounds))
 	outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
@@ -36,7 +37,7 @@ func NewService(ctx context.Context, options option.Options) (*Service, error) {
 		var inboundService adapter.Inbound
 		inboundService, err = inbound.New(ctx, router, logger, i, inboundOptions)
 		if err != nil {
-			return nil, err
+			return nil, E.Cause(err, "parse inbound[", i, "]")
 		}
 		inbounds = append(inbounds, inboundService)
 	}
@@ -44,7 +45,7 @@ func NewService(ctx context.Context, options option.Options) (*Service, error) {
 		var outboundService adapter.Outbound
 		outboundService, err = outbound.New(router, logger, i, outboundOptions)
 		if err != nil {
-			return nil, err
+			return nil, E.Cause(err, "parse outbound[", i, "]")
 		}
 		outbounds = append(outbounds, outboundService)
 	}
@@ -61,13 +62,19 @@ func NewService(ctx context.Context, options option.Options) (*Service, error) {
 }
 
 func (s *Service) Start() error {
+	err := s.logger.Start()
+	if err != nil {
+		return err
+	}
 	for _, in := range s.inbounds {
-		err := in.Start()
+		err = in.Start()
 		if err != nil {
 			return err
 		}
 	}
-	return nil
+	return common.AnyError(
+		s.router.Start(),
+	)
 }
 
 func (s *Service) Close() error {
@@ -77,7 +84,8 @@ func (s *Service) Close() error {
 	for _, out := range s.outbounds {
 		common.Close(out)
 	}
-	s.logger.Close()
-	s.router.Close()
-	return nil
+	return common.Close(
+		s.router,
+		s.logger,
+	)
 }