mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-04 21:33:32 +00:00
Separate Clash mode management from Clash API server
This commit is contained in:
@@ -8,18 +8,9 @@ import (
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
)
|
||||
|
||||
type ClashServer interface {
|
||||
LifecycleService
|
||||
Mode() string
|
||||
ModeList() []string
|
||||
SetMode(mode string)
|
||||
AddModeUpdateHook(hook *observable.Subscriber[struct{}])
|
||||
}
|
||||
|
||||
type URLTestHistory struct {
|
||||
Time time.Time `json:"time"`
|
||||
Delay uint16 `json:"delay"`
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/clashmode"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -157,7 +158,7 @@ func New(options Options) (*Box, error) {
|
||||
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
||||
needCacheFile = true
|
||||
}
|
||||
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
|
||||
if experimentalOptions.ClashAPI != nil {
|
||||
needClashAPI = true
|
||||
}
|
||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||
@@ -177,7 +178,7 @@ func New(options Options) (*Box, error) {
|
||||
logFactory, err := log.New(log.Options{
|
||||
Context: ctx,
|
||||
Options: common.PtrValueOrDefault(options.Log),
|
||||
Observable: needClashAPI || needAPIService,
|
||||
Observable: needClashAPI && experimentalOptions.ClashAPI.ExternalController != "",
|
||||
DefaultWriter: defaultLogWriter,
|
||||
BaseTime: createdAt,
|
||||
PlatformWriter: options.PlatformLogWriter,
|
||||
@@ -243,11 +244,18 @@ func New(options Options) (*Box, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize router")
|
||||
}
|
||||
if needClashAPI || needAPIService {
|
||||
if needClashAPI || needAPIService || options.PlatformLogWriter != nil {
|
||||
trafficManager := trafficcontrol.NewManager(outboundManager)
|
||||
service.MustRegisterPtr(ctx, trafficManager)
|
||||
router.AppendTracker(trafficManager)
|
||||
internalServices = append(internalServices, trafficManager)
|
||||
var clashDefaultMode string
|
||||
if experimentalOptions.ClashAPI != nil {
|
||||
clashDefaultMode = experimentalOptions.ClashAPI.DefaultMode
|
||||
}
|
||||
clashMode := clashmode.NewManager(ctx, logFactory.NewLogger("clash-mode"), clashDefaultMode, clashmode.CalculateModeList(options.Options))
|
||||
service.MustRegisterPtr(ctx, clashMode)
|
||||
internalServices = append(internalServices, clashMode)
|
||||
}
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
@@ -420,13 +428,10 @@ func New(options Options) (*Box, error) {
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
if needClashAPI {
|
||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(experimentalOptions.ClashAPI))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create clash-server")
|
||||
}
|
||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||
internalServices = append(internalServices, clashServer)
|
||||
}
|
||||
if needV2RayAPI {
|
||||
|
||||
@@ -3,8 +3,6 @@ package daemon
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
const defaultAttachedLogMaxLines = 3000
|
||||
@@ -20,9 +18,9 @@ func NewAttachedService(ctx context.Context) *StartedService {
|
||||
s.serviceStatus = &ServiceStatus{Status: ServiceStatus_STARTED}
|
||||
s.startedAt = time.Now()
|
||||
instance.urlTestHistoryStorage.AddUpdateHook(s.urlTestSubscriber)
|
||||
if instance.clashServer != nil {
|
||||
instance.clashServer.AddModeUpdateHook(s.clashModeSubscriber)
|
||||
if instance.clashMode != nil {
|
||||
instance.clashMode.AddUpdateHook(s.clashModeSubscriber)
|
||||
}
|
||||
instance.logFactory.(log.ObservableFactory).AttachPlatformWriter(s)
|
||||
instance.logFactory.AttachPlatformWriter(s)
|
||||
return s
|
||||
}
|
||||
|
||||
+4
-3
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/sagernet/sing-box/common/trafficcontrol"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/clashmode"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/experimental/locale"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -25,7 +26,7 @@ type Instance struct {
|
||||
cancel context.CancelFunc
|
||||
instance *box.Box
|
||||
connectionManager adapter.ConnectionManager
|
||||
clashServer adapter.ClashServer
|
||||
clashMode *clashmode.Manager
|
||||
trafficManager *trafficcontrol.Manager
|
||||
cacheFile adapter.CacheFile
|
||||
pauseManager pause.Manager
|
||||
@@ -130,7 +131,7 @@ func (s *StartedService) newInstance(ctx context.Context, profileContent string,
|
||||
}
|
||||
i.instance = boxInstance
|
||||
i.connectionManager = service.FromContext[adapter.ConnectionManager](ctx)
|
||||
i.clashServer = service.FromContext[adapter.ClashServer](ctx)
|
||||
i.clashMode = service.PtrFromContext[clashmode.Manager](ctx)
|
||||
i.trafficManager = service.PtrFromContext[trafficcontrol.Manager](ctx)
|
||||
i.pauseManager = service.FromContext[pause.Manager](ctx)
|
||||
i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
|
||||
@@ -145,7 +146,7 @@ func attachInstance(ctx context.Context) *Instance {
|
||||
return &Instance{
|
||||
ctx: ctx,
|
||||
connectionManager: service.FromContext[adapter.ConnectionManager](ctx),
|
||||
clashServer: service.FromContext[adapter.ClashServer](ctx),
|
||||
clashMode: service.PtrFromContext[clashmode.Manager](ctx),
|
||||
trafficManager: service.PtrFromContext[trafficcontrol.Manager](ctx),
|
||||
pauseManager: service.FromContext[pause.Manager](ctx),
|
||||
cacheFile: service.FromContext[adapter.CacheFile](ctx),
|
||||
|
||||
+12
-12
@@ -277,8 +277,8 @@ func (s *StartedService) StartOrReloadService(ctx context.Context, profileConten
|
||||
return err
|
||||
}
|
||||
instance.urlTestHistoryStorage.AddUpdateHook(s.urlTestSubscriber)
|
||||
if instance.clashServer != nil {
|
||||
instance.clashServer.AddModeUpdateHook(s.clashModeSubscriber)
|
||||
if instance.clashMode != nil {
|
||||
instance.clashMode.AddUpdateHook(s.clashModeSubscriber)
|
||||
}
|
||||
s.serviceAccess.Lock()
|
||||
s.instance = instance
|
||||
@@ -635,14 +635,14 @@ func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
clashServer := s.instance.clashServer
|
||||
clashMode := s.instance.clashMode
|
||||
s.serviceAccess.RUnlock()
|
||||
if clashServer == nil {
|
||||
if clashMode == nil {
|
||||
return nil, status.Error(codes.NotFound, "clash mode not available")
|
||||
}
|
||||
return &ClashModeStatus{
|
||||
ModeList: clashServer.ModeList(),
|
||||
CurrentMode: clashServer.Mode(),
|
||||
ModeList: clashMode.ModeList(),
|
||||
CurrentMode: clashMode.Mode(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -665,12 +665,12 @@ func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.Se
|
||||
s.serviceAccess.RLock()
|
||||
var message *ClashMode
|
||||
if s.serviceStatus.Status == ServiceStatus_STARTED {
|
||||
clashServer := s.instance.clashServer
|
||||
if clashServer == nil {
|
||||
clashMode := s.instance.clashMode
|
||||
if clashMode == nil {
|
||||
s.serviceAccess.RUnlock()
|
||||
return status.Error(codes.NotFound, "clash mode not available")
|
||||
}
|
||||
message = &ClashMode{Mode: clashServer.Mode()}
|
||||
message = &ClashMode{Mode: clashMode.Mode()}
|
||||
} else {
|
||||
message = &ClashMode{}
|
||||
}
|
||||
@@ -700,12 +700,12 @@ func (s *StartedService) SetClashMode(ctx context.Context, request *ClashMode) (
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
clashServer := s.instance.clashServer
|
||||
clashMode := s.instance.clashMode
|
||||
s.serviceAccess.RUnlock()
|
||||
if clashServer == nil {
|
||||
if clashMode == nil {
|
||||
return nil, status.Error(codes.NotFound, "clash mode not available")
|
||||
}
|
||||
clashServer.SetMode(request.Mode)
|
||||
clashMode.SetMode(request.Mode)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,13 @@ package experimental
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
type ClashServerConstructor = func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error)
|
||||
type ClashServerConstructor = func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.LifecycleService, error)
|
||||
|
||||
var clashServerConstructor ClashServerConstructor
|
||||
|
||||
@@ -20,62 +17,9 @@ func RegisterClashServerConstructor(constructor ClashServerConstructor) {
|
||||
clashServerConstructor = constructor
|
||||
}
|
||||
|
||||
func NewClashServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
|
||||
func NewClashServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.LifecycleService, error) {
|
||||
if clashServerConstructor == nil {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
return clashServerConstructor(ctx, logFactory, options)
|
||||
}
|
||||
|
||||
func CalculateClashModeList(options option.Options) []string {
|
||||
var clashModes []string
|
||||
clashModes = append(clashModes, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...)
|
||||
clashModes = append(clashModes, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...)
|
||||
clashModes = common.FilterNotDefault(common.Uniq(clashModes))
|
||||
predefinedOrder := []string{
|
||||
"Rule", "Global", "Direct",
|
||||
}
|
||||
var newClashModes []string
|
||||
for _, mode := range clashModes {
|
||||
if !common.Contains(predefinedOrder, mode) {
|
||||
newClashModes = append(newClashModes, mode)
|
||||
}
|
||||
}
|
||||
sort.Strings(newClashModes)
|
||||
for _, mode := range predefinedOrder {
|
||||
if common.Contains(clashModes, mode) {
|
||||
newClashModes = append(newClashModes, mode)
|
||||
}
|
||||
}
|
||||
return newClashModes
|
||||
}
|
||||
|
||||
func extraClashModeFromRule(rules []option.Rule) []string {
|
||||
var clashMode []string
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if rule.DefaultOptions.ClashMode != "" {
|
||||
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...)
|
||||
}
|
||||
}
|
||||
return clashMode
|
||||
}
|
||||
|
||||
func extraClashModeFromDNSRule(rules []option.DNSRule) []string {
|
||||
var clashMode []string
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if rule.DefaultOptions.ClashMode != "" {
|
||||
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...)
|
||||
}
|
||||
}
|
||||
return clashMode
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ func getConfigs(server *Server, logFactory log.Factory) func(w http.ResponseWrit
|
||||
logLevel = log.LevelError
|
||||
}
|
||||
render.JSON(w, r, &configSchema{
|
||||
Mode: server.mode,
|
||||
ModeList: server.modeList,
|
||||
Mode: server.clashMode.Mode(),
|
||||
ModeList: server.clashMode.ModeList(),
|
||||
BindAddress: "*",
|
||||
LogLevel: log.FormatLevel(logLevel),
|
||||
})
|
||||
@@ -60,7 +60,7 @@ func patchConfigs(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if newConfig.Mode != "" {
|
||||
server.SetMode(newConfig.Mode)
|
||||
server.clashMode.SetMode(newConfig.Mode)
|
||||
}
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -19,12 +18,12 @@ import (
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/clashmode"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
"github.com/sagernet/ws"
|
||||
@@ -38,7 +37,7 @@ func init() {
|
||||
experimental.RegisterClashServerConstructor(NewServer)
|
||||
}
|
||||
|
||||
var _ adapter.ClashServer = (*Server)(nil)
|
||||
var _ adapter.LifecycleService = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
ctx context.Context
|
||||
@@ -51,20 +50,16 @@ type Server struct {
|
||||
httpServer *http.Server
|
||||
trafficManager *trafficcontrol.Manager
|
||||
urlTestHistory *urltest.HistoryStorage
|
||||
clashMode *clashmode.Manager
|
||||
logDebug bool
|
||||
|
||||
mode string
|
||||
modeList []string
|
||||
modeUpdateAccess sync.Mutex
|
||||
modeUpdateHooks []*observable.Subscriber[struct{}]
|
||||
|
||||
externalController bool
|
||||
externalUI string
|
||||
externalUIDownloadURL string
|
||||
externalUIDownloadDetour string
|
||||
}
|
||||
|
||||
func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
|
||||
func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.LifecycleService, error) {
|
||||
trafficManager := service.PtrFromContext[trafficcontrol.Manager](ctx)
|
||||
if trafficManager == nil {
|
||||
return nil, E.New("missing traffic manager")
|
||||
@@ -73,6 +68,10 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
||||
if urlTestHistory == nil {
|
||||
return nil, E.New("missing URL test history storage")
|
||||
}
|
||||
clashMode := service.PtrFromContext[clashmode.Manager](ctx)
|
||||
if clashMode == nil {
|
||||
return nil, E.New("missing clash mode manager")
|
||||
}
|
||||
chiRouter := chi.NewRouter()
|
||||
s := &Server{
|
||||
ctx: ctx,
|
||||
@@ -88,20 +87,12 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
||||
},
|
||||
trafficManager: trafficManager,
|
||||
urlTestHistory: urlTestHistory,
|
||||
clashMode: clashMode,
|
||||
logDebug: logFactory.Level() >= log.LevelDebug,
|
||||
modeList: options.ModeList,
|
||||
externalController: options.ExternalController != "",
|
||||
externalUIDownloadURL: options.ExternalUIDownloadURL,
|
||||
externalUIDownloadDetour: options.ExternalUIDownloadDetour,
|
||||
}
|
||||
defaultMode := "Rule"
|
||||
if options.DefaultMode != "" {
|
||||
defaultMode = options.DefaultMode
|
||||
}
|
||||
if !common.Contains(s.modeList, defaultMode) {
|
||||
s.modeList = append([]string{defaultMode}, s.modeList...)
|
||||
}
|
||||
s.mode = defaultMode
|
||||
//goland:noinspection GoDeprecation
|
||||
//nolint:staticcheck
|
||||
if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" {
|
||||
@@ -157,45 +148,34 @@ func (s *Server) Name() string {
|
||||
}
|
||||
|
||||
func (s *Server) Start(stage adapter.StartStage) error {
|
||||
switch stage {
|
||||
case adapter.StartStateStart:
|
||||
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
|
||||
}
|
||||
}
|
||||
case adapter.StartStateStarted:
|
||||
if s.externalController {
|
||||
s.checkAndDownloadExternalUI()
|
||||
var (
|
||||
listener net.Listener
|
||||
err error
|
||||
)
|
||||
for range 3 {
|
||||
listener, err = net.Listen("tcp", s.httpServer.Addr)
|
||||
if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return E.Cause(err, "external controller listen error")
|
||||
}
|
||||
s.logger.Info("restful api listening at ", listener.Addr())
|
||||
go func() {
|
||||
err = s.httpServer.Serve(listener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Error("external controller serve error: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if stage != adapter.StartStateStarted {
|
||||
return nil
|
||||
}
|
||||
if s.externalController {
|
||||
s.checkAndDownloadExternalUI()
|
||||
var (
|
||||
listener net.Listener
|
||||
err error
|
||||
)
|
||||
for range 3 {
|
||||
listener, err = net.Listen("tcp", s.httpServer.Addr)
|
||||
if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return E.Cause(err, "external controller listen error")
|
||||
}
|
||||
s.logger.Info("restful api listening at ", listener.Addr())
|
||||
go func() {
|
||||
err = s.httpServer.Serve(listener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Error("external controller serve error: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -205,49 +185,6 @@ func (s *Server) Close() error {
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Server) Mode() string {
|
||||
return s.mode
|
||||
}
|
||||
|
||||
func (s *Server) ModeList() []string {
|
||||
return s.modeList
|
||||
}
|
||||
|
||||
func (s *Server) AddModeUpdateHook(hook *observable.Subscriber[struct{}]) {
|
||||
s.modeUpdateAccess.Lock()
|
||||
defer s.modeUpdateAccess.Unlock()
|
||||
s.modeUpdateHooks = append(s.modeUpdateHooks, 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
|
||||
s.modeUpdateAccess.Lock()
|
||||
for _, hook := range s.modeUpdateHooks {
|
||||
hook.Emit(struct{}{})
|
||||
}
|
||||
s.modeUpdateAccess.Unlock()
|
||||
s.dnsRouter.ClearCache()
|
||||
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
|
||||
if cacheFile != nil {
|
||||
err := cacheFile.StoreMode(newMode)
|
||||
if err != nil {
|
||||
s.logger.Error(E.Cause(err, "save mode"))
|
||||
}
|
||||
}
|
||||
s.logger.Info("updated mode: ", newMode)
|
||||
}
|
||||
|
||||
func authentication(serverSecret string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package clashmode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
ctx context.Context
|
||||
logger log.Logger
|
||||
dnsRouter adapter.DNSRouter
|
||||
mode string
|
||||
modeList []string
|
||||
updateAccess sync.Mutex
|
||||
updateHooks []*observable.Subscriber[struct{}]
|
||||
}
|
||||
|
||||
func NewManager(ctx context.Context, logger log.Logger, defaultMode string, modeList []string) *Manager {
|
||||
if defaultMode == "" {
|
||||
defaultMode = "Rule"
|
||||
}
|
||||
if !common.Contains(modeList, defaultMode) {
|
||||
modeList = append([]string{defaultMode}, modeList...)
|
||||
}
|
||||
return &Manager{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
mode: defaultMode,
|
||||
modeList: modeList,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Name() string {
|
||||
return "clash mode manager"
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
cacheFile := service.FromContext[adapter.CacheFile](m.ctx)
|
||||
if cacheFile != nil {
|
||||
mode := cacheFile.LoadMode()
|
||||
if common.Any(m.modeList, func(it string) bool {
|
||||
return strings.EqualFold(it, mode)
|
||||
}) {
|
||||
m.mode = mode
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Mode() string {
|
||||
return m.mode
|
||||
}
|
||||
|
||||
func (m *Manager) ModeList() []string {
|
||||
return m.modeList
|
||||
}
|
||||
|
||||
func (m *Manager) AddUpdateHook(hook *observable.Subscriber[struct{}]) {
|
||||
m.updateAccess.Lock()
|
||||
defer m.updateAccess.Unlock()
|
||||
m.updateHooks = append(m.updateHooks, hook)
|
||||
}
|
||||
|
||||
func (m *Manager) SetMode(newMode string) {
|
||||
if !common.Contains(m.modeList, newMode) {
|
||||
newMode = common.Find(m.modeList, func(it string) bool {
|
||||
return strings.EqualFold(it, newMode)
|
||||
})
|
||||
}
|
||||
if !common.Contains(m.modeList, newMode) {
|
||||
return
|
||||
}
|
||||
if newMode == m.mode {
|
||||
return
|
||||
}
|
||||
m.mode = newMode
|
||||
m.updateAccess.Lock()
|
||||
for _, hook := range m.updateHooks {
|
||||
hook.Emit(struct{}{})
|
||||
}
|
||||
m.updateAccess.Unlock()
|
||||
m.dnsRouter.ClearCache()
|
||||
cacheFile := service.FromContext[adapter.CacheFile](m.ctx)
|
||||
if cacheFile != nil {
|
||||
err := cacheFile.StoreMode(newMode)
|
||||
if err != nil {
|
||||
m.logger.Error(E.Cause(err, "save mode"))
|
||||
}
|
||||
}
|
||||
m.logger.Info("updated mode: ", newMode)
|
||||
}
|
||||
|
||||
var _ adapter.LifecycleService = (*Manager)(nil)
|
||||
@@ -0,0 +1,62 @@
|
||||
package clashmode
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
func CalculateModeList(options option.Options) []string {
|
||||
var clashModes []string
|
||||
clashModes = append(clashModes, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...)
|
||||
clashModes = append(clashModes, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...)
|
||||
clashModes = common.FilterNotDefault(common.Uniq(clashModes))
|
||||
predefinedOrder := []string{
|
||||
"Rule", "Global", "Direct",
|
||||
}
|
||||
var newClashModes []string
|
||||
for _, mode := range clashModes {
|
||||
if !common.Contains(predefinedOrder, mode) {
|
||||
newClashModes = append(newClashModes, mode)
|
||||
}
|
||||
}
|
||||
sort.Strings(newClashModes)
|
||||
for _, mode := range predefinedOrder {
|
||||
if common.Contains(clashModes, mode) {
|
||||
newClashModes = append(newClashModes, mode)
|
||||
}
|
||||
}
|
||||
return newClashModes
|
||||
}
|
||||
|
||||
func extraClashModeFromRule(rules []option.Rule) []string {
|
||||
var clashMode []string
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if rule.DefaultOptions.ClashMode != "" {
|
||||
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...)
|
||||
}
|
||||
}
|
||||
return clashMode
|
||||
}
|
||||
|
||||
func extraClashModeFromDNSRule(rules []option.DNSRule) []string {
|
||||
var clashMode []string
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if rule.DefaultOptions.ClashMode != "" {
|
||||
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...)
|
||||
}
|
||||
}
|
||||
return clashMode
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
experimental.RegisterClashServerConstructor(func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
|
||||
experimental.RegisterClashServerConstructor(func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.LifecycleService, error) {
|
||||
return nil, E.New(`clash api is not included in this build, rebuild with -tags with_clash_api`)
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,12 +17,12 @@ type Factory interface {
|
||||
SetLevel(level Level)
|
||||
Logger() ContextLogger
|
||||
NewLogger(tag string) ContextLogger
|
||||
AttachPlatformWriter(writer PlatformWriter)
|
||||
}
|
||||
|
||||
type ObservableFactory interface {
|
||||
Factory
|
||||
observable.Observable[Entry]
|
||||
AttachPlatformWriter(writer PlatformWriter)
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
|
||||
@@ -26,7 +26,6 @@ type ClashAPIOptions struct {
|
||||
ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty" reference:"outbound"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
DefaultMode string `json:"default_mode,omitempty"`
|
||||
ModeList []string `json:"-"`
|
||||
AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"`
|
||||
AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"`
|
||||
|
||||
|
||||
@@ -5,15 +5,16 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/experimental/clashmode"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*ClashModeItem)(nil)
|
||||
|
||||
type ClashModeItem struct {
|
||||
ctx context.Context
|
||||
clashServer adapter.ClashServer
|
||||
mode string
|
||||
ctx context.Context
|
||||
clashMode *clashmode.Manager
|
||||
mode string
|
||||
}
|
||||
|
||||
func NewClashModeItem(ctx context.Context, mode string) *ClashModeItem {
|
||||
@@ -24,15 +25,15 @@ func NewClashModeItem(ctx context.Context, mode string) *ClashModeItem {
|
||||
}
|
||||
|
||||
func (r *ClashModeItem) Start() error {
|
||||
r.clashServer = service.FromContext[adapter.ClashServer](r.ctx)
|
||||
r.clashMode = service.PtrFromContext[clashmode.Manager](r.ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ClashModeItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if r.clashServer == nil {
|
||||
if r.clashMode == nil {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(r.clashServer.Mode(), r.mode)
|
||||
return strings.EqualFold(r.clashMode.Mode(), r.mode)
|
||||
}
|
||||
|
||||
func (r *ClashModeItem) String() string {
|
||||
|
||||
Reference in New Issue
Block a user