Browse Source

Migrate to independent cache file

世界 1 year ago
parent
commit
bf4e556f67

+ 6 - 4
adapter/experimental.go

@@ -13,15 +13,17 @@ type ClashServer interface {
 	PreStarter
 	PreStarter
 	Mode() string
 	Mode() string
 	ModeList() []string
 	ModeList() []string
-	StoreSelected() bool
-	StoreFakeIP() bool
-	CacheFile() ClashCacheFile
 	HistoryStorage() *urltest.HistoryStorage
 	HistoryStorage() *urltest.HistoryStorage
 	RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
 	RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
 	RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
 	RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
 }
 }
 
 
-type ClashCacheFile interface {
+type CacheFile interface {
+	Service
+	PreStarter
+
+	StoreFakeIP() bool
+
 	LoadMode() string
 	LoadMode() string
 	StoreMode(mode string) error
 	StoreMode(mode string) error
 	LoadSelected(group string) string
 	LoadSelected(group string) string

+ 44 - 9
box.go

@@ -10,6 +10,7 @@ import (
 
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/experimental"
 	"github.com/sagernet/sing-box/experimental"
+	"github.com/sagernet/sing-box/experimental/cachefile"
 	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/inbound"
 	"github.com/sagernet/sing-box/inbound"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
@@ -32,7 +33,8 @@ type Box struct {
 	outbounds    []adapter.Outbound
 	outbounds    []adapter.Outbound
 	logFactory   log.Factory
 	logFactory   log.Factory
 	logger       log.ContextLogger
 	logger       log.ContextLogger
-	preServices  map[string]adapter.Service
+	preServices1 map[string]adapter.Service
+	preServices2 map[string]adapter.Service
 	postServices map[string]adapter.Service
 	postServices map[string]adapter.Service
 	done         chan struct{}
 	done         chan struct{}
 }
 }
@@ -45,17 +47,21 @@ type Options struct {
 }
 }
 
 
 func New(options Options) (*Box, error) {
 func New(options Options) (*Box, error) {
+	createdAt := time.Now()
 	ctx := options.Context
 	ctx := options.Context
 	if ctx == nil {
 	if ctx == nil {
 		ctx = context.Background()
 		ctx = context.Background()
 	}
 	}
 	ctx = service.ContextWithDefaultRegistry(ctx)
 	ctx = service.ContextWithDefaultRegistry(ctx)
 	ctx = pause.ContextWithDefaultManager(ctx)
 	ctx = pause.ContextWithDefaultManager(ctx)
-	createdAt := time.Now()
 	experimentalOptions := common.PtrValueOrDefault(options.Experimental)
 	experimentalOptions := common.PtrValueOrDefault(options.Experimental)
 	applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
 	applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
+	var needCacheFile bool
 	var needClashAPI bool
 	var needClashAPI bool
 	var needV2RayAPI bool
 	var needV2RayAPI bool
+	if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
+		needCacheFile = true
+	}
 	if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
 	if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
 		needClashAPI = true
 		needClashAPI = true
 	}
 	}
@@ -145,8 +151,14 @@ func New(options Options) (*Box, error) {
 			return nil, E.Cause(err, "initialize platform interface")
 			return nil, E.Cause(err, "initialize platform interface")
 		}
 		}
 	}
 	}
-	preServices := make(map[string]adapter.Service)
+	preServices1 := make(map[string]adapter.Service)
+	preServices2 := make(map[string]adapter.Service)
 	postServices := make(map[string]adapter.Service)
 	postServices := make(map[string]adapter.Service)
