Browse Source

Add `store_mode` and platform Clash mode selector

世界 2 years ago
parent
commit
43f72a6419

+ 3 - 0
adapter/experimental.go

@@ -12,6 +12,7 @@ type ClashServer interface {
 	Service
 	Service
 	PreStarter
 	PreStarter
 	Mode() string
 	Mode() string
+	ModeList() []string
 	StoreSelected() bool
 	StoreSelected() bool
 	StoreFakeIP() bool
 	StoreFakeIP() bool
 	CacheFile() ClashCacheFile
 	CacheFile() ClashCacheFile
@@ -21,6 +22,8 @@ type ClashServer interface {
 }
 }
 
 
 type ClashCacheFile interface {
 type ClashCacheFile interface {
+	LoadMode() string
+	StoreMode(mode string) error
 	LoadSelected(group string) string
 	LoadSelected(group string) string
 	StoreSelected(group string, selected string) error
 	StoreSelected(group string, selected string) error
 	LoadGroupExpand(group string) (isExpand bool, loaded bool)
 	LoadGroupExpand(group string) (isExpand bool, loaded bool)

+ 1 - 0
adapter/router.go

@@ -32,6 +32,7 @@ type Router interface {
 	Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
 	Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
 	Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
 	Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
 	LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
 	LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
+	ClearDNSCache()
 
 
 	InterfaceFinder() control.InterfaceFinder
 	InterfaceFinder() control.InterfaceFinder
 	UpdateInterfaces() error
 	UpdateInterfaces() error

+ 3 - 1
box.go

@@ -145,7 +145,9 @@ func New(options Options) (*Box, error) {
 	preServices := make(map[string]adapter.Service)
 	preServices := make(map[string]adapter.Service)
 	postServices := make(map[string]adapter.Service)
 	postServices := make(map[string]adapter.Service)
 	if needClashAPI {
 	if needClashAPI {
-		clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(experimentalOptions.ClashAPI))
+		clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
+		clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
+		clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions)
 		if err != nil {
 		if err != nil {
 			return nil, E.Cause(err, "create clash api server")
 			return nil, E.Cause(err, "create clash api server")
 		}
 		}

+ 14 - 16
common/urltest/urltest.go

@@ -10,7 +10,6 @@ import (
 
 
 	M "github.com/sagernet/sing/common/metadata"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
-	"github.com/sagernet/sing/common/x/list"
 )
 )
 
 
 type History struct {
 type History struct {
@@ -21,7 +20,7 @@ type History struct {
 type HistoryStorage struct {
 type HistoryStorage struct {
 	access       sync.RWMutex
 	access       sync.RWMutex
 	delayHistory map[string]*History
 	delayHistory map[string]*History
-	callbacks    list.List[func()]
+	updateHook   chan<- struct{}
 }
 }
 
 
 func NewHistoryStorage() *HistoryStorage {
 func NewHistoryStorage() *HistoryStorage {
@@ -30,16 +29,8 @@ func NewHistoryStorage() *HistoryStorage {
 	}
 	}
 }
 }
 
 
-func (s *HistoryStorage) AddListener(listener func()) *list.Element[func()] {
-	s.access.Lock()
-	defer s.access.Unlock()
-	return s.callbacks.PushBack(listener)
-}
-
-func (s *HistoryStorage) RemoveListener(element *list.Element[func()]) {
-	s.access.Lock()
-	defer s.access.Unlock()
-	s.callbacks.Remove(element)
+func (s *HistoryStorage) SetHook(hook chan<- struct{}) {
+	s.updateHook = hook
 }
 }
 
 
 func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
 func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
@@ -66,13 +57,20 @@ func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
 }
 }
 
 
 func (s *HistoryStorage) notifyUpdated() {
 func (s *HistoryStorage) notifyUpdated() {
-	s.access.RLock()
-	defer s.access.RUnlock()
-	for element := s.callbacks.Front(); element != nil; element = element.Next() {
-		element.Value()
+	updateHook := s.updateHook
+	if updateHook != nil {
+		select {
+		case updateHook <- struct{}{}:
+		default:
+		}
 	}
 	}
 }
 }
 
 
+func (s *HistoryStorage) Close() error {
+	s.updateHook = nil
+	return nil
+}
+
 func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) {
 func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) {
 	if link == "" {
 	if link == "" {
 		link = "https://www.gstatic.com/generate_204"
 		link = "https://www.gstatic.com/generate_204"

+ 10 - 0
docs/configuration/experimental/index.md

@@ -12,7 +12,9 @@
       "external_ui_download_detour": "",
       "external_ui_download_detour": "",
       "secret": "",
       "secret": "",
       "default_mode": "",
       "default_mode": "",
+      "store_mode": false,
       "store_selected": false,
       "store_selected": false,
+      "store_fakeip": false,
       "cache_file": "",
       "cache_file": "",
       "cache_id": ""
       "cache_id": ""
     },
     },
@@ -80,6 +82,10 @@ Default mode in clash, `rule` will be used if empty.
 
 
 This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item.
 This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item.
 
 
+#### store_mode
+
+Store Clash mode in cache file.
+
 #### store_selected
 #### store_selected
 
 
 !!! note ""
 !!! note ""
@@ -88,6 +94,10 @@ This setting has no direct effect, but can be used in routing and DNS rules via
 
 
 Store selected outbound for the `Selector` outbound in cache file.
 Store selected outbound for the `Selector` outbound in cache file.
 
 
+#### store_fakeip
+
+Store fakeip in cache file.
+
 #### cache_file
 #### cache_file
 
 
 Cache file path, `cache.db` will be used if empty.
 Cache file path, `cache.db` will be used if empty.

+ 10 - 0
docs/configuration/experimental/index.zh.md

@@ -12,7 +12,9 @@
       "external_ui_download_detour": "",
       "external_ui_download_detour": "",
       "secret": "",
       "secret": "",
       "default_mode": "",
       "default_mode": "",
+      "store_mode": false,
       "store_selected": false,
       "store_selected": false,
+      "store_fakeip": false,
       "cache_file": "",
       "cache_file": "",
       "cache_id": ""
       "cache_id": ""
     },
     },
@@ -78,6 +80,10 @@ Clash 中的默认模式,默认使用 `rule`。
 
 
 此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。
 此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。
 
 
+#### store_mode
+
+将 Clash 模式存储在缓存文件中。
+
 #### store_selected
 #### store_selected
 
 
 !!! note ""
 !!! note ""
@@ -86,6 +92,10 @@ Clash 中的默认模式,默认使用 `rule`。
 
 
 将 `Selector` 中出站的选定的目标出站存储在缓存文件中。
 将 `Selector` 中出站的选定的目标出站存储在缓存文件中。
 
 
+#### store_fakeip
+
+将 fakeip 存储在缓存文件中。
+
 #### cache_file
 #### cache_file
 
 
 缓存文件路径,默认使用`cache.db`。
 缓存文件路径,默认使用`cache.db`。

+ 26 - 0
experimental/clashapi.go

@@ -7,6 +7,7 @@ import (
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
 )
 )
 
 
 type ClashServerConstructor = func(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error)
 type ClashServerConstructor = func(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error)
