diff --git a/adapter/inbound/builder.go b/adapter/inbound/builder.go index 794011df..251a2cf6 100644 --- a/adapter/inbound/builder.go +++ b/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) } } diff --git a/adapter/outbound/builder.go b/adapter/outbound/builder.go index 7a4fff84..08841f6c 100644 --- a/adapter/outbound/builder.go +++ b/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) } } diff --git a/adapter/route/rule.go b/adapter/route/rule.go index 21ad4c76..83d27a95 100644 --- a/adapter/route/rule.go +++ b/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) diff --git a/cmd/sing-box/main.go b/cmd/sing-box/main.go index 0993a1d0..5f456f87 100644 --- a/cmd/sing-box/main.go +++ b/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) diff --git a/go.mod b/go.mod index 1524eef9..e3c2c640 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index b8b4117a..f06ee00e 100644 --- a/go.sum +++ b/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= diff --git a/log/log.go b/log/log.go index 4096d605..d0cce52d 100644 --- a/log/log.go +++ b/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) { diff --git a/log/logrus.go b/log/logrus.go index 254aa202..828a9b5f 100644 --- a/log/logrus.go +++ b/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)} } diff --git a/log/nop.go b/log/nop.go index ad21fe55..1ab8d71a 100644 --- a/log/nop.go +++ b/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 -} diff --git a/option/config.go b/option/config.go index ebc620d0..7e4a0286 100644 --- a/option/config.go +++ b/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"` diff --git a/option/inbound.go b/option/inbound.go index 21afb334..0cf29e8c 100644 --- a/option/inbound.go +++ b/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) + return nil, E.New("unknown inbound type: ", i.Type) } - if err != nil { - return nil, err - } - 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 *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 +} diff --git a/option/json.go b/option/json.go new file mode 100644 index 00000000..e17a33f2 --- /dev/null +++ b/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) +} diff --git a/option/listable.go b/option/listable.go index 8ccc628c..877c06a4 100644 --- a/option/listable.go +++ b/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) diff --git a/option/outbound.go b/option/outbound.go index 53982285..713f25c6 100644 --- a/option/outbound.go +++ b/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) + return nil, E.New("unknown outbound type: ", i.Type) } - if err != nil { - return nil, err - } - 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 { diff --git a/option/route.go b/option/route.go index ce1d0cef..51ab78ec 100644 --- a/option/route.go +++ b/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 } - switch r.Type { - case "", C.RuleTypeDefault: - if r.DefaultOptions == nil { - break - } - err = json.Unmarshal(bytes, r.DefaultOptions) - case C.RuleTypeLogical: - if r.LogicalOptions == nil { - break - } - err = json.Unmarshal(bytes, r.LogicalOptions) - default: - err = E.Extend(ErrUnknownRuleType, r.Type) + if r.Type == "" { + r.Type = C.RuleTypeDefault } - return err + var v any + switch r.Type { + case C.RuleTypeDefault: + v = &r.DefaultOptions + case C.RuleTypeLogical: + v = &r.LogicalOptions + default: + return E.New("unknown rule type: " + r.Type) + } + 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 +} diff --git a/service.go b/service.go index 669abec8..ead3b19e 100644 --- a/service.go +++ b/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, + ) }