+	if needCacheFile {
+		cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
+		preServices1["cache file"] = cacheFile
+		service.MustRegister[adapter.CacheFile](ctx, cacheFile)
+	}
 	if needClashAPI {
 	if needClashAPI {
 		clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
 		clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
 		clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
 		clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
@@ -155,7 +167,7 @@ func New(options Options) (*Box, error) {
 			return nil, E.Cause(err, "create clash api server")
 			return nil, E.Cause(err, "create clash api server")
 		}
 		}
 		router.SetClashServer(clashServer)
 		router.SetClashServer(clashServer)
-		preServices["clash api"] = clashServer
+		preServices2["clash api"] = clashServer
 	}
 	}
 	if needV2RayAPI {
 	if needV2RayAPI {
 		v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
 		v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
@@ -163,7 +175,7 @@ func New(options Options) (*Box, error) {
 			return nil, E.Cause(err, "create v2ray api server")
 			return nil, E.Cause(err, "create v2ray api server")
 		}
 		}
 		router.SetV2RayServer(v2rayServer)
 		router.SetV2RayServer(v2rayServer)
-		preServices["v2ray api"] = v2rayServer
+		preServices2["v2ray api"] = v2rayServer
 	}
 	}
 	return &Box{
 	return &Box{
 		router:       router,
 		router:       router,
@@ -172,7 +184,8 @@ func New(options Options) (*Box, error) {
 		createdAt:    createdAt,
 		createdAt:    createdAt,
 		logFactory:   logFactory,
 		logFactory:   logFactory,
 		logger:       logFactory.Logger(),
 		logger:       logFactory.Logger(),
-		preServices:  preServices,
+		preServices1: preServices1,
+		preServices2: preServices2,
 		postServices: postServices,
 		postServices: postServices,
 		done:         make(chan struct{}),
 		done:         make(chan struct{}),
 	}, nil
 	}, nil
@@ -217,7 +230,16 @@ func (s *Box) Start() error {
 }
 }
 
 
 func (s *Box) preStart() error {
 func (s *Box) preStart() error {
-	for serviceName, service := range s.preServices {
+	for serviceName, service := range s.preServices1 {
+		if preService, isPreService := service.(adapter.PreStarter); isPreService {
+			s.logger.Trace("pre-start ", serviceName)
+			err := preService.PreStart()
+			if err != nil {
+				return E.Cause(err, "pre-starting ", serviceName)
+			}
+		}
+	}
+	for serviceName, service := range s.preServices2 {
 		if preService, isPreService := service.(adapter.PreStarter); isPreService {
 		if preService, isPreService := service.(adapter.PreStarter); isPreService {
 			s.logger.Trace("pre-start ", serviceName)
 			s.logger.Trace("pre-start ", serviceName)
 			err := preService.PreStart()
 			err := preService.PreStart()
@@ -238,7 +260,14 @@ func (s *Box) start() error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	for serviceName, service := range s.preServices {
+	for serviceName, service := range s.preServices1 {
+		s.logger.Trace("starting ", serviceName)
+		err = service.Start()
+		if err != nil {
+			return E.Cause(err, "start ", serviceName)
+		}
+	}
+	for serviceName, service := range s.preServices2 {
 		s.logger.Trace("starting ", serviceName)
 		s.logger.Trace("starting ", serviceName)
 		err = service.Start()
 		err = service.Start()
 		if err != nil {
 		if err != nil {
@@ -314,7 +343,13 @@ func (s *Box) Close() error {
 			return E.Cause(err, "close router")
 			return E.Cause(err, "close router")
 		})
 		})
 	}
 	}
-	for serviceName, service := range s.preServices {
+	for serviceName, service := range s.preServices1 {
+		s.logger.Trace("closing ", serviceName)
+		errors = E.Append(errors, service.Close(), func(err error) error {
+			return E.Cause(err, "close ", serviceName)
+		})
+	}
+	for serviceName, service := range s.preServices2 {
 		s.logger.Trace("closing ", serviceName)
 		s.logger.Trace("closing ", serviceName)
 		errors = E.Append(errors, service.Close(), func(err error) error {
 		errors = E.Append(errors, service.Close(), func(err error) error {
 			return E.Cause(err, "close ", serviceName)
 			return E.Cause(err, "close ", serviceName)

+ 60 - 25
experimental/clashapi/cachefile/cache.go → experimental/cachefile/cache.go

@@ -12,6 +12,7 @@ import (
 	"github.com/sagernet/bbolt"
 	"github.com/sagernet/bbolt"
 	bboltErrors "github.com/sagernet/bbolt/errors"
 	bboltErrors "github.com/sagernet/bbolt/errors"
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/service/filemanager"
 	"github.com/sagernet/sing/service/filemanager"
@@ -31,11 +32,15 @@ var (
 	cacheIDDefault = []byte("default")
 	cacheIDDefault = []byte("default")
 )
 )
 
 
-var _ adapter.ClashCacheFile = (*CacheFile)(nil)
+var _ adapter.CacheFile = (*CacheFile)(nil)
 
 
 type CacheFile struct {
 type CacheFile struct {
+	ctx         context.Context
+	path        string
+	cacheID     []byte
+	storeFakeIP bool
+
 	DB                *bbolt.DB
 	DB                *bbolt.DB
-	cacheID           []byte
 	saveAccess        sync.RWMutex
 	saveAccess        sync.RWMutex
 	saveDomain        map[netip.Addr]string
 	saveDomain        map[netip.Addr]string
 	saveAddress4      map[string]netip.Addr
 	saveAddress4      map[string]netip.Addr
@@ -43,7 +48,29 @@ type CacheFile struct {
 	saveMetadataTimer *time.Timer
 	saveMetadataTimer *time.Timer
 }
 }
 
 
-func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) {
+func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
+	var path string
+	if options.Path != "" {
+		path = options.Path
+	} else {
+		path = "cache.db"
+	}
+	var cacheIDBytes []byte
+	if options.CacheID != "" {
+		cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...)
+	}
+	return &CacheFile{
+		ctx:          ctx,
+		path:         filemanager.BasePath(ctx, path),
+		cacheID:      cacheIDBytes,
+		storeFakeIP:  options.StoreFakeIP,
+		saveDomain:   make(map[netip.Addr]string),
+		saveAddress4: make(map[string]netip.Addr),
+		saveAddress6: make(map[string]netip.Addr),
+	}
+}
+
+func (c *CacheFile) start() error {
 	const fileMode = 0o666
 	const fileMode = 0o666
 	options := bbolt.Options{Timeout: time.Second}
 	options := bbolt.Options{Timeout: time.Second}
 	var (
 	var (
@@ -51,7 +78,7 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error)
 		err error
 		err error
 	)
 	)
 	for i := 0; i < 10; i++ {
 	for i := 0; i < 10; i++ {
-		db, err = bbolt.Open(path, fileMode, &options)
+		db, err = bbolt.Open(c.path, fileMode, &options)
 		if err == nil {
 		if err == nil {
 			break
 			break
 		}
 		}
@@ -59,23 +86,20 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error)
 			continue
 			continue
 		}
 		}
 		if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) {
 		if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) {
-			rmErr := os.Remove(path)
+			rmErr := os.Remove(c.path)
 			if rmErr != nil {
 			if rmErr != nil {
-				return nil, err
+				return err
 			}
 			}
 		}
 		}
 		time.Sleep(100 * time.Millisecond)
 		time.Sleep(100 * time.Millisecond)
 	}
 	}
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return err
 	}
 	}
-	err = filemanager.Chown(ctx, path)
+	err = filemanager.Chown(c.ctx, c.path)
 	if err != nil {
 	if err != nil {
-		return nil, E.Cause(err, "platform chown")
-	}
-	var cacheIDBytes []byte
-	if cacheID != "" {
-		cacheIDBytes = append([]byte{0}, []byte(cacheID)...)
+		db.Close()
+		return E.Cause(err, "platform chown")
 	}
 	}
 	err = db.Batch(func(tx *bbolt.Tx) error {
 	err = db.Batch(func(tx *bbolt.Tx) error {
 		return tx.ForEach(func(name []byte, b *bbolt.Bucket) error {
 		return tx.ForEach(func(name []byte, b *bbolt.Bucket) error {
@@ -97,15 +121,30 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error)
 		})
 		})
 	})
 	})
 	if err != nil {
 	if err != nil {
-		return nil, err
+		db.Close()
+		return err
 	}
 	}
-	return &CacheFile{
-		DB:           db,
-		cacheID:      cacheIDBytes,
-		saveDomain:   make(map[netip.Addr]string),
-		saveAddress4: make(map[string]netip.Addr),
-		saveAddress6: make(map[string]netip.Addr),
-	}, nil
+	c.DB = db
+	return nil
+}
+
+func (c *CacheFile) PreStart() error {
+	return c.start()
+}
+
+func (c *CacheFile) Start() error {
+	return nil
+}
+
+func (c *CacheFile) Close() error {
+	if c.DB == nil {
+		return nil
+	}
+	return c.DB.Close()
+}
+
+func (c *CacheFile) StoreFakeIP() bool {
+	return c.storeFakeIP
 }
 }
 
 
 func (c *CacheFile) LoadMode() string {
 func (c *CacheFile) LoadMode() string {
@@ -218,7 +257,3 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
 		}
 		}
 	})
 	})
 }
 }