@@ -23,3 +24,28 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O
 	}
 	}
 	return clashServerConstructor(ctx, router, logFactory, options)
 	return clashServerConstructor(ctx, router, logFactory, options)
 }
 }
+
+func CalculateClashModeList(options option.Options) []string {
+	var clashMode []string
+	for _, dnsRule := range common.PtrValueOrDefault(options.DNS).Rules {
+		if dnsRule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, dnsRule.DefaultOptions.ClashMode) {
+			clashMode = append(clashMode, dnsRule.DefaultOptions.ClashMode)
+		}
+		for _, defaultRule := range dnsRule.LogicalOptions.Rules {
+			if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) {
+				clashMode = append(clashMode, defaultRule.ClashMode)
+			}
+		}
+	}
+	for _, rule := range common.PtrValueOrDefault(options.Route).Rules {
+		if rule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, rule.DefaultOptions.ClashMode) {
+			clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
+		}
+		for _, defaultRule := range rule.LogicalOptions.Rules {
+			if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) {
+				clashMode = append(clashMode, defaultRule.ClashMode)
+			}
+		}
+	}
+	return clashMode
+}

+ 45 - 2
experimental/clashapi/cachefile/cache.go

@@ -8,6 +8,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing/common"
 
 
 	"go.etcd.io/bbolt"
 	"go.etcd.io/bbolt"
 )
 )
@@ -15,6 +16,15 @@ import (
 var (
 var (
 	bucketSelected = []byte("selected")
 	bucketSelected = []byte("selected")
 	bucketExpand   = []byte("group_expand")
 	bucketExpand   = []byte("group_expand")
+	bucketMode     = []byte("clash_mode")
+
+	bucketNameList = []string{
+		string(bucketSelected),
+		string(bucketExpand),
+		string(bucketMode),
+	}
+
+	cacheIDDefault = []byte("default")
 )
 )
 
 
 var _ adapter.ClashCacheFile = (*CacheFile)(nil)
 var _ adapter.ClashCacheFile = (*CacheFile)(nil)
@@ -52,14 +62,14 @@ func Open(path string, cacheID string) (*CacheFile, error) {
 			if name[0] == 0 {
 			if name[0] == 0 {
 				return b.ForEachBucket(func(k []byte) error {
 				return b.ForEachBucket(func(k []byte) error {
 					bucketName := string(k)
 					bucketName := string(k)
-					if !(bucketName == string(bucketSelected) || bucketName == string(bucketExpand)) {
+					if !(common.Contains(bucketNameList, bucketName)) {
 						_ = b.DeleteBucket(name)
 						_ = b.DeleteBucket(name)
 					}
 					}
 					return nil
 					return nil
 				})
 				})
 			} else {
 			} else {
 				bucketName := string(name)
 				bucketName := string(name)
-				if !(bucketName == string(bucketSelected) || bucketName == string(bucketExpand) || strings.HasPrefix(bucketName, fakeipBucketPrefix)) {
+				if !(common.Contains(bucketNameList, bucketName) || strings.HasPrefix(bucketName, fakeipBucketPrefix)) {
 					_ = tx.DeleteBucket(name)
 					_ = tx.DeleteBucket(name)
 				}
 				}
 			}
 			}
@@ -78,6 +88,39 @@ func Open(path string, cacheID string) (*CacheFile, error) {
 	}, nil
 	}, nil
 }
 }
 
 
