Add store_mode and platform Clash mode selector

This commit is contained in:
世界 2023-08-24 21:52:38 +08:00
parent 6dcacf3b5e
commit 43f72a6419
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
29 changed files with 405 additions and 71 deletions

View File

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

View File

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

4
box.go
View File

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

View File

@ -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()] { func (s *HistoryStorage) SetHook(hook chan<- struct{}) {
s.access.Lock() s.updateHook = hook
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) 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() updateHook := s.updateHook
defer s.access.RUnlock() if updateHook != nil {
for element := s.callbacks.Front(); element != nil; element = element.Next() { select {
element.Value() 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"

View File

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

View File

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

View File

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

View File

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

View File

@ -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) server.SetMode(newConfig.Mode)
if server.mode != mode {
server.mode = mode
logger.Info("updated mode: ", mode)
}
} }
render.NoContent(w, r) render.NoContent(w, r)
} }

View File

@ -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), modeList: options.ModeList,
storeSelected: options.StoreSelected,
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 == "" { defaultMode := "Rule"
server.mode = "rule" if options.DefaultMode != "" {
defaultMode = options.DefaultMode
} }
if options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" { if !common.Contains(server.modeList, defaultMode) {
server.modeList = append(server.modeList, defaultMode)
}
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
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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 { if newService != nil {
service.PtrFromContext[urltest.HistoryStorage](s.service.ctx).RemoveListener(s.urlTestListener) service.PtrFromContext[urltest.HistoryStorage](newService.ctx).SetHook(s.urlTestUpdate)
s.urlTestListener = nil newService.instance.Router().ClashServer().(*clashapi.Server).SetModeUpdateHook(s.modeUpdate)
} }
s.service = newService s.service = newService
if newService != nil {
s.urlTestListener = service.PtrFromContext[urltest.HistoryStorage](newService.ctx).AddListener(s.notifyURLTestUpdate)
}
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)
} }

View File

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

View File

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

View File

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

View File

@ -25,10 +25,11 @@ import (
) )
type BoxService struct { type BoxService struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
instance *box.Box instance *box.Box
pauseManager pause.Manager 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()) urlTestHistoryStorage := urltest.NewHistoryStorage()
sleepManager := pause.NewDefaultManager(ctx) ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
ctx = pause.ContextWithManager(ctx, sleepManager) 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, ctx: ctx,
cancel: cancel, cancel: cancel,
instance: instance, instance: instance,
pauseManager: sleepManager, 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()
}

4
go.mod
View File

@ -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 v0.2.10-0.20230824115837-8d731e68853a
github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659 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

8
go.sum
View File

@ -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.20230824115837-8d731e68853a h1:eV4HEz9NP7eAlQ/IHD6OF2VVM6ke4Vw6htuSAsvgtDk=
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.20230824120133-4d5cbceb40c1 h1:5w+jXz8y/8UQAxO74TjftN5okYkpg5mGvVxXunlKdqI=
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=

View File

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

View File

@ -1005,14 +1005,7 @@ func (r *Router) notifyNetworkUpdate(event int) {
} }
} }
conntrack.Close() r.ResetNetwork()
for _, outbound := range r.outbounds {
listener, isListener := outbound.(adapter.InterfaceUpdateListener)
if isListener {
listener.InterfaceUpdated()
}
}
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
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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