-
-func (c *CacheFile) Close() error {
-	return c.DB.Close()
-}

+ 0 - 0
experimental/clashapi/cachefile/fakeip.go → experimental/cachefile/fakeip.go


+ 7 - 4
experimental/clashapi/cache.go

@@ -1,23 +1,26 @@
 package clashapi
 package clashapi
 
 
 import (
 import (
+	"context"
 	"net/http"
 	"net/http"
 
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing/service"
 
 
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/render"
 	"github.com/go-chi/render"
 )
 )
 
 
-func cacheRouter(router adapter.Router) http.Handler {
+func cacheRouter(ctx context.Context) http.Handler {
 	r := chi.NewRouter()
 	r := chi.NewRouter()
-	r.Post("/fakeip/flush", flushFakeip(router))
+	r.Post("/fakeip/flush", flushFakeip(ctx))
 	return r
 	return r
 }
 }
 
 
-func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
+func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
-		if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil {
+		cacheFile := service.FromContext[adapter.CacheFile](ctx)
+		if cacheFile != nil {
 			err := cacheFile.FakeIPReset()
 			err := cacheFile.FakeIPReset()
 			if err != nil {
 			if err != nil {
 				render.Status(r, http.StatusInternalServerError)
 				render.Status(r, http.StatusInternalServerError)

+ 15 - 51
experimental/clashapi/server.go

@@ -15,7 +15,6 @@ import (
 	"github.com/sagernet/sing-box/common/urltest"
 	"github.com/sagernet/sing-box/common/urltest"
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/experimental"
 	"github.com/sagernet/sing-box/experimental"
-	"github.com/sagernet/sing-box/experimental/clashapi/cachefile"
 	"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
 	"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
@@ -49,12 +48,6 @@ type Server struct {
 	mode           string
 	mode           string
 	modeList       []string
 	modeList       []string
 	modeUpdateHook chan<- struct{}
 	modeUpdateHook chan<- struct{}
-	storeMode      bool
-	storeSelected  bool
-	storeFakeIP    bool
-	cacheFilePath  string
-	cacheID        string
-	cacheFile      adapter.ClashCacheFile
 
 
 	externalController       bool
 	externalController       bool
 	externalUI               string
 	externalUI               string
@@ -76,9 +69,6 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
 		trafficManager:           trafficManager,
 		trafficManager:           trafficManager,
 		modeList:                 options.ModeList,
 		modeList:                 options.ModeList,
 		externalController:       options.ExternalController != "",
 		externalController:       options.ExternalController != "",
-		storeMode:                options.StoreMode,
-		storeSelected:            options.StoreSelected,
-		storeFakeIP:              options.StoreFakeIP,
 		externalUIDownloadURL:    options.ExternalUIDownloadURL,
 		externalUIDownloadURL:    options.ExternalUIDownloadURL,
 		externalUIDownloadDetour: options.ExternalUIDownloadDetour,
 		externalUIDownloadDetour: options.ExternalUIDownloadDetour,
 	}
 	}
@@ -94,18 +84,10 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
 		server.modeList = append([]string{defaultMode}, server.modeList...)
 		server.modeList = append([]string{defaultMode}, server.modeList...)
 	}
 	}
 	server.mode = defaultMode
 	server.mode = defaultMode
-	if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" {
-		cachePath := os.ExpandEnv(options.CacheFile)
-		if cachePath == "" {
-			cachePath = "cache.db"
-		}
-		if foundPath, loaded := C.FindPath(cachePath); loaded {
-			cachePath = foundPath
-		} else {
-			cachePath = filemanager.BasePath(ctx, cachePath)
-		}
-		server.cacheFilePath = cachePath
-		server.cacheID = options.CacheID
+	//goland:noinspection GoDeprecation
+	//nolint:staticcheck
+	if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" {
+		return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.")
 	}
 	}
 	cors := cors.New(cors.Options{
 	cors := cors.New(cors.Options{
 		AllowedOrigins: []string{"*"},
 		AllowedOrigins: []string{"*"},
@@ -128,7 +110,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
 		r.Mount("/providers/rules", ruleProviderRouter())
 		r.Mount("/providers/rules", ruleProviderRouter())
 		r.Mount("/script", scriptRouter())
 		r.Mount("/script", scriptRouter())
 		r.Mount("/profile", profileRouter())
 		r.Mount("/profile", profileRouter())
-		r.Mount("/cache", cacheRouter(router))
+		r.Mount("/cache", cacheRouter(ctx))
 		r.Mount("/dns", dnsRouter(router))
 		r.Mount("/dns", dnsRouter(router))
 
 
 		server.setupMetaAPI(r)
 		server.setupMetaAPI(r)
@@ -147,19 +129,13 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
 }
 }
 
 
 func (s *Server) PreStart() error {
 func (s *Server) PreStart() error {
-	if s.cacheFilePath != "" {
-		cacheFile, err := cachefile.Open(s.ctx, s.cacheFilePath, s.cacheID)
-		if err != nil {
-			return E.Cause(err, "open cache file")
-		}
-		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
-			}
+	cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
+	if cacheFile != nil {
+		mode := cacheFile.LoadMode()
+		if common.Any(s.modeList, func(it string) bool {
+			return strings.EqualFold(it, mode)
+		}) {
+			s.mode = mode
 		}
 		}
 	}
 	}
 	return nil
 	return nil
@@ -187,7 +163,6 @@ func (s *Server) Close() error {
 	return common.Close(
 	return common.Close(
 		common.PtrOrNil(s.httpServer),
 		common.PtrOrNil(s.httpServer),
 		s.trafficManager,
 		s.trafficManager,
-		s.cacheFile,
 		s.urlTestHistory,
 		s.urlTestHistory,
 	)
 	)
 }
 }
@@ -224,8 +199,9 @@ func (s *Server) SetMode(newMode string) {
 		}
 		}
 	}
 	}
 	s.router.ClearDNSCache()
 	s.router.ClearDNSCache()