+func (c *CacheFile) LoadMode() string {
+	var mode string
+	c.DB.View(func(t *bbolt.Tx) error {
+		bucket := t.Bucket(bucketMode)
+		if bucket == nil {
+			return nil
+		}
+		var modeBytes []byte
+		if len(c.cacheID) > 0 {
+			modeBytes = bucket.Get(c.cacheID)
+		} else {
+			modeBytes = bucket.Get(cacheIDDefault)
+		}
+		mode = string(modeBytes)
+		return nil
+	})
+	return mode
+}
+
+func (c *CacheFile) StoreMode(mode string) error {
+	return c.DB.Batch(func(t *bbolt.Tx) error {
+		bucket, err := t.CreateBucketIfNotExists(bucketMode)
+		if err != nil {
+			return err
+		}
+		if len(c.cacheID) > 0 {
+			return bucket.Put(c.cacheID, []byte(mode))
+		} else {
+			return bucket.Put(cacheIDDefault, []byte(mode))
+		}
+	})
+}
+
 func (c *CacheFile) bucket(t *bbolt.Tx, key []byte) *bbolt.Bucket {
 func (c *CacheFile) bucket(t *bbolt.Tx, key []byte) *bbolt.Bucket {
 	if c.cacheID == nil {
 	if c.cacheID == nil {
 		return t.Bucket(key)
 		return t.Bucket(key)

+ 4 - 9
experimental/clashapi/configs.go

@@ -2,7 +2,6 @@ package clashapi
 
 
 import (
 import (
 	"net/http"
 	"net/http"
-	"strings"
 
 
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 
 
@@ -10,11 +9,11 @@ import (
 	"github.com/go-chi/render"
 	"github.com/go-chi/render"
 )
 )
 
 
-func configRouter(server *Server, logFactory log.Factory, logger log.Logger) http.Handler {
+func configRouter(server *Server, logFactory log.Factory) http.Handler {
 	r := chi.NewRouter()
 	r := chi.NewRouter()
 	r.Get("/", getConfigs(server, logFactory))
 	r.Get("/", getConfigs(server, logFactory))
 	r.Put("/", updateConfigs)
 	r.Put("/", updateConfigs)
-	r.Patch("/", patchConfigs(server, logger))
+	r.Patch("/", patchConfigs(server))
 	return r
 	return r
 }
 }
 
 
@@ -48,7 +47,7 @@ func getConfigs(server *Server, logFactory log.Factory) func(w http.ResponseWrit
 	}
 	}
 }
 }
 
 
-func patchConfigs(server *Server, logger log.Logger) func(w http.ResponseWriter, r *http.Request) {
+func patchConfigs(server *Server) func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var newConfig configSchema
 		var newConfig configSchema
 		err := render.DecodeJSON(r.Body, &newConfig)
 		err := render.DecodeJSON(r.Body, &newConfig)
@@ -58,11 +57,7 @@ func patchConfigs(server *Server, logger log.Logger) func(w http.ResponseWriter,
 			return
 			return
 		}
 		}
 		if newConfig.Mode != "" {
 		if newConfig.Mode != "" {
-			mode := strings.ToLower(newConfig.Mode)
-			if server.mode != mode {
-				server.mode = mode
-				logger.Info("updated mode: ", mode)
-			}
+			server.SetMode(newConfig.Mode)
 		}
 		}
 		render.NoContent(w, r)
 		render.NoContent(w, r)
 	}
 	}

+ 61 - 6
experimental/clashapi/server.go

@@ -46,6 +46,9 @@ type Server struct {
 	trafficManager *trafficontrol.Manager
 	trafficManager *trafficontrol.Manager
 	urlTestHistory *urltest.HistoryStorage
 	urlTestHistory *urltest.HistoryStorage
 	mode           string
 	mode           string
+	modeList       []string
+	modeUpdateHook chan<- struct{}
+	storeMode      bool
 	storeSelected  bool
 	storeSelected  bool
 	storeFakeIP    bool
 	storeFakeIP    bool
 	cacheFilePath  string
 	cacheFilePath  string
@@ -70,9 +73,10 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
 			Handler: chiRouter,
 			Handler: chiRouter,
 		},
 		},
 		trafficManager:           trafficManager,
 		trafficManager:           trafficManager,
-		mode:                     strings.ToLower(options.DefaultMode),
-		storeSelected:            options.StoreSelected,
+		modeList:                 options.ModeList,
 		externalController:       options.ExternalController != "",
 		externalController:       options.ExternalController != "",
+		storeMode:                options.StoreMode,
+		storeSelected:            options.StoreSelected,
 		storeFakeIP:              options.StoreFakeIP,
 		storeFakeIP:              options.StoreFakeIP,
 		externalUIDownloadURL:    options.ExternalUIDownloadURL,
 		externalUIDownloadURL:    options.ExternalUIDownloadURL,
 		externalUIDownloadDetour: options.ExternalUIDownloadDetour,
 		externalUIDownloadDetour: options.ExternalUIDownloadDetour,
@@ -81,10 +85,15 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
 	if server.urlTestHistory == nil {
 	if server.urlTestHistory == nil {
 		server.urlTestHistory = urltest.NewHistoryStorage()
 		server.urlTestHistory = urltest.NewHistoryStorage()
 	}
 	}
-	if server.mode == "" {
-		server.mode = "rule"
+	defaultMode := "Rule"
+	if options.DefaultMode != "" {
+		defaultMode = options.DefaultMode
+	}
+	if !common.Contains(server.modeList, defaultMode) {
+		server.modeList = append(server.modeList, defaultMode)
 	}
 	}
-	if options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" {
+	server.mode = defaultMode
+	if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" {
 		cachePath := os.ExpandEnv(options.CacheFile)
 		cachePath := os.ExpandEnv(options.CacheFile)
 		if cachePath == "" {
 		if cachePath == "" {
 			cachePath = "cache.db"
 			cachePath = "cache.db"
@@ -110,7 +119,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
 		r.Get("/logs", getLogs(logFactory))
 		r.Get("/logs", getLogs(logFactory))
 		r.Get("/traffic", traffic(trafficManager))
 		r.Get("/traffic", traffic(trafficManager))
 		r.Get("/version", version)
 		r.Get("/version", version)
-		r.Mount("/configs", configRouter(server, logFactory, server.logger))
+		r.Mount("/configs", configRouter(server, logFactory))
 		r.Mount("/proxies", proxyRouter(server, router))
 		r.Mount("/proxies", proxyRouter(server, router))
 		r.Mount("/rules", ruleRouter(router))
 		r.Mount("/rules", ruleRouter(router))
 		r.Mount("/connections", connectionRouter(router, trafficManager))
 		r.Mount("/connections", connectionRouter(router, trafficManager))
@@ -143,6 +152,14 @@ func (s *Server) PreStart() error {
 			return E.Cause(err, "open cache file")
 			return E.Cause(err, "open cache file")
 		}
 		}
 		s.cacheFile = cacheFile
 		s.cacheFile = cacheFile
+		if s.storeMode {
+			mode := s.cacheFile.LoadMode()
+			if common.Any(s.modeList, func(it string) bool {
+				return strings.EqualFold(it, mode)
+			}) {
+				s.mode = mode
+			}
+		}
 	}
 	}
 	return nil
 	return nil
 }
 }
