Migrate to independent cache file

This commit is contained in:
世界 2023-11-28 12:00:28 +08:00
parent e3f8567690
commit bf4e556f67
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
16 changed files with 227 additions and 179 deletions

View File

@ -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

53
box.go
View File

@ -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)

View File

@ -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") db.Close()
} return E.Cause(err, "platform chown")
var cacheIDBytes []byte
if cacheID != "" {
cacheIDBytes = append([]byte{0}, []byte(cacheID)...)
} }
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{ c.DB = db
DB: db, return nil
cacheID: cacheIDBytes, }
saveDomain: make(map[netip.Addr]string),
saveAddress4: make(map[string]netip.Addr), func (c *CacheFile) PreStart() error {
saveAddress6: make(map[string]netip.Addr), return c.start()
}, nil }
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()
}

View File

@ -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)

View File

@ -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 == "" { //goland:noinspection GoDeprecation
cachePath := os.ExpandEnv(options.CacheFile) //nolint:staticcheck
if cachePath == "" { if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" {
cachePath = "cache.db" 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.")
}
if foundPath, loaded := C.FindPath(cachePath); loaded {
cachePath = foundPath
} else {
cachePath = filemanager.BasePath(ctx, cachePath)
}
server.cacheFilePath = cachePath
server.cacheID = options.CacheID
} }
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 := service.FromContext[adapter.CacheFile](s.ctx)
cacheFile, err := cachefile.Open(s.ctx, s.cacheFilePath, s.cacheID) if cacheFile != nil {
if err != nil { mode := cacheFile.LoadMode()
return E.Cause(err, "open cache file") if common.Any(s.modeList, func(it string) bool {
} return strings.EqualFold(it, mode)
s.cacheFile = cacheFile }) {
if s.storeMode { s.mode = mode
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
@ -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 { cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
err := s.cacheFile.StoreMode(newMode) 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
} }

View File

@ -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 cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx)
if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil {
cacheFile = clashServer.CacheFile()
}
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 serviceNow := s.service
if service == nil { 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 { cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx)
if cacheFile := clashServer.CacheFile(); cacheFile != nil { if cacheFile != nil {
err = cacheFile.StoreGroupExpand(groupTag, isExpand) err = cacheFile.StoreGroupExpand(groupTag, isExpand)
if err != nil { if err != nil {
return writeError(conn, err) return writeError(conn, err)
}
} }
} }
return writeError(conn, nil) return writeError(conn, nil)

View File

@ -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 serviceNow := s.service
if service == nil { 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 historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx)
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"))
}
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 {

View File

@ -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"`
}

View File

@ -1,7 +1,48 @@
package option package option
type ExperimentalOptions struct { type ExperimentalOptions struct {
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
Debug *DebugOptions `json:"debug,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
option/group.go Normal file
View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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:

View File

@ -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() { cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
selected := clashServer.CacheFile().LoadSelected(s.tag) 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() { cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
err := clashServer.CacheFile().StoreSelected(s.tag, tag) 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)
} }

View File

@ -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()

View File

@ -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() { cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
if cacheFile := clashServer.CacheFile(); cacheFile != nil { if cacheFile != nil && cacheFile.StoreFakeIP() {
storage = cacheFile storage = cacheFile
}
} }
if storage == nil { if storage == nil {
storage = NewMemoryStorage() storage = NewMemoryStorage()