-	if s.storeMode {
-		err := s.cacheFile.StoreMode(newMode)
+	cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
+	if cacheFile != nil {
+		err := cacheFile.StoreMode(newMode)
 		if err != nil {
 		if err != nil {
 			s.logger.Error(E.Cause(err, "save mode"))
 			s.logger.Error(E.Cause(err, "save mode"))
 		}
 		}
@@ -233,18 +209,6 @@ func (s *Server) SetMode(newMode string) {
 	s.logger.Info("updated mode: ", newMode)
 	s.logger.Info("updated mode: ", newMode)
 }
 }
 
 
-func (s *Server) StoreSelected() bool {
-	return s.storeSelected
-}
-
-func (s *Server) StoreFakeIP() bool {
-	return s.storeFakeIP
-}
-
-func (s *Server) CacheFile() adapter.ClashCacheFile {
-	return s.cacheFile
-}
-
 func (s *Server) HistoryStorage() *urltest.HistoryStorage {
 func (s *Server) HistoryStorage() *urltest.HistoryStorage {
 	return s.urlTestHistory
 	return s.urlTestHistory
 }
 }

+ 8 - 13
experimental/libbox/command_group.go

@@ -159,11 +159,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
 
 
 func writeGroups(writer io.Writer, boxService *BoxService) error {
 func writeGroups(writer io.Writer, boxService *BoxService) error {
 	historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx)
 	historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx)
-	var cacheFile adapter.ClashCacheFile
-	if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil {
-		cacheFile = clashServer.CacheFile()
-	}
-
+	cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx)
 	outbounds := boxService.instance.Router().Outbounds()
 	outbounds := boxService.instance.Router().Outbounds()
 	var iGroups []adapter.OutboundGroup
 	var iGroups []adapter.OutboundGroup
 	for _, it := range outbounds {
 	for _, it := range outbounds {
@@ -288,16 +284,15 @@ func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	service := s.service
-	if service == nil {
+	serviceNow := s.service
+	if serviceNow == nil {
 		return writeError(conn, E.New("service not ready"))
 		return writeError(conn, E.New("service not ready"))
 	}
 	}
-	if clashServer := service.instance.Router().ClashServer(); clashServer != nil {
-		if cacheFile := clashServer.CacheFile(); cacheFile != nil {
-			err = cacheFile.StoreGroupExpand(groupTag, isExpand)
-			if err != nil {
-				return writeError(conn, err)
-			}
+	cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx)
+	if cacheFile != nil {
+		err = cacheFile.StoreGroupExpand(groupTag, isExpand)
+		if err != nil {
+			return writeError(conn, err)
 		}
 		}
 	}
 	}
 	return writeError(conn, nil)
 	return writeError(conn, nil)

+ 8 - 13
experimental/libbox/command_urltest.go

@@ -12,6 +12,7 @@ import (
 	"github.com/sagernet/sing/common/batch"
 	"github.com/sagernet/sing/common/batch"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/rw"
 	"github.com/sagernet/sing/common/rw"
+	"github.com/sagernet/sing/service"
 )
 )
 
 
 func (c *CommandClient) URLTest(groupTag string) error {
 func (c *CommandClient) URLTest(groupTag string) error {
@@ -37,11 +38,11 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	service := s.service
-	if service == nil {
+	serviceNow := s.service
+	if serviceNow == nil {
 		return nil
 		return nil
 	}
 	}
-	abstractOutboundGroup, isLoaded := service.instance.Router().Outbound(groupTag)
+	abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag)
 	if !isLoaded {
 	if !isLoaded {
 		return writeError(conn, E.New("outbound group not found: ", groupTag))
 		return writeError(conn, E.New("outbound group not found: ", groupTag))
 	}
 	}
@@ -53,15 +54,9 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error {
 	if isURLTest {
 	if isURLTest {
 		go urlTest.CheckOutbounds()
 		go urlTest.CheckOutbounds()
 	} else {
 	} else {
-		var historyStorage *urltest.HistoryStorage
-		if clashServer := service.instance.Router().ClashServer(); clashServer != nil {
-			historyStorage = clashServer.HistoryStorage()
-		} else {
-			return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group"))
-		}
-
+		historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx)
 		outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
 		outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
-			itOutbound, _ := service.instance.Router().Outbound(it)
+			itOutbound, _ := serviceNow.instance.Router().Outbound(it)
 			return itOutbound
 			return itOutbound
 		}), func(it adapter.Outbound) bool {
 		}), func(it adapter.Outbound) bool {
 			if it == nil {
 			if it == nil {
@@ -73,12 +68,12 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error {
 			}
 			}
 			return true
 			return true
 		})
 		})