@@ -170,6 +187,7 @@ func (s *Server) Close() error {
 		common.PtrOrNil(s.httpServer),
 		common.PtrOrNil(s.httpServer),
 		s.trafficManager,
 		s.trafficManager,
 		s.cacheFile,
 		s.cacheFile,
+		s.urlTestHistory,
 	)
 	)
 }
 }
 
 
@@ -177,6 +195,43 @@ func (s *Server) Mode() string {
 	return s.mode
 	return s.mode
 }
 }
 
 
+func (s *Server) ModeList() []string {
+	return s.modeList
+}
+
+func (s *Server) SetModeUpdateHook(hook chan<- struct{}) {
+	s.modeUpdateHook = hook
+}
+
+func (s *Server) SetMode(newMode string) {
+	if !common.Contains(s.modeList, newMode) {
+		newMode = common.Find(s.modeList, func(it string) bool {
+			return strings.EqualFold(it, newMode)
+		})
+	}
+	if !common.Contains(s.modeList, newMode) {
+		return
+	}
+	if newMode == s.mode {
+		return
+	}
+	s.mode = newMode
+	if s.modeUpdateHook != nil {
+		select {
+		case s.modeUpdateHook <- struct{}{}:
+		default:
+		}
+	}
+	s.router.ClearDNSCache()
+	if s.storeMode {
+		err := s.cacheFile.StoreMode(newMode)
+		if err != nil {
+			s.logger.Error(E.Cause(err, "save mode"))
+		}
+	}
+	s.logger.Info("updated mode: ", newMode)
+}
+
 func (s *Server) StoreSelected() bool {
 func (s *Server) StoreSelected() bool {
 	return s.storeSelected
 	return s.storeSelected
 }
 }

+ 2 - 0
experimental/libbox/command.go

@@ -9,4 +9,6 @@ const (
 	CommandSelectOutbound
 	CommandSelectOutbound
 	CommandURLTest
 	CommandURLTest
 	CommandGroupExpand
 	CommandGroupExpand
+	CommandClashMode
+	CommandSetClashMode
 )
 )

+ 135 - 0
experimental/libbox/command_clash_mode.go

@@ -0,0 +1,135 @@
+package libbox
+
+import (
+	"encoding/binary"
+	"io"
+	"net"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/experimental/clashapi"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/rw"
+)
+
+func (c *CommandClient) SetClashMode(newMode string) error {
+	conn, err := c.directConnect()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+	err = binary.Write(conn, binary.BigEndian, uint8(CommandSetClashMode))
+	if err != nil {
+		return err
+	}
+	err = rw.WriteVString(conn, newMode)
+	if err != nil {
+		return err
+	}
+	return readError(conn)
+}
+
+func (s *CommandServer) handleSetClashMode(conn net.Conn) error {
+	defer conn.Close()
+	newMode, err := rw.ReadVString(conn)
+	if err != nil {
+		return err
+	}
+	service := s.service
+	if service == nil {
+		return writeError(conn, E.New("service not ready"))
+	}
+	clashServer := service.instance.Router().ClashServer()
+	if clashServer == nil {
+		return writeError(conn, E.New("Clash API disabled"))
+	}
+	clashServer.(*clashapi.Server).SetMode(newMode)
+	return writeError(conn, nil)
+}
+
+func (c *CommandClient) handleModeConn(conn net.Conn) {
+	defer conn.Close()
+
+	for {
+		newMode, err := rw.ReadVString(conn)
+		if err != nil {
+			c.handler.Disconnected(err.Error())
+			return
+		}
+		c.handler.UpdateClashMode(newMode)
+	}
+}
+
+func (s *CommandServer) handleModeConn(conn net.Conn) error {
+	defer conn.Close()
+	ctx := connKeepAlive(conn)
+	for s.service == nil {
+		select {
+		case <-time.After(time.Second):
+			continue
+		case <-ctx.Done():
+			return ctx.Err()
+		}
+	}
+	clashServer := s.service.instance.Router().ClashServer()
+	if clashServer == nil {
+		defer conn.Close()
+		return binary.Write(conn, binary.BigEndian, uint16(0))
+	}
+	err := writeClashModeList(conn, clashServer)
+	if err != nil {
+		return err
+	}
+	for {
+		select {
+		case <-s.modeUpdate:
+			err = rw.WriteVString(conn, clashServer.Mode())
+			if err != nil {
+				return err
+			}
+		case <-ctx.Done():
+			return ctx.Err()
+		}
+	}
+}
+
+func readClashModeList(reader io.Reader) (modeList []string, currentMode string, err error) {
+	var modeListLength uint16
+	err = binary.Read(reader, binary.BigEndian, &modeListLength)
+	if err != nil {
+		return
+	}
+	if modeListLength == 0 {
+		return
+	}
+	modeList = make([]string, modeListLength)
+	for i := 0; i < int(modeListLength); i++ {
+		modeList[i], err = rw.ReadVString(reader)
+		if err != nil {
+			return
+		}
+	}
+	currentMode, err = rw.ReadVString(reader)
+	return
+}
+
+func writeClashModeList(writer io.Writer, clashServer adapter.ClashServer) error {
+	modeList := clashServer.ModeList()
+	err := binary.Write(writer, binary.BigEndian, uint16(len(modeList)))
+	if err != nil {
+		return err
+	}
+	if len(modeList) > 0 {
+		for _, mode := range modeList {
+			err = rw.WriteVString(writer, mode)
+			if err != nil {
+				return err
+			}
+		}
+		err = rw.WriteVString(writer, clashServer.Mode())
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 20 - 0
experimental/libbox/command_client.go

@@ -3,6 +3,7 @@ package libbox
 import (
 import (
 	"encoding/binary"
 	"encoding/binary"
 	"net"
 	"net"
+	"os"
 	"path/filepath"
 	"path/filepath"
 
 
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common"
@@ -26,6 +27,8 @@ type CommandClientHandler interface {
 	WriteLog(message string)
 	WriteLog(message string)
 	WriteStatus(message *StatusMessage)
 	WriteStatus(message *StatusMessage)
 	WriteGroups(message OutboundGroupIterator)
 	WriteGroups(message OutboundGroupIterator)
+	InitializeClashMode(modeList StringIterator, currentMode string)
+	UpdateClashMode(newMode string)
 }
 }
 
 
 func NewStandaloneCommandClient() *CommandClient {
 func NewStandaloneCommandClient() *CommandClient {
@@ -79,6 +82,23 @@ func (c *CommandClient) Connect() error {
 		}
 		}
 		c.handler.Connected()
 		c.handler.Connected()
 		go c.handleGroupConn(conn)
 		go c.handleGroupConn(conn)
+	case CommandClashMode:
+		var (
+			modeList    []string
+			currentMode string
+		)
+		modeList, currentMode, err = readClashModeList(conn)
+		if err != nil {
+			return err
+		}
+		c.handler.Connected()
+		c.handler.InitializeClashMode(newIterator(modeList), currentMode)
+		if len(modeList) == 0 {
+			conn.Close()
+			c.handler.Disconnected(os.ErrInvalid.Error())
+			return nil
+		}
+		go c.handleModeConn(conn)
 	}
 	}
 	return nil
 	return nil
 }
 }

