From a21eaa7de5ca5d8dd450e75ed77d8dbbe8da647c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= <i@sekai.icu>
Date: Thu, 7 Nov 2024 12:02:36 +0800
Subject: [PATCH] Implement new deprecated warnings

---
 cmd/sing-box/cmd.go                           |  2 +-
 cmd/sing-box/cmd_check.go                     |  2 +-
 experimental/deprecated/constants.go          | 27 +++++++++++++++++++
 experimental/deprecated/manager.go            |  2 ++
 experimental/deprecated/{env.go => stderr.go} | 18 +++++++++----
 experimental/libbox/deprecated.go             |  3 ++-
 experimental/libbox/service.go                |  4 +--
 option/outbound.go                            | 11 ++++++++
 option/rule_action.go                         | 15 ++++++-----
 option/rule_dns.go                            | 13 ++++-----
 10 files changed, 74 insertions(+), 23 deletions(-)
 rename experimental/deprecated/{env.go => stderr.go} (59%)

diff --git a/cmd/sing-box/cmd.go b/cmd/sing-box/cmd.go
index b38d6518..dc7a8309 100644
--- a/cmd/sing-box/cmd.go
+++ b/cmd/sing-box/cmd.go
@@ -68,6 +68,6 @@ func preRun(cmd *cobra.Command, args []string) {
 	if len(configPaths) == 0 && len(configDirectories) == 0 {
 		configPaths = append(configPaths, "config.json")
 	}
-	globalCtx = service.ContextWith(globalCtx, deprecated.NewEnvManager(log.StdLogger()))
+	globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
 	globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry())
 }