-		b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10))
+		b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10))
 		for _, detour := range outbounds {
 		for _, detour := range outbounds {
 			outboundToTest := detour
 			outboundToTest := detour
 			outboundTag := outboundToTest.Tag()
 			outboundTag := outboundToTest.Tag()
 			b.Go(outboundTag, func() (any, error) {
 			b.Go(outboundTag, func() (any, error) {
-				t, err := urltest.URLTest(service.ctx, "", outboundToTest)
+				t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest)
 				if err != nil {
 				if err != nil {
 					historyStorage.DeleteURLTestHistory(outboundTag)
 					historyStorage.DeleteURLTestHistory(outboundTag)
 				} else {
 				} else {

+ 0 - 31
option/clash.go

@@ -1,31 +0,0 @@
-package option
-
-type ClashAPIOptions struct {
-	ExternalController       string `json:"external_controller,omitempty"`
-	ExternalUI               string `json:"external_ui,omitempty"`
-	ExternalUIDownloadURL    string `json:"external_ui_download_url,omitempty"`
-	ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"`
-	Secret                   string `json:"secret,omitempty"`
-	DefaultMode              string `json:"default_mode,omitempty"`
-	StoreMode                bool   `json:"store_mode,omitempty"`
-	StoreSelected            bool   `json:"store_selected,omitempty"`
-	StoreFakeIP              bool   `json:"store_fakeip,omitempty"`
-	CacheFile                string `json:"cache_file,omitempty"`
-	CacheID                  string `json:"cache_id,omitempty"`
-
-	ModeList []string `json:"-"`
-}
-
-type SelectorOutboundOptions struct {
-	Outbounds                 []string `json:"outbounds"`
-	Default                   string   `json:"default,omitempty"`
-	InterruptExistConnections bool     `json:"interrupt_exist_connections,omitempty"`
-}
-
-type URLTestOutboundOptions struct {
-	Outbounds                 []string `json:"outbounds"`
-	URL                       string   `json:"url,omitempty"`
-	Interval                  Duration `json:"interval,omitempty"`
-	Tolerance                 uint16   `json:"tolerance,omitempty"`
-	InterruptExistConnections bool     `json:"interrupt_exist_connections,omitempty"`
-}

+ 44 - 3
option/experimental.go

@@ -1,7 +1,48 @@
 package option
 package option
 
 
 type ExperimentalOptions struct {
 type ExperimentalOptions struct {
-	ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
-	V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
-	Debug    *DebugOptions    `json:"debug,omitempty"`
+	CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
+	ClashAPI  *ClashAPIOptions  `json:"clash_api,omitempty"`
+	V2RayAPI  *V2RayAPIOptions  `json:"v2ray_api,omitempty"`
+	Debug     *DebugOptions     `json:"debug,omitempty"`
+}
+
+type CacheFileOptions struct {
+	Enabled     bool   `json:"enabled,omitempty"`
+	Path        string `json:"path,omitempty"`
+	CacheID     string `json:"cache_id,omitempty"`
+	StoreFakeIP bool   `json:"store_fakeip,omitempty"`
+}
+
+type ClashAPIOptions struct {
+	ExternalController       string   `json:"external_controller,omitempty"`
+	ExternalUI               string   `json:"external_ui,omitempty"`
+	ExternalUIDownloadURL    string   `json:"external_ui_download_url,omitempty"`
+	ExternalUIDownloadDetour string   `json:"external_ui_download_detour,omitempty"`
+	Secret                   string   `json:"secret,omitempty"`
+	DefaultMode              string   `json:"default_mode,omitempty"`
+	ModeList                 []string `json:"-"`
+
+	// Deprecated: migrated to global cache file
+	StoreMode bool `json:"store_mode,omitempty"`
+	// Deprecated: migrated to global cache file
+	StoreSelected bool `json:"store_selected,omitempty"`
+	// Deprecated: migrated to global cache file
+	StoreFakeIP bool `json:"store_fakeip,omitempty"`
+	// Deprecated: migrated to global cache file
+	CacheFile string `json:"cache_file,omitempty"`
+	// Deprecated: migrated to global cache file
+	CacheID string `json:"cache_id,omitempty"`
+}
+
+type V2RayAPIOptions struct {
+	Listen string                    `json:"listen,omitempty"`
+	Stats  *V2RayStatsServiceOptions `json:"stats,omitempty"`
+}
+
+type V2RayStatsServiceOptions struct {
+	Enabled   bool     `json:"enabled,omitempty"`
+	Inbounds  []string `json:"inbounds,omitempty"`
+	Outbounds []string `json:"outbounds,omitempty"`
+	Users     []string `json:"users,omitempty"`
 }
 }

+ 15 - 0
option/group.go

@@ -0,0 +1,15 @@
+package option
+
+type SelectorOutboundOptions struct {
+	Outbounds                 []string `json:"outbounds"`
+	Default                   string   `json:"default,omitempty"`
+	InterruptExistConnections bool     `json:"interrupt_exist_connections,omitempty"`
+}
+
+type URLTestOutboundOptions struct {
+	Outbounds                 []string `json:"outbounds"`
+	URL                       string   `json:"url,omitempty"`
+	Interval                  Duration `json:"interval,omitempty"`
+	Tolerance                 uint16   `json:"tolerance,omitempty"`
+	InterruptExistConnections bool     `json:"interrupt_exist_connections,omitempty"`
+}

+ 0 - 12
option/v2ray.go

@@ -1,13 +1 @@
 package option
 package option
-
-type V2RayAPIOptions struct {
-	Listen string                    `json:"listen,omitempty"`
-	Stats  *V2RayStatsServiceOptions `json:"stats,omitempty"`
-}
-
-type V2RayStatsServiceOptions struct {
-	Enabled   bool     `json:"enabled,omitempty"`
-	Inbounds  []string `json:"inbounds,omitempty"`
-	Outbounds []string `json:"outbounds,omitempty"`
-	Users     []string `json:"users,omitempty"`
-}

+ 1 - 1
outbound/builder.go

@@ -56,7 +56,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
 	case C.TypeHysteria2:
 	case C.TypeHysteria2:
 		return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options)
 		return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options)
 	case C.TypeSelector:
 	case C.TypeSelector:
-		return NewSelector(router, logger, tag, options.SelectorOptions)
+		return NewSelector(ctx, router, logger, tag, options.SelectorOptions)
 	case C.TypeURLTest:
 	case C.TypeURLTest:
 		return NewURLTest(ctx, router, logger, tag, options.URLTestOptions)
 		return NewURLTest(ctx, router, logger, tag, options.URLTestOptions)
 	default:
 	default:

+ 10 - 5
outbound/selector.go

@@ -12,6 +12,7 @@ import (
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	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/service"
 )
 )
 
 
 var (
 var (
@@ -21,6 +22,7 @@ var (
 
 
 type Selector struct {
 type Selector struct {
 	myOutboundAdapter
 	myOutboundAdapter
+	ctx                          context.Context
 	tags                         []string
 	tags                         []string
 	defaultTag                   string
 	defaultTag                   string
 	outbounds                    map[string]adapter.Outbound
 	outbounds                    map[string]adapter.Outbound
@@ -29,7 +31,7 @@ type Selector struct {
 	interruptExternalConnections bool
 	interruptExternalConnections bool
 }
 }
 
 
-func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) {
+func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) {
 	outbound := &Selector{
 	outbound := &Selector{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeSelector,
 			protocol:     C.TypeSelector,
@@ -38,6 +40,7 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op
 			tag:          tag,
 			tag:          tag,
 			dependencies: options.Outbounds,
 			dependencies: options.Outbounds,
 		},
 		},
+		ctx:                          ctx,
 		tags:                         options.Outbounds,
 		tags:                         options.Outbounds,
 		defaultTag:                   options.Default,
 		defaultTag:                   options.Default,
 		outbounds:                    make(map[string]adapter.Outbound),
 		outbounds:                    make(map[string]adapter.Outbound),
@@ -67,8 +70,9 @@ func (s *Selector) Start() error {
 	}
 	}
 
 
 	if s.tag != "" {
 	if s.tag != "" {
-		if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() {
-			selected := clashServer.CacheFile().LoadSelected(s.tag)
+		cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
+		if cacheFile != nil {
+			selected := cacheFile.LoadSelected(s.tag)
 			if selected != "" {
 			if selected != "" {
 				detour, loaded := s.outbounds[selected]
 				detour, loaded := s.outbounds[selected]
 				if loaded {
 				if loaded {
@@ -110,8 +114,9 @@ func (s *Selector) SelectOutbound(tag string) bool {
 	}
 	}
 	s.selected = detour
 	s.selected = detour
 	if s.tag != "" {
 	if s.tag != "" {
-		if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() {
-			err := clashServer.CacheFile().StoreSelected(s.tag, tag)
+		cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
+		if cacheFile != nil {
+			err := cacheFile.StoreSelected(s.tag, tag)
 			if err != nil {
 			if err != nil {
 				s.logger.Error("store selected: ", err)
 				s.logger.Error("store selected: ", err)
 			}
 			}

+ 1 - 1
route/router.go

@@ -262,7 +262,7 @@ func NewRouter(
 		if fakeIPOptions.Inet6Range != nil {
 		if fakeIPOptions.Inet6Range != nil {
 			inet6Range = *fakeIPOptions.Inet6Range
 			inet6Range = *fakeIPOptions.Inet6Range
 		}
 		}
-		router.fakeIPStore = fakeip.NewStore(router, router.logger, inet4Range, inet6Range)
+		router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range)
 	}
 	}
 
 
 	usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor()
 	usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor()

+ 8 - 7
transport/fakeip/store.go

@@ -1,17 +1,19 @@
 package fakeip
 package fakeip
 
 
 import (
 import (
+	"context"
 	"net/netip"
 	"net/netip"
 
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
 	"github.com/sagernet/sing/common/logger"
+	"github.com/sagernet/sing/service"
 )
 )
 
 
 var _ adapter.FakeIPStore = (*Store)(nil)
 var _ adapter.FakeIPStore = (*Store)(nil)
 
 
 type Store struct {
 type Store struct {
-	router       adapter.Router
+	ctx          context.Context
 	logger       logger.Logger
 	logger       logger.Logger
 	inet4Range   netip.Prefix
 	inet4Range   netip.Prefix
 	inet6Range   netip.Prefix
 	inet6Range   netip.Prefix
@@ -20,9 +22,9 @@ type Store struct {
 	inet6Current netip.Addr
 	inet6Current netip.Addr
 }
 }
 
 
-func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store {
+func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store {
 	return &Store{
 	return &Store{
-		router:     router,
+		ctx:        ctx,
 		logger:     logger,
 		logger:     logger,
 		inet4Range: inet4Range,
 		inet4Range: inet4Range,
 		inet6Range: inet6Range,
 		inet6Range: inet6Range,
@@ -31,10 +33,9 @@ func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Pref
 
 
 func (s *Store) Start() error {
 func (s *Store) Start() error {
 	var storage adapter.FakeIPStorage
 	var storage adapter.FakeIPStorage
-	if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() {
-		if cacheFile := clashServer.CacheFile(); cacheFile != nil {
-			storage = cacheFile
-		}
+	cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
+	if cacheFile != nil && cacheFile.StoreFakeIP() {
+		storage = cacheFile
 	}
 	}
 	if storage == nil {
 	if storage == nil {
 		storage = NewMemoryStorage()
 		storage = NewMemoryStorage()