+ 3 - 0
experimental/libbox/command_group.go

@@ -199,6 +199,9 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
 			}
 			}
 			group.items = append(group.items, &item)
 			group.items = append(group.items, &item)
 		}
 		}
+		if len(group.items) < 2 {
+			continue
+		}
 		groups = append(groups, group)
 		groups = append(groups, group)
 	}
 	}
 
 

+ 11 - 8
experimental/libbox/command_server.go

@@ -8,6 +8,7 @@ import (
 	"sync"
 	"sync"
 
 
 	"github.com/sagernet/sing-box/common/urltest"
 	"github.com/sagernet/sing-box/common/urltest"
+	"github.com/sagernet/sing-box/experimental/clashapi"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/debug"
 	"github.com/sagernet/sing/common/debug"
@@ -28,8 +29,8 @@ type CommandServer struct {
 	observer   *observable.Observer[string]
 	observer   *observable.Observer[string]
 	service    *BoxService
 	service    *BoxService
 
 
-	urlTestListener *list.Element[func()]
-	urlTestUpdate   chan struct{}
+	urlTestUpdate chan struct{}
+	modeUpdate    chan struct{}
 }
 }
 
 
 type CommandServerHandler interface {
 type CommandServerHandler interface {
@@ -43,20 +44,18 @@ func NewCommandServer(handler CommandServerHandler, maxLines int32) *CommandServ
 		maxLines:      int(maxLines),
 		maxLines:      int(maxLines),
 		subscriber:    observable.NewSubscriber[string](128),
 		subscriber:    observable.NewSubscriber[string](128),
 		urlTestUpdate: make(chan struct{}, 1),
 		urlTestUpdate: make(chan struct{}, 1),
+		modeUpdate:    make(chan struct{}, 1),
 	}
 	}
 	server.observer = observable.NewObserver[string](server.subscriber, 64)
 	server.observer = observable.NewObserver[string](server.subscriber, 64)
 	return server
 	return server
 }
 }
 
 
 func (s *CommandServer) SetService(newService *BoxService) {
 func (s *CommandServer) SetService(newService *BoxService) {
-	if s.service != nil && s.listener != nil {
-		service.PtrFromContext[urltest.HistoryStorage](s.service.ctx).RemoveListener(s.urlTestListener)
-		s.urlTestListener = nil
-	}
-	s.service = newService
 	if newService != nil {
 	if newService != nil {
-		s.urlTestListener = service.PtrFromContext[urltest.HistoryStorage](newService.ctx).AddListener(s.notifyURLTestUpdate)
+		service.PtrFromContext[urltest.HistoryStorage](newService.ctx).SetHook(s.urlTestUpdate)
+		newService.instance.Router().ClashServer().(*clashapi.Server).SetModeUpdateHook(s.modeUpdate)
 	}
 	}
+	s.service = newService
 	s.notifyURLTestUpdate()
 	s.notifyURLTestUpdate()
 }
 }
 
 
@@ -156,6 +155,10 @@ func (s *CommandServer) handleConnection(conn net.Conn) error {
 		return s.handleURLTest(conn)
 		return s.handleURLTest(conn)
 	case CommandGroupExpand:
 	case CommandGroupExpand:
 		return s.handleSetGroupExpand(conn)
 		return s.handleSetGroupExpand(conn)
+	case CommandClashMode:
+		return s.handleModeConn(conn)
+	case CommandSetClashMode:
+		return s.handleSetClashMode(conn)
 	default:
 	default:
 		return E.New("unknown command: ", command)
 		return E.New("unknown command: ", command)
 	}
 	}