diff --git a/cmd/sing-box/cmd_check.go b/cmd/sing-box/cmd_check.go
index 1beab954..29a39081 100644
--- a/cmd/sing-box/cmd_check.go
+++ b/cmd/sing-box/cmd_check.go
@@ -30,7 +30,7 @@ func check() error {
 	if err != nil {
 		return err
 	}
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(globalCtx)
 	instance, err := box.New(box.Options{
 		Context: ctx,
 		Options: options,
diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go
index 34ea5fc3..c830f49f 100644
--- a/experimental/deprecated/constants.go
+++ b/experimental/deprecated/constants.go
@@ -78,9 +78,36 @@ var OptionTUNAddressX = Note{
 	MigrationLink:     "https://sing-box.sagernet.org/migration/#tun-address-fields-are-merged",
 }
 
+var OptionSpecialOutbounds = Note{
+	Name:              "special-outbounds",
+	Description:       "legacy special outbounds",
+	DeprecatedVersion: "1.11.0",
+	ScheduledVersion:  "1.13.0",
+	MigrationLink:     "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions",
+}
+
+var OptionInboundOptions = Note{
+	Name:              "inbound-options",
+	Description:       "legacy inbound fields",
+	DeprecatedVersion: "1.11.0",
+	ScheduledVersion:  "1.13.0",
+	MigrationLink:     "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions",
+}
+
+var OptionLegacyDNSRouteOptions = Note{
+	Name:              "legacy-dns-route-options",
+	Description:       "legacy dns route options",
+	DeprecatedVersion: "1.11.0",
+	ScheduledVersion:  "1.12.0",
+	MigrationLink:     "https://sing-box.sagernet.org/migration/#migrate-legacy-dns-route-options-to-rule-actions",
+}
+
 var Options = []Note{
 	OptionBadMatchSource,
 	OptionGEOIP,
 	OptionGEOSITE,
 	OptionTUNAddressX,
+	OptionSpecialOutbounds,
+	OptionInboundOptions,
+	OptionLegacyDNSRouteOptions,
 }
diff --git a/experimental/deprecated/manager.go b/experimental/deprecated/manager.go
index d12acf48..48493589 100644
--- a/experimental/deprecated/manager.go
+++ b/experimental/deprecated/manager.go
@@ -2,6 +2,7 @@ package deprecated
 
 import (
 	"context"
+	"runtime/debug"
 
 	"github.com/sagernet/sing/service"
 )
@@ -13,6 +14,7 @@ type Manager interface {
 func Report(ctx context.Context, feature Note) {
 	manager := service.FromContext[Manager](ctx)
 	if manager == nil {
+		debug.PrintStack()
 		return
 	}
 	manager.ReportDeprecated(feature)
diff --git a/experimental/deprecated/env.go b/experimental/deprecated/stderr.go
similarity index 59%
rename from experimental/deprecated/env.go
rename to experimental/deprecated/stderr.go
index 113b8717..0826baf9 100644
--- a/experimental/deprecated/env.go
+++ b/experimental/deprecated/stderr.go
@@ -7,15 +7,23 @@ import (
 	"github.com/sagernet/sing/common/logger"
 )
 
-type envManager struct {
-	logger logger.Logger
+type stderrManager struct {
+	logger   logger.Logger
+	reported map[string]bool
 }
 
-func NewEnvManager(logger logger.Logger) Manager {
-	return &envManager{logger: logger}
+func NewStderrManager(logger logger.Logger) Manager {
+	return &stderrManager{
+		logger:   logger,
+		reported: make(map[string]bool),
+	}
 }
 
-func (f *envManager) ReportDeprecated(feature Note) {
+func (f *stderrManager) ReportDeprecated(feature Note) {
+	if f.reported[feature.Name] {
+		return
+	}
+	f.reported[feature.Name] = true
 	if !feature.Impending() {
 		f.logger.Warn(feature.MessageWithLink())
 		return
diff --git a/experimental/libbox/deprecated.go b/experimental/libbox/deprecated.go
index b953014f..d27be576 100644
--- a/experimental/libbox/deprecated.go
+++ b/experimental/libbox/deprecated.go
@@ -4,6 +4,7 @@ import (
 	"sync"
 
 	"github.com/sagernet/sing-box/experimental/deprecated"
+	"github.com/sagernet/sing/common"
 )
 
 var _ deprecated.Manager = (*deprecatedManager)(nil)
@@ -16,7 +17,7 @@ type deprecatedManager struct {
 func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) {
 	m.access.Lock()
 	defer m.access.Unlock()
-	m.features = append(m.features, feature)
+	m.features = common.Uniq(append(m.features, feature))
 }
 
 func (m *deprecatedManager) Get() []deprecated.Note {
diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go
index eadd4c27..dcb0370f 100644
--- a/experimental/libbox/service.go
+++ b/experimental/libbox/service.go
@@ -43,16 +43,16 @@ type BoxService struct {
 
 func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
 	ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry())
+	ctx = service.ContextWith[deprecated.Manager](ctx, new(deprecatedManager))
+	ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
 	options, err := parseConfig(ctx, configContent)
 	if err != nil {
 		return nil, err
 	}
 	runtimeDebug.FreeOSMemory()
 	ctx, cancel := context.WithCancel(ctx)
-	ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
 	urlTestHistoryStorage := urltest.NewHistoryStorage()
 	ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
-	ctx = service.ContextWith[deprecated.Manager](ctx, new(deprecatedManager))
 	platformWrapper := &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()}
 	ctx = service.ContextWith[platform.Interface](ctx, platformWrapper)
 	instance, err := box.New(box.Options{
diff --git a/option/outbound.go b/option/outbound.go
index 00a20aa5..1dddb354 100644
--- a/option/outbound.go
+++ b/option/outbound.go
@@ -3,6 +3,8 @@ package option
 import (
 	"context"
 
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/experimental/deprecated"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
 	"github.com/sagernet/sing/common/json/badjson"
@@ -35,6 +37,10 @@ func (h *Outbound) UnmarshalJSONContext(ctx context.Context, content []byte) err
 	if registry == nil {
 		return E.New("missing outbound options registry in context")
 	}
+	switch h.Type {
+	case C.TypeBlock, C.TypeDNS:
+		deprecated.Report(ctx, deprecated.OptionSpecialOutbounds)
+	}
 	options, loaded := registry.CreateOptions(h.Type)
 	if !loaded {
 		return E.New("unknown outbound type: ", h.Type)
@@ -43,6 +49,11 @@ func (h *Outbound) UnmarshalJSONContext(ctx context.Context, content []byte) err
 	if err != nil {
 		return err
 	}
+	if listenWrapper, isListen := options.(ListenOptionsWrapper); isListen {
+		if listenWrapper.TakeListenOptions().InboundOptions != (InboundOptions{}) {
+			deprecated.Report(ctx, deprecated.OptionInboundOptions)
+		}
+	}
 	h.Options = options
 	return nil
 }
diff --git a/option/rule_action.go b/option/rule_action.go
index f122f959..f95cd99d 100644
--- a/option/rule_action.go
+++ b/option/rule_action.go
@@ -1,10 +1,12 @@
 package option
 
 import (
+	"context"
 	"fmt"
 	"time"
 
 	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/experimental/deprecated"
 	dns "github.com/sagernet/sing-dns"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
@@ -107,7 +109,7 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) {
 	return badjson.MarshallObjects((_DNSRuleAction)(r), v)
 }
 
-func (r *DNSRuleAction) UnmarshalJSON(data []byte) error {
+func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) error {
 	err := json.Unmarshal(data, (*_DNSRuleAction)(r))
 	if err != nil {
 		return err
@@ -124,11 +126,7 @@ func (r *DNSRuleAction) UnmarshalJSON(data []byte) error {
 	default:
 		return E.New("unknown DNS rule action: " + r.Action)
 	}
-	if v == nil {
-		// check unknown fields
-		return json.UnmarshalDisallowUnknownFields(data, &_DNSRuleAction{})
-	}
-	return badjson.UnmarshallExcluded(data, (*_DNSRuleAction)(r), v)
+	return badjson.UnmarshallExcludedContext(ctx, data, (*_DNSRuleAction)(r), v)
 }
 
 type _RouteActionOptions struct {
@@ -178,7 +176,7 @@ type _DNSRouteActionOptions struct {
 
 type DNSRouteActionOptions _DNSRouteActionOptions
 
-func (r *DNSRouteActionOptions) UnmarshalJSON(data []byte) error {
+func (r *DNSRouteActionOptions) UnmarshalJSONContext(ctx context.Context, data []byte) error {
 	err := json.Unmarshal(data, (*_DNSRouteActionOptions)(r))
 	if err != nil {
 		return err
@@ -186,6 +184,9 @@ func (r *DNSRouteActionOptions) UnmarshalJSON(data []byte) error {
 	if r.Server == "" {
 		return E.New("missing server")
 	}
+	if r.DisableCache || r.RewriteTTL != nil || r.ClientSubnet != nil {
+		deprecated.Report(ctx, deprecated.OptionLegacyDNSRouteOptions)
+	}
 	return nil
 }
 
diff --git a/option/rule_dns.go b/option/rule_dns.go
index b987fc02..88b8aa91 100644
--- a/option/rule_dns.go
+++ b/option/rule_dns.go
@@ -1,6 +1,7 @@
 package option
 
 import (
+	"context"
 	"reflect"
 
 	C "github.com/sagernet/sing-box/constant"
@@ -32,7 +33,7 @@ func (r DNSRule) MarshalJSON() ([]byte, error) {
 	return badjson.MarshallObjects((_DNSRule)(r), v)
 }
 
-func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
+func (r *DNSRule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
 	err := json.Unmarshal(bytes, (*_DNSRule)(r))
 	if err != nil {
 		return err
@@ -47,7 +48,7 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
 	default:
 		return E.New("unknown rule type: " + r.Type)
 	}
-	err = badjson.UnmarshallExcluded(bytes, (*_DNSRule)(r), v)
+	err = badjson.UnmarshallExcludedContext(ctx, bytes, (*_DNSRule)(r), v)
 	if err != nil {
 		return err
 	}
@@ -115,12 +116,12 @@ func (r DefaultDNSRule) MarshalJSON() ([]byte, error) {
 	return badjson.MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction)
 }
 
-func (r *DefaultDNSRule) UnmarshalJSON(data []byte) error {
+func (r *DefaultDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error {
 	err := json.Unmarshal(data, &r.RawDefaultDNSRule)
 	if err != nil {
 		return err
 	}
-	return badjson.UnmarshallExcluded(data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
+	return badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
 }
 
 func (r DefaultDNSRule) IsValid() bool {
@@ -145,12 +146,12 @@ func (r *LogicalDNSRule) MarshalJSON() ([]byte, error) {
 	return badjson.MarshallObjects(r._LogicalDNSRule, r.DNSRuleAction)
 }
 
-func (r *LogicalDNSRule) UnmarshalJSON(data []byte) error {
+func (r *LogicalDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error {
 	err := json.Unmarshal(data, &r._LogicalDNSRule)
 	if err != nil {
 		return err
 	}
-	return badjson.UnmarshallExcluded(data, &r._LogicalDNSRule, &r.DNSRuleAction)
+	return badjson.UnmarshallExcludedContext(ctx, data, &r._LogicalDNSRule, &r.DNSRuleAction)
 }
 
 func (r *LogicalDNSRule) IsValid() bool {