+ 3 - 0
experimental/libbox/dns.go

@@ -49,6 +49,9 @@ func (p *platformLocalDNSTransport) Start() error {
 	return nil
 	return nil
 }
 }
 
 
+func (p *platformLocalDNSTransport) Reset() {
+}
+
 func (p *platformLocalDNSTransport) Close() error {
 func (p *platformLocalDNSTransport) Close() error {
 	return nil
 	return nil
 }
 }

+ 1 - 0
experimental/libbox/platform.go

@@ -19,6 +19,7 @@ type PlatformInterface interface {
 	UsePlatformInterfaceGetter() bool
 	UsePlatformInterfaceGetter() bool
 	GetInterfaces() (NetworkInterfaceIterator, error)
 	GetInterfaces() (NetworkInterfaceIterator, error)
 	UnderNetworkExtension() bool
 	UnderNetworkExtension() bool
+	ClearDNSCache()
 }
 }
 
 
 type TunInterface interface {
 type TunInterface interface {

+ 1 - 0
experimental/libbox/platform/interface.go

@@ -23,6 +23,7 @@ type Interface interface {
 	UsePlatformInterfaceGetter() bool
 	UsePlatformInterfaceGetter() bool
 	Interfaces() ([]NetworkInterface, error)
 	Interfaces() ([]NetworkInterface, error)
 	UnderNetworkExtension() bool
 	UnderNetworkExtension() bool
+	ClearDNSCache()
 	process.Searcher
 	process.Searcher
 	io.Writer
 	io.Writer
 }
 }

+ 19 - 11
experimental/libbox/service.go

@@ -25,10 +25,11 @@ import (
 )
 )
 
 
 type BoxService struct {
 type BoxService struct {
-	ctx          context.Context
-	cancel       context.CancelFunc
-	instance     *box.Box
-	pauseManager pause.Manager
+	ctx                   context.Context
+	cancel                context.CancelFunc
+	instance              *box.Box
+	pauseManager          pause.Manager
+	urlTestHistoryStorage *urltest.HistoryStorage
 }
 }
 
 
 func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
 func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
@@ -39,9 +40,10 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
 	runtimeDebug.FreeOSMemory()
 	runtimeDebug.FreeOSMemory()
 	ctx, cancel := context.WithCancel(context.Background())
 	ctx, cancel := context.WithCancel(context.Background())
 	ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
 	ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
-	ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage())
-	sleepManager := pause.NewDefaultManager(ctx)
-	ctx = pause.ContextWithManager(ctx, sleepManager)
+	urlTestHistoryStorage := urltest.NewHistoryStorage()
+	ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
+	pauseManager := pause.NewDefaultManager(ctx)
+	ctx = pause.ContextWithManager(ctx, pauseManager)
 	instance, err := box.New(box.Options{
 	instance, err := box.New(box.Options{
 		Context:           ctx,
 		Context:           ctx,
 		Options:           options,
 		Options:           options,
@@ -53,10 +55,11 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
 	}
 	}
 	runtimeDebug.FreeOSMemory()
 	runtimeDebug.FreeOSMemory()
 	return &BoxService{
 	return &BoxService{
-		ctx:          ctx,
-		cancel:       cancel,
-		instance:     instance,
-		pauseManager: sleepManager,
+		ctx:                   ctx,
+		cancel:                cancel,
+		instance:              instance,
+		urlTestHistoryStorage: urlTestHistoryStorage,
+		pauseManager:          pauseManager,
 	}, nil
 	}, nil
 }
 }
 
 
@@ -66,6 +69,7 @@ func (s *BoxService) Start() error {
 
 
 func (s *BoxService) Close() error {
 func (s *BoxService) Close() error {
 	s.cancel()
 	s.cancel()
+	s.urlTestHistoryStorage.Close()
 	return s.instance.Close()
 	return s.instance.Close()
 }
 }
 
 
@@ -194,3 +198,7 @@ func (w *platformInterfaceWrapper) Interfaces() ([]platform.NetworkInterface, er
 func (w *platformInterfaceWrapper) UnderNetworkExtension() bool {
 func (w *platformInterfaceWrapper) UnderNetworkExtension() bool {
 	return w.iif.UnderNetworkExtension()
 	return w.iif.UnderNetworkExtension()
 }
 }
+
+func (w *platformInterfaceWrapper) ClearDNSCache() {
+	w.iif.ClearDNSCache()
+}

+ 2 - 2
go.mod

@@ -25,8 +25,8 @@ require (
 	github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2
 	github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2
 	github.com/sagernet/quic-go v0.0.0-20230824033040-30ef72e3be3e
 	github.com/sagernet/quic-go v0.0.0-20230824033040-30ef72e3be3e
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
-	github.com/sagernet/sing v0.2.10-0.20230821073500-620f3a3b882d
-	github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659
+	github.com/sagernet/sing v0.2.10-0.20230824115837-8d731e68853a
+	github.com/sagernet/sing-dns v0.1.9-0.20230824120133-4d5cbceb40c1
 	github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c
 	github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c
 	github.com/sagernet/sing-shadowsocks v0.2.4
 	github.com/sagernet/sing-shadowsocks v0.2.4
 	github.com/sagernet/sing-shadowsocks2 v0.1.3
 	github.com/sagernet/sing-shadowsocks2 v0.1.3

+ 4 - 4
go.sum

@@ -113,10 +113,10 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
 github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
-github.com/sagernet/sing v0.2.10-0.20230821073500-620f3a3b882d h1:4kgoOCE48CuQcBUcoRnE0QTPXkl8yM8i7Nipmzp/978=
-github.com/sagernet/sing v0.2.10-0.20230821073500-620f3a3b882d/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
-github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659 h1:1DAKccGNqTYJ8nsBR765FS0LVBVXfuFlFAHqKsGN3EI=
-github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659/go.mod h1:W7GHTZFS8RkoLI3bA2LFY27/0E+uoQESWtMFLepO/JA=
+github.com/sagernet/sing v0.2.10-0.20230824115837-8d731e68853a h1:eV4HEz9NP7eAlQ/IHD6OF2VVM6ke4Vw6htuSAsvgtDk=
+github.com/sagernet/sing v0.2.10-0.20230824115837-8d731e68853a/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
+github.com/sagernet/sing-dns v0.1.9-0.20230824120133-4d5cbceb40c1 h1:5w+jXz8y/8UQAxO74TjftN5okYkpg5mGvVxXunlKdqI=
+github.com/sagernet/sing-dns v0.1.9-0.20230824120133-4d5cbceb40c1/go.mod h1:Kg98PBJEg/08jsNFtmZWmPomhskn9Ausn50ecNm4M+8=
 github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c h1:35/FowAvt3Z62mck0TXzVc4jS5R5CWq62qcV2P1cp0I=
 github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c h1:35/FowAvt3Z62mck0TXzVc4jS5R5CWq62qcV2P1cp0I=
 github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c/go.mod h1:TKxqIvfQQgd36jp2tzsPavGjYTVZilV+atip1cssjIY=
 github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c/go.mod h1:TKxqIvfQQgd36jp2tzsPavGjYTVZilV+atip1cssjIY=
 github.com/sagernet/sing-shadowsocks v0.2.4 h1:s/CqXlvFAZhlIoHWUwPw5CoNnQ9Ibki9pckjuugtVfY=
 github.com/sagernet/sing-shadowsocks v0.2.4 h1:s/CqXlvFAZhlIoHWUwPw5CoNnQ9Ibki9pckjuugtVfY=

+ 3 - 0
option/clash.go

@@ -7,10 +7,13 @@ type ClashAPIOptions struct {
 	ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"`
 	ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"`
 	Secret                   string `json:"secret,omitempty"`
 	Secret                   string `json:"secret,omitempty"`
 	DefaultMode              string `json:"default_mode,omitempty"`
 	DefaultMode              string `json:"default_mode,omitempty"`
+	StoreMode                bool   `json:"store_mode,omitempty"`
 	StoreSelected            bool   `json:"store_selected,omitempty"`
 	StoreSelected            bool   `json:"store_selected,omitempty"`
 	StoreFakeIP              bool   `json:"store_fakeip,omitempty"`
 	StoreFakeIP              bool   `json:"store_fakeip,omitempty"`
 	CacheFile                string `json:"cache_file,omitempty"`
 	CacheFile                string `json:"cache_file,omitempty"`
 	CacheID                  string `json:"cache_id,omitempty"`
 	CacheID                  string `json:"cache_id,omitempty"`
+
+	ModeList []string `json:"-"`
 }
 }
 
 
 type SelectorOutboundOptions struct {
 type SelectorOutboundOptions struct {

+ 5 - 8
route/router.go

@@ -1005,14 +1005,7 @@ func (r *Router) notifyNetworkUpdate(event int) {
 		}
 		}
 	}
 	}
 
 
-	conntrack.Close()
-
-	for _, outbound := range r.outbounds {
-		listener, isListener := outbound.(adapter.InterfaceUpdateListener)
-		if isListener {
-			listener.InterfaceUpdated()
-		}
-	}
+	r.ResetNetwork()
 	return
 	return
 }
 }
 
 
@@ -1025,5 +1018,9 @@ func (r *Router) ResetNetwork() error {
 			listener.InterfaceUpdated()
 			listener.InterfaceUpdated()
 		}
 		}
 	}
 	}
+
+	for _, transport := range r.transports {
+		transport.Reset()
+	}
 	return nil
 	return nil
 }
 }

+ 7 - 0
route/router_dns.go

@@ -146,6 +146,13 @@ func (r *Router) LookupDefault(ctx context.Context, domain string) ([]netip.Addr
 	return r.Lookup(ctx, domain, dns.DomainStrategyAsIS)
 	return r.Lookup(ctx, domain, dns.DomainStrategyAsIS)
 }
 }
 
 
+func (r *Router) ClearDNSCache() {
+	r.dnsClient.ClearCache()
+	if r.platformInterface != nil {
+		r.platformInterface.ClearDNSCache()
+	}
+}
+
 func LogDNSAnswers(logger log.ContextLogger, ctx context.Context, domain string, answers []mDNS.RR) {
 func LogDNSAnswers(logger log.ContextLogger, ctx context.Context, domain string, answers []mDNS.RR) {
 	for _, answer := range answers {
 	for _, answer := range answers {
 		logger.InfoContext(ctx, "exchanged ", domain, " ", mDNS.Type(answer.Header().Rrtype).String(), " ", formatQuestion(answer.String()))
 		logger.InfoContext(ctx, "exchanged ", domain, " ", mDNS.Type(answer.Header().Rrtype).String(), " ", formatQuestion(answer.String()))

+ 2 - 2
route/rule_item_clash_mode.go

@@ -16,7 +16,7 @@ type ClashModeItem struct {
 func NewClashModeItem(router adapter.Router, mode string) *ClashModeItem {
 func NewClashModeItem(router adapter.Router, mode string) *ClashModeItem {
 	return &ClashModeItem{
 	return &ClashModeItem{
 		router: router,
 		router: router,
-		mode:   strings.ToLower(mode),
+		mode:   mode,
 	}
 	}
 }
 }
 
 
@@ -25,7 +25,7 @@ func (r *ClashModeItem) Match(metadata *adapter.InboundContext) bool {
 	if clashServer == nil {
 	if clashServer == nil {
 		return false
 		return false
 	}
 	}
-	return clashServer.Mode() == r.mode
+	return strings.EqualFold(clashServer.Mode(), r.mode)
 }
 }
 
 
 func (r *ClashModeItem) String() string {
 func (r *ClashModeItem) String() string {

+ 2 - 2
test/go.mod

@@ -10,7 +10,7 @@ require (
 	github.com/docker/docker v24.0.5+incompatible
 	github.com/docker/docker v24.0.5+incompatible
 	github.com/docker/go-connections v0.4.0
 	github.com/docker/go-connections v0.4.0
 	github.com/gofrs/uuid/v5 v5.0.0
 	github.com/gofrs/uuid/v5 v5.0.0
-	github.com/sagernet/sing v0.2.10-0.20230821073500-620f3a3b882d
+	github.com/sagernet/sing v0.2.10-0.20230824115837-8d731e68853a
 	github.com/sagernet/sing-shadowsocks v0.2.4
 	github.com/sagernet/sing-shadowsocks v0.2.4
 	github.com/sagernet/sing-shadowsocks2 v0.1.3
 	github.com/sagernet/sing-shadowsocks2 v0.1.3
 	github.com/spyzhov/ajson v0.9.0
 	github.com/spyzhov/ajson v0.9.0
@@ -72,7 +72,7 @@ require (
 	github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
 	github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
 	github.com/sagernet/quic-go v0.0.0-20230824033040-30ef72e3be3e // indirect
 	github.com/sagernet/quic-go v0.0.0-20230824033040-30ef72e3be3e // indirect
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
 	github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
-	github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659 // indirect
+	github.com/sagernet/sing-dns v0.1.9-0.20230824120133-4d5cbceb40c1 // indirect
 	github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c // indirect
 	github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c // indirect
 	github.com/sagernet/sing-shadowtls v0.1.4 // indirect
 	github.com/sagernet/sing-shadowtls v0.1.4 // indirect
 	github.com/sagernet/sing-tun v0.1.12-0.20230821065522-7545dc2d5641 // indirect
 	github.com/sagernet/sing-tun v0.1.12-0.20230821065522-7545dc2d5641 // indirect

+ 2 - 0
test/go.sum

@@ -131,8 +131,10 @@ github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2
 github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
 github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
 github.com/sagernet/sing v0.2.10-0.20230821073500-620f3a3b882d h1:4kgoOCE48CuQcBUcoRnE0QTPXkl8yM8i7Nipmzp/978=
 github.com/sagernet/sing v0.2.10-0.20230821073500-620f3a3b882d h1:4kgoOCE48CuQcBUcoRnE0QTPXkl8yM8i7Nipmzp/978=
 github.com/sagernet/sing v0.2.10-0.20230821073500-620f3a3b882d/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
 github.com/sagernet/sing v0.2.10-0.20230821073500-620f3a3b882d/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
+github.com/sagernet/sing v0.2.10-0.20230824115837-8d731e68853a/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
 github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659 h1:1DAKccGNqTYJ8nsBR765FS0LVBVXfuFlFAHqKsGN3EI=
 github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659 h1:1DAKccGNqTYJ8nsBR765FS0LVBVXfuFlFAHqKsGN3EI=
 github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659/go.mod h1:W7GHTZFS8RkoLI3bA2LFY27/0E+uoQESWtMFLepO/JA=
 github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659/go.mod h1:W7GHTZFS8RkoLI3bA2LFY27/0E+uoQESWtMFLepO/JA=
+github.com/sagernet/sing-dns v0.1.9-0.20230824120133-4d5cbceb40c1/go.mod h1:Kg98PBJEg/08jsNFtmZWmPomhskn9Ausn50ecNm4M+8=
 github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c h1:35/FowAvt3Z62mck0TXzVc4jS5R5CWq62qcV2P1cp0I=
 github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c h1:35/FowAvt3Z62mck0TXzVc4jS5R5CWq62qcV2P1cp0I=
 github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c/go.mod h1:TKxqIvfQQgd36jp2tzsPavGjYTVZilV+atip1cssjIY=
 github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c/go.mod h1:TKxqIvfQQgd36jp2tzsPavGjYTVZilV+atip1cssjIY=
 github.com/sagernet/sing-shadowsocks v0.2.4 h1:s/CqXlvFAZhlIoHWUwPw5CoNnQ9Ibki9pckjuugtVfY=
 github.com/sagernet/sing-shadowsocks v0.2.4 h1:s/CqXlvFAZhlIoHWUwPw5CoNnQ9Ibki9pckjuugtVfY=

+ 3 - 0
transport/dhcp/server.go

@@ -85,6 +85,9 @@ func (t *Transport) Start() error {
 	return nil
 	return nil
 }
 }
 
 
+func (t *Transport) Reset() {
+}
+
 func (t *Transport) Close() error {
 func (t *Transport) Close() error {
 	if t.interfaceCallback != nil {
 	if t.interfaceCallback != nil {
 		t.router.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
 		t.router.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)

+ 3 - 0
transport/fakeip/server.go

@@ -54,6 +54,9 @@ func (s *Transport) Start() error {
 	return nil
 	return nil
 }
 }
 
 
+func (s *Transport) Reset() {
+}
+
 func (s *Transport) Close() error {
 func (s *Transport) Close() error {
 	return nil
 	return nil
 }
 }