From 166245f44fd60a2c0c54665b0297642131caf886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 12:00:28 +0800 Subject: [PATCH 01/15] Migrate to independent cache file --- adapter/experimental.go | 10 ++- box.go | 53 ++++++++++-- .../{clashapi => }/cachefile/cache.go | 85 +++++++++++++------ .../{clashapi => }/cachefile/fakeip.go | 0 experimental/clashapi/cache.go | 11 ++- experimental/clashapi/server.go | 66 ++++---------- experimental/libbox/command_group.go | 21 ++--- experimental/libbox/command_urltest.go | 21 ++--- option/clash.go | 31 ------- option/experimental.go | 47 +++++++++- option/group.go | 15 ++++ option/v2ray.go | 12 --- outbound/builder.go | 2 +- outbound/selector.go | 15 ++-- route/router.go | 2 +- transport/fakeip/store.go | 15 ++-- 16 files changed, 227 insertions(+), 179 deletions(-) rename experimental/{clashapi => }/cachefile/cache.go (81%) rename experimental/{clashapi => }/cachefile/fakeip.go (100%) delete mode 100644 option/clash.go create mode 100644 option/group.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 87eb936c4f..3ba9419eb1 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -13,15 +13,17 @@ type ClashServer interface { PreStarter Mode() string ModeList() []string - StoreSelected() bool - StoreFakeIP() bool - CacheFile() ClashCacheFile HistoryStorage() *urltest.HistoryStorage 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) } -type ClashCacheFile interface { +type CacheFile interface { + Service + PreStarter + + StoreFakeIP() bool + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string diff --git a/box.go b/box.go index 3c0479c770..5bc8bdcf36 100644 --- a/box.go +++ b/box.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "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/inbound" "github.com/sagernet/sing-box/log" @@ -32,7 +33,8 @@ type Box struct { outbounds []adapter.Outbound logFactory log.Factory logger log.ContextLogger - preServices map[string]adapter.Service + preServices1 map[string]adapter.Service + preServices2 map[string]adapter.Service postServices map[string]adapter.Service done chan struct{} } @@ -45,17 +47,21 @@ type Options struct { } func New(options Options) (*Box, error) { + createdAt := time.Now() ctx := options.Context if ctx == nil { ctx = context.Background() } ctx = service.ContextWithDefaultRegistry(ctx) ctx = pause.ContextWithDefaultManager(ctx) - createdAt := time.Now() experimentalOptions := common.PtrValueOrDefault(options.Experimental) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + var needCacheFile bool var needClashAPI bool var needV2RayAPI bool + if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { + needCacheFile = true + } if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil { needClashAPI = true } @@ -145,8 +151,14 @@ func New(options Options) (*Box, error) { 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) + if needCacheFile { + cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + preServices1["cache file"] = cacheFile + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) 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") } router.SetClashServer(clashServer) - preServices["clash api"] = clashServer + preServices2["clash api"] = clashServer } if needV2RayAPI { 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") } router.SetV2RayServer(v2rayServer) - preServices["v2ray api"] = v2rayServer + preServices2["v2ray api"] = v2rayServer } return &Box{ router: router, @@ -172,7 +184,8 @@ func New(options Options) (*Box, error) { createdAt: createdAt, logFactory: logFactory, logger: logFactory.Logger(), - preServices: preServices, + preServices1: preServices1, + preServices2: preServices2, postServices: postServices, done: make(chan struct{}), }, nil @@ -217,7 +230,16 @@ func (s *Box) Start() 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 { s.logger.Trace("pre-start ", serviceName) err := preService.PreStart() @@ -238,7 +260,14 @@ func (s *Box) start() error { if err != nil { 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) err = service.Start() if err != nil { @@ -314,7 +343,13 @@ func (s *Box) Close() error { 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) errors = E.Append(errors, service.Close(), func(err error) error { return E.Cause(err, "close ", serviceName) diff --git a/experimental/clashapi/cachefile/cache.go b/experimental/cachefile/cache.go similarity index 81% rename from experimental/clashapi/cachefile/cache.go rename to experimental/cachefile/cache.go index 0911829749..262d1c1e4b 100644 --- a/experimental/clashapi/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service/filemanager" @@ -31,11 +32,15 @@ var ( cacheIDDefault = []byte("default") ) -var _ adapter.ClashCacheFile = (*CacheFile)(nil) +var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { + ctx context.Context + path string + cacheID []byte + storeFakeIP bool + DB *bbolt.DB - cacheID []byte saveAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr @@ -43,7 +48,29 @@ type CacheFile struct { 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 options := bbolt.Options{Timeout: time.Second} var ( @@ -51,7 +78,7 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) err error ) for i := 0; i < 10; i++ { - db, err = bbolt.Open(path, fileMode, &options) + db, err = bbolt.Open(c.path, fileMode, &options) if err == nil { break } @@ -59,23 +86,20 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) continue } if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { - rmErr := os.Remove(path) + rmErr := os.Remove(c.path) if rmErr != nil { - return nil, err + return err } } time.Sleep(100 * time.Millisecond) } if err != nil { - return nil, err + return err } - err = filemanager.Chown(ctx, path) + err = filemanager.Chown(c.ctx, c.path) if err != nil { - return nil, E.Cause(err, "platform chown") - } - var cacheIDBytes []byte - if cacheID != "" { - cacheIDBytes = append([]byte{0}, []byte(cacheID)...) + db.Close() + return E.Cause(err, "platform chown") } err = db.Batch(func(tx *bbolt.Tx) error { 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 { - return nil, err + db.Close() + return err } - return &CacheFile{ - DB: db, - cacheID: cacheIDBytes, - saveDomain: make(map[netip.Addr]string), - saveAddress4: make(map[string]netip.Addr), - saveAddress6: make(map[string]netip.Addr), - }, nil + c.DB = db + return nil +} + +func (c *CacheFile) PreStart() error { + return c.start() +} + +func (c *CacheFile) Start() error { + return nil +} + +func (c *CacheFile) Close() error { + if c.DB == nil { + return nil + } + return c.DB.Close() +} + +func (c *CacheFile) StoreFakeIP() bool { + return c.storeFakeIP } func (c *CacheFile) LoadMode() string { @@ -218,7 +257,3 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } - -func (c *CacheFile) Close() error { - return c.DB.Close() -} diff --git a/experimental/clashapi/cachefile/fakeip.go b/experimental/cachefile/fakeip.go similarity index 100% rename from experimental/clashapi/cachefile/fakeip.go rename to experimental/cachefile/fakeip.go diff --git a/experimental/clashapi/cache.go b/experimental/clashapi/cache.go index 7582fde5ae..9c088a82f7 100644 --- a/experimental/clashapi/cache.go +++ b/experimental/clashapi/cache.go @@ -1,23 +1,26 @@ package clashapi import ( + "context" "net/http" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func cacheRouter(router adapter.Router) http.Handler { +func cacheRouter(ctx context.Context) http.Handler { r := chi.NewRouter() - r.Post("/fakeip/flush", flushFakeip(router)) + r.Post("/fakeip/flush", flushFakeip(ctx)) 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) { - if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile != nil { err := cacheFile.FakeIPReset() if err != nil { render.Status(r, http.StatusInternalServerError) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 6a3d6f66f7..c40ff93846 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -15,7 +15,6 @@ 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/clashapi/cachefile" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -49,12 +48,6 @@ type Server struct { mode string modeList []string modeUpdateHook chan<- struct{} - storeMode bool - storeSelected bool - storeFakeIP bool - cacheFilePath string - cacheID string - cacheFile adapter.ClashCacheFile externalController bool externalUI string @@ -76,9 +69,6 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ trafficManager: trafficManager, modeList: options.ModeList, externalController: options.ExternalController != "", - storeMode: options.StoreMode, - storeSelected: options.StoreSelected, - storeFakeIP: options.StoreFakeIP, externalUIDownloadURL: options.ExternalUIDownloadURL, 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.mode = defaultMode - if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" { - cachePath := os.ExpandEnv(options.CacheFile) - if cachePath == "" { - cachePath = "cache.db" - } - if foundPath, loaded := C.FindPath(cachePath); loaded { - cachePath = foundPath - } else { - cachePath = filemanager.BasePath(ctx, cachePath) - } - server.cacheFilePath = cachePath - server.cacheID = options.CacheID + //goland:noinspection GoDeprecation + //nolint:staticcheck + if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { + return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") } cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -128,7 +110,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) - r.Mount("/cache", cacheRouter(router)) + r.Mount("/cache", cacheRouter(ctx)) r.Mount("/dns", dnsRouter(router)) server.setupMetaAPI(r) @@ -147,19 +129,13 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ } func (s *Server) PreStart() error { - if s.cacheFilePath != "" { - cacheFile, err := cachefile.Open(s.ctx, s.cacheFilePath, s.cacheID) - if err != nil { - return E.Cause(err, "open cache file") - } - s.cacheFile = cacheFile - if s.storeMode { - mode := s.cacheFile.LoadMode() - if common.Any(s.modeList, func(it string) bool { - return strings.EqualFold(it, mode) - }) { - s.mode = mode - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + mode := cacheFile.LoadMode() + if common.Any(s.modeList, func(it string) bool { + return strings.EqualFold(it, mode) + }) { + s.mode = mode } } return nil @@ -187,7 +163,6 @@ func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), s.trafficManager, - s.cacheFile, s.urlTestHistory, ) } @@ -224,8 +199,9 @@ func (s *Server) SetMode(newMode string) { } } s.router.ClearDNSCache() - if s.storeMode { - err := s.cacheFile.StoreMode(newMode) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreMode(newMode) if err != nil { s.logger.Error(E.Cause(err, "save mode")) } @@ -233,18 +209,6 @@ func (s *Server) SetMode(newMode string) { 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 { return s.urlTestHistory } diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 934820889e..2fc69b98b4 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -159,11 +159,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) { func writeGroups(writer io.Writer, boxService *BoxService) error { historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) - var cacheFile adapter.ClashCacheFile - if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil { - cacheFile = clashServer.CacheFile() - } - + cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) outbounds := boxService.instance.Router().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { @@ -288,16 +284,15 @@ func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return writeError(conn, E.New("service not ready")) } - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - err = cacheFile.StoreGroupExpand(groupTag, isExpand) - if err != nil { - return writeError(conn, err) - } + cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx) + if cacheFile != nil { + err = cacheFile.StoreGroupExpand(groupTag, isExpand) + if err != nil { + return writeError(conn, err) } } return writeError(conn, nil) diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 88e86a8f8c..3563d8c6a3 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/service" ) func (c *CommandClient) URLTest(groupTag string) error { @@ -37,11 +38,11 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return nil } - abstractOutboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag) if !isLoaded { return writeError(conn, E.New("outbound group not found: ", groupTag)) } @@ -53,15 +54,9 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if isURLTest { go urlTest.CheckOutbounds() } else { - var historyStorage *urltest.HistoryStorage - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - historyStorage = clashServer.HistoryStorage() - } else { - return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group")) - } - + historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx) outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := service.instance.Router().Outbound(it) + itOutbound, _ := serviceNow.instance.Router().Outbound(it) return itOutbound }), func(it adapter.Outbound) bool { if it == nil { @@ -73,12 +68,12 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { } return true }) - b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10)) + b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10)) for _, detour := range outbounds { outboundToTest := detour outboundTag := outboundToTest.Tag() b.Go(outboundTag, func() (any, error) { - t, err := urltest.URLTest(service.ctx, "", outboundToTest) + t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest) if err != nil { historyStorage.DeleteURLTestHistory(outboundTag) } else { diff --git a/option/clash.go b/option/clash.go deleted file mode 100644 index 63ee2aebdf..0000000000 --- a/option/clash.go +++ /dev/null @@ -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"` -} diff --git a/option/experimental.go b/option/experimental.go index a5b6acbd32..72751a590e 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -1,7 +1,48 @@ package option type ExperimentalOptions struct { - ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` - V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` - Debug *DebugOptions `json:"debug,omitempty"` + CacheFile *CacheFileOptions `json:"cache_file,omitempty"` + ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` + V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` + Debug *DebugOptions `json:"debug,omitempty"` +} + +type CacheFileOptions struct { + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` +} + +type ClashAPIOptions struct { + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + + // Deprecated: migrated to global cache file + StoreMode bool `json:"store_mode,omitempty"` + // Deprecated: migrated to global cache file + StoreSelected bool `json:"store_selected,omitempty"` + // Deprecated: migrated to global cache file + StoreFakeIP bool `json:"store_fakeip,omitempty"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` +} + +type V2RayAPIOptions struct { + Listen string `json:"listen,omitempty"` + Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` +} + +type V2RayStatsServiceOptions struct { + Enabled bool `json:"enabled,omitempty"` + Inbounds []string `json:"inbounds,omitempty"` + Outbounds []string `json:"outbounds,omitempty"` + Users []string `json:"users,omitempty"` } diff --git a/option/group.go b/option/group.go new file mode 100644 index 0000000000..58824e808b --- /dev/null +++ b/option/group.go @@ -0,0 +1,15 @@ +package option + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/option/v2ray.go b/option/v2ray.go index 37f7b8c4b0..774a651d1d 100644 --- a/option/v2ray.go +++ b/option/v2ray.go @@ -1,13 +1 @@ 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"` -} diff --git a/outbound/builder.go b/outbound/builder.go index 141758d870..e4d6a80e06 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -56,7 +56,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t case C.TypeHysteria2: return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) case C.TypeSelector: - return NewSelector(router, logger, tag, options.SelectorOptions) + return NewSelector(ctx, router, logger, tag, options.SelectorOptions) case C.TypeURLTest: return NewURLTest(ctx, router, logger, tag, options.URLTestOptions) default: diff --git a/outbound/selector.go b/outbound/selector.go index c66591cdee..e801daeadc 100644 --- a/outbound/selector.go +++ b/outbound/selector.go @@ -12,6 +12,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) var ( @@ -21,6 +22,7 @@ var ( type Selector struct { myOutboundAdapter + ctx context.Context tags []string defaultTag string outbounds map[string]adapter.Outbound @@ -29,7 +31,7 @@ type Selector struct { 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{ myOutboundAdapter: myOutboundAdapter{ protocol: C.TypeSelector, @@ -38,6 +40,7 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op tag: tag, dependencies: options.Outbounds, }, + ctx: ctx, tags: options.Outbounds, defaultTag: options.Default, outbounds: make(map[string]adapter.Outbound), @@ -67,8 +70,9 @@ func (s *Selector) Start() error { } if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - selected := clashServer.CacheFile().LoadSelected(s.tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + selected := cacheFile.LoadSelected(s.tag) if selected != "" { detour, loaded := s.outbounds[selected] if loaded { @@ -110,8 +114,9 @@ func (s *Selector) SelectOutbound(tag string) bool { } s.selected = detour if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - err := clashServer.CacheFile().StoreSelected(s.tag, tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreSelected(s.tag, tag) if err != nil { s.logger.Error("store selected: ", err) } diff --git a/route/router.go b/route/router.go index b5f0bbbf34..2e5514ac3a 100644 --- a/route/router.go +++ b/route/router.go @@ -262,7 +262,7 @@ func NewRouter( if fakeIPOptions.Inet6Range != nil { 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() diff --git a/transport/fakeip/store.go b/transport/fakeip/store.go index 96f6bf031e..83677b0d05 100644 --- a/transport/fakeip/store.go +++ b/transport/fakeip/store.go @@ -1,17 +1,19 @@ package fakeip import ( + "context" "net/netip" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" ) var _ adapter.FakeIPStore = (*Store)(nil) type Store struct { - router adapter.Router + ctx context.Context logger logger.Logger inet4Range netip.Prefix inet6Range netip.Prefix @@ -20,9 +22,9 @@ type Store struct { 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{ - router: router, + ctx: ctx, logger: logger, inet4Range: inet4Range, inet6Range: inet6Range, @@ -31,10 +33,9 @@ func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Pref func (s *Store) Start() error { var storage adapter.FakeIPStorage - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - storage = cacheFile - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil && cacheFile.StoreFakeIP() { + storage = cacheFile } if storage == nil { storage = NewMemoryStorage() From 097fc344a5c1af0d92c1637626562351ffdd916c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 23:47:32 +0800 Subject: [PATCH 02/15] Allow nested logical rules --- experimental/clashapi.go | 42 +++++++++++++++++++++++------------ option/rule.go | 21 +++++++++++++----- option/rule_dns.go | 25 +++++++++++++++------ route/router.go | 4 ++-- route/router_geo_resources.go | 12 ++++------ route/rule_default.go | 8 +++---- route/rule_dns.go | 8 +++---- 7 files changed, 76 insertions(+), 44 deletions(-) diff --git a/experimental/clashapi.go b/experimental/clashapi.go index 894d40a74a..805fbd5be7 100644 --- a/experimental/clashapi.go +++ b/experimental/clashapi.go @@ -5,6 +5,7 @@ import ( "os" "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" @@ -27,24 +28,37 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O 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) + clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) + clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) + clashMode = common.FilterNotDefault(common.Uniq(clashMode)) + return clashMode +} + +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)...) } } - 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 +} + +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 diff --git a/option/rule.go b/option/rule.go index 8caba96e57..4f4042025f 100644 --- a/option/rule.go +++ b/option/rule.go @@ -53,6 +53,17 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error { return nil } +func (r Rule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + type DefaultRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -92,12 +103,12 @@ func (r DefaultRule) IsValid() bool { } type LogicalRule struct { - Mode string `json:"mode"` - Rules []DefaultRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Mode string `json:"mode"` + Rules []Rule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r LogicalRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid) } diff --git a/option/rule_dns.go b/option/rule_dns.go index ba572b9aa2..fca3432293 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -53,6 +53,17 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error { return nil } +func (r DNSRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown DNS rule type: " + r.Type) + } +} + type DefaultDNSRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -96,14 +107,14 @@ func (r DefaultDNSRule) IsValid() bool { } type LogicalDNSRule struct { - Mode string `json:"mode"` - Rules []DefaultDNSRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Mode string `json:"mode"` + Rules []DNSRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r LogicalDNSRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) } diff --git a/route/router.go b/route/router.go index 2e5514ac3a..2145583101 100644 --- a/route/router.go +++ b/route/router.go @@ -128,14 +128,14 @@ func NewRouter( Logger: router.dnsLogger, }) for i, ruleOptions := range options.Rules { - routeRule, err := NewRule(router, router.logger, ruleOptions) + routeRule, err := NewRule(router, router.logger, ruleOptions, true) if err != nil { return nil, E.Cause(err, "parse rule[", i, "]") } router.rules = append(router.rules, routeRule) } for i, dnsRuleOptions := range dnsOptions.Rules { - dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions) + dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions, true) if err != nil { return nil, E.Cause(err, "parse dns rule[", i, "]") } diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index 8715cf922c..638d00df62 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -252,10 +252,8 @@ func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasRule(rule.LogicalOptions.Rules, cond) { + return true } } } @@ -270,10 +268,8 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasDNSRule(rule.LogicalOptions.Rules, cond) { + return true } } } diff --git a/route/rule_default.go b/route/rule_default.go index 2d62f97a46..8c8473abf5 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule) (adapter.Rule, error) { +func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Outbound == "" { + if options.DefaultOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewDefaultRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rul if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Outbound == "" { + if options.LogicalOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewLogicalRule(router, logger, options.LogicalOptions) @@ -220,7 +220,7 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultRule(router, logger, subRule) + rule, err := NewRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule_dns.go b/route/rule_dns.go index 5132f02456..b444932582 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule) (adapter.DNSRule, error) { +func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Server == "" { + if options.DefaultOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewDefaultDNSRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option. if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Server == "" { + if options.LogicalOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewLogicalDNSRule(router, logger, options.LogicalOptions) @@ -228,7 +228,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultDNSRule(router, logger, subRule) + rule, err := NewDNSRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } From 2e4c3067f2082c341147d414cb19e0e51271a3e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 17:35:40 +0800 Subject: [PATCH 03/15] Update buffer usage --- go.mod | 2 +- go.sum | 4 ++-- outbound/dns.go | 6 +++--- route/router.go | 2 -- transport/vless/vision.go | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index f9205f9f8b..93ebd1cbad 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 github.com/sagernet/quic-go v0.40.0 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.2.18 + github.com/sagernet/sing v0.2.19-0.20231205104330-6f79e46c4dd3 github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-mux v0.1.5 github.com/sagernet/sing-quic v0.1.5 diff --git a/go.sum b/go.sum index 1319f534d4..9d3e4a4b65 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ 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/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.2.18 h1:2Ce4dl0pkWft+4914NGXPb8OiQpgA8UHQ9xFOmgvKuY= -github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.19-0.20231205104330-6f79e46c4dd3 h1:WdtgW8IIPXokHPzqztKM4qhc/2/bISizIjdz++BkKR0= +github.com/sagernet/sing v0.2.19-0.20231205104330-6f79e46c4dd3/go.mod h1:Ce5LNojQOgOiWhiD8pPD6E9H7e2KgtOe3Zxx4Ou5u80= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= github.com/sagernet/sing-mux v0.1.5 h1:jUbYth9QQd1wsDmU8Ush+fKce7lNo9TMv2dp8PJtSOY= diff --git a/outbound/dns.go b/outbound/dns.go index 3b2ad5e3ca..74adb3ae70 100644 --- a/outbound/dns.go +++ b/outbound/dns.go @@ -165,6 +165,7 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada } timeout.Update() responseBuffer := buf.NewPacket() + responseBuffer.Resize(1024, 0) n, err := response.PackBuffer(responseBuffer.FreeBytes()) if err != nil { cancel(err) @@ -194,9 +195,7 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa group.Append0(func(ctx context.Context) error { var buffer *buf.Buffer readWaiter.InitializeReadWaiter(func() *buf.Buffer { - buffer = buf.NewSize(dns.FixedPacketSize) - buffer.FullReset() - return buffer + return buf.NewSize(dns.FixedPacketSize) }) defer readWaiter.InitializeReadWaiter(nil) for { @@ -243,6 +242,7 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa } timeout.Update() responseBuffer := buf.NewPacket() + responseBuffer.Resize(1024, 0) n, err := response.PackBuffer(responseBuffer.FreeBytes()) if err != nil { cancel(err) diff --git a/route/router.go b/route/router.go index 2145583101..dc98f5e22f 100644 --- a/route/router.go +++ b/route/router.go @@ -652,7 +652,6 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad if metadata.InboundOptions.SniffEnabled { buffer := buf.NewPacket() - buffer.FullReset() sniffMetadata, err := sniff.PeekStream(ctx, conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), sniff.StreamDomainNameQuery, sniff.TLSClientHello, sniff.HTTPHost) if sniffMetadata != nil { metadata.Protocol = sniffMetadata.Protocol @@ -768,7 +767,6 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() { buffer := buf.NewPacket() - buffer.FullReset() destination, err := conn.ReadPacket(buffer) if err != nil { buffer.Release() diff --git a/transport/vless/vision.go b/transport/vless/vision.go index 3851f6d52e..6919ce8176 100644 --- a/transport/vless/vision.go +++ b/transport/vless/vision.go @@ -134,7 +134,7 @@ func (c *VisionConn) Read(p []byte) (n int, err error) { buffers = common.Map(buffers, func(it *buf.Buffer) *buf.Buffer { return it.ToOwned() }) - chunkBuffer.FullReset() + chunkBuffer.Reset() } if c.remainingContent == 0 && c.remainingPadding == 0 { if c.currentCommand == commandPaddingEnd { From e4162b28f1a18202cb68f95e66ca6fac84797dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Dec 2023 13:24:12 +0800 Subject: [PATCH 04/15] Add rule-set --- .gitignore | 1 + adapter/experimental.go | 66 ++- adapter/inbound.go | 17 +- adapter/router.go | 30 +- box.go | 4 + cmd/sing-box/cmd_format.go | 39 -- cmd/sing-box/cmd_geoip.go | 43 ++ cmd/sing-box/cmd_geoip_export.go | 98 ++++ cmd/sing-box/cmd_geoip_list.go | 31 ++ cmd/sing-box/cmd_geoip_lookup.go | 47 ++ cmd/sing-box/cmd_geosite.go | 41 ++ cmd/sing-box/cmd_geosite_export.go | 81 +++ cmd/sing-box/cmd_geosite_list.go | 50 ++ cmd/sing-box/cmd_geosite_lookup.go | 97 ++++ cmd/sing-box/cmd_geosite_matcher.go | 56 ++ cmd/sing-box/cmd_merge.go | 2 +- cmd/sing-box/cmd_rule_set.go | 14 + cmd/sing-box/cmd_rule_set_compile.go | 80 +++ cmd/sing-box/cmd_rule_set_format.go | 87 ++++ cmd/sing-box/cmd_tools.go | 6 +- cmd/sing-box/cmd_tools_connect.go | 2 +- common/dialer/router.go | 12 +- common/srs/binary.go | 485 ++++++++++++++++++ common/srs/ip_set.go | 116 +++++ constant/rule.go | 8 + experimental/cachefile/cache.go | 35 ++ experimental/cachefile/fakeip.go | 2 +- experimental/clashapi/proxies.go | 6 +- experimental/clashapi/server_resources.go | 6 +- .../clashapi/trafficontrol/tracker.go | 8 +- option/experimental.go | 8 +- option/route.go | 1 + option/rule.go | 58 ++- option/rule_dns.go | 1 + option/rule_set.go | 230 +++++++++ option/time_unit.go | 226 ++++++++ option/types.go | 10 +- route/router.go | 170 ++++-- route/router_dns.go | 1 + route/router_geo_resources.go | 70 --- route/router_rule.go | 99 ++++ route/rule_abstract.go | 81 +-- route/rule_default.go | 7 +- route/rule_dns.go | 7 +- route/rule_headless.go | 173 +++++++ route/rule_item_cidr.go | 19 +- route/rule_item_domain.go | 7 + route/rule_item_rule_set.go | 55 ++ route/rule_set.go | 67 +++ route/rule_set_local.go | 82 +++ route/rule_set_remote.go | 264 ++++++++++ 51 files changed, 2957 insertions(+), 249 deletions(-) create mode 100644 cmd/sing-box/cmd_geoip.go create mode 100644 cmd/sing-box/cmd_geoip_export.go create mode 100644 cmd/sing-box/cmd_geoip_list.go create mode 100644 cmd/sing-box/cmd_geoip_lookup.go create mode 100644 cmd/sing-box/cmd_geosite.go create mode 100644 cmd/sing-box/cmd_geosite_export.go create mode 100644 cmd/sing-box/cmd_geosite_list.go create mode 100644 cmd/sing-box/cmd_geosite_lookup.go create mode 100644 cmd/sing-box/cmd_geosite_matcher.go create mode 100644 cmd/sing-box/cmd_rule_set.go create mode 100644 cmd/sing-box/cmd_rule_set_compile.go create mode 100644 cmd/sing-box/cmd_rule_set_format.go create mode 100644 common/srs/binary.go create mode 100644 common/srs/ip_set.go create mode 100644 option/rule_set.go create mode 100644 option/time_unit.go create mode 100644 route/router_rule.go create mode 100644 route/rule_headless.go create mode 100644 route/rule_item_rule_set.go create mode 100644 route/rule_set.go create mode 100644 route/rule_set_local.go create mode 100644 route/rule_set_remote.go diff --git a/.gitignore b/.gitignore index 6630f428ce..55bdab3a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.idea/ /vendor/ /*.json +/*.srs /*.db /site/ /bin/ diff --git a/adapter/experimental.go b/adapter/experimental.go index 3ba9419eb1..2a6776cd0e 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -1,11 +1,16 @@ package adapter import ( + "bytes" "context" + "encoding/binary" + "io" "net" + "time" "github.com/sagernet/sing-box/common/urltest" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" ) type ClashServer interface { @@ -23,6 +28,7 @@ type CacheFile interface { PreStarter StoreFakeIP() bool + FakeIPStorage LoadMode() string StoreMode(mode string) error @@ -30,7 +36,65 @@ type CacheFile interface { StoreSelected(group string, selected string) error LoadGroupExpand(group string) (isExpand bool, loaded bool) StoreGroupExpand(group string, expand bool) error - FakeIPStorage + LoadRuleSet(tag string) *SavedRuleSet + SaveRuleSet(tag string, set *SavedRuleSet) error +} + +type SavedRuleSet struct { + Content []byte + LastUpdated time.Time + LastEtag string +} + +func (s *SavedRuleSet) MarshalBinary() ([]byte, error) { + var buffer bytes.Buffer + err := binary.Write(&buffer, binary.BigEndian, uint8(1)) + if err != nil { + return nil, err + } + err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) + if err != nil { + return nil, err + } + buffer.Write(s.Content) + err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) + if err != nil { + return nil, err + } + err = rw.WriteVString(&buffer, s.LastEtag) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { + reader := bytes.NewReader(data) + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return err + } + contentLen, err := rw.ReadUVariant(reader) + if err != nil { + return err + } + s.Content = make([]byte, contentLen) + _, err = io.ReadFull(reader, s.Content) + if err != nil { + return err + } + var lastUpdated int64 + err = binary.Read(reader, binary.BigEndian, &lastUpdated) + if err != nil { + return err + } + s.LastUpdated = time.Unix(lastUpdated, 0) + s.LastEtag, err = rw.ReadVString(reader) + if err != nil { + return err + } + return nil } type Tracker interface { diff --git a/adapter/inbound.go b/adapter/inbound.go index 2d24083c4a..f32b804d21 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -46,11 +46,24 @@ type InboundContext struct { SourceGeoIPCode string GeoIPCode string ProcessInfo *process.Info + QueryType uint16 FakeIP bool - // dns cache + // rule cache - QueryType uint16 + IPCIDRMatchSource bool + SourceAddressMatch bool + SourcePortMatch bool + DestinationAddressMatch bool + DestinationPortMatch bool +} + +func (c *InboundContext) ResetRuleCache() { + c.IPCIDRMatchSource = false + c.SourceAddressMatch = false + c.SourcePortMatch = false + c.DestinationAddressMatch = false + c.DestinationPortMatch = false } type inboundContextKey struct{} diff --git a/adapter/router.go b/adapter/router.go index 3d18eb387f..b4bdd143a5 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -2,12 +2,14 @@ package adapter import ( "context" + "net/http" "net/netip" "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" + N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" mdns "github.com/miekg/dns" @@ -19,7 +21,7 @@ type Router interface { Outbounds() []Outbound Outbound(tag string) (Outbound, bool) - DefaultOutbound(network string) Outbound + DefaultOutbound(network string) (Outbound, error) FakeIPStore() FakeIPStore @@ -28,6 +30,8 @@ type Router interface { GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) + RuleSet(tag string) (RuleSet, bool) + Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) @@ -62,11 +66,15 @@ func RouterFromContext(ctx context.Context) Router { return service.FromContext[Router](ctx) } +type HeadlessRule interface { + Match(metadata *InboundContext) bool +} + type Rule interface { + HeadlessRule Service Type() string UpdateGeosite() error - Match(metadata *InboundContext) bool Outbound() string String() string } @@ -77,6 +85,24 @@ type DNSRule interface { RewriteTTL() *uint32 } +type RuleSet interface { + StartContext(ctx context.Context, startContext RuleSetStartContext) error + PostStart() error + Metadata() RuleSetMetadata + Close() error + HeadlessRule +} + +type RuleSetMetadata struct { + ContainsProcessRule bool + ContainsWIFIRule bool +} + +type RuleSetStartContext interface { + HTTPClient(detour string, dialer N.Dialer) *http.Client + Close() +} + type InterfaceUpdateListener interface { InterfaceUpdated() } diff --git a/box.go b/box.go index 5bc8bdcf36..8c1e3d4b5a 100644 --- a/box.go +++ b/box.go @@ -308,6 +308,10 @@ func (s *Box) postStart() error { } } s.logger.Trace("post-starting router") + err := s.router.PostStart() + if err != nil { + return E.Cause(err, "post-start router") + } return s.router.PostStart() } diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index 10a5497cd4..c5e939e4a9 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -7,7 +7,6 @@ import ( "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/spf13/cobra" @@ -69,41 +68,3 @@ func format() error { } return nil } - -func formatOne(configPath string) error { - configContent, err := os.ReadFile(configPath) - if err != nil { - return E.Cause(err, "read config") - } - var options option.Options - err = options.UnmarshalJSON(configContent) - if err != nil { - return E.Cause(err, "decode config") - } - buffer := new(bytes.Buffer) - encoder := json.NewEncoder(buffer) - encoder.SetIndent("", " ") - err = encoder.Encode(options) - if err != nil { - return E.Cause(err, "encode config") - } - if !commandFormatFlagWrite { - os.Stdout.WriteString(buffer.String() + "\n") - return nil - } - if bytes.Equal(configContent, buffer.Bytes()) { - return nil - } - output, err := os.Create(configPath) - if err != nil { - return E.Cause(err, "open output") - } - _, err = output.Write(buffer.Bytes()) - output.Close() - if err != nil { - return E.Cause(err, "write output") - } - outputPath, _ := filepath.Abs(configPath) - os.Stderr.WriteString(outputPath + "\n") - return nil -} diff --git a/cmd/sing-box/cmd_geoip.go b/cmd/sing-box/cmd_geoip.go new file mode 100644 index 0000000000..dbbbff135e --- /dev/null +++ b/cmd/sing-box/cmd_geoip.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var ( + geoipReader *maxminddb.Reader + commandGeoIPFlagFile string +) + +var commandGeoip = &cobra.Command{ + Use: "geoip", + Short: "GeoIP tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geoipPreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file") + mainCommand.AddCommand(commandGeoip) +} + +func geoipPreRun() error { + reader, err := maxminddb.Open(commandGeoIPFlagFile) + if err != nil { + return err + } + if reader.Metadata.DatabaseType != "sing-geoip" { + reader.Close() + return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType) + } + geoipReader = reader + return nil +} diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go new file mode 100644 index 0000000000..d170d10b72 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_export.go @@ -0,0 +1,98 @@ +package main + +import ( + "io" + "net" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var flagGeoipExportOutput string + +const flagGeoipExportDefaultOutput = "geoip-.srs" + +var commandGeoipExport = &cobra.Command{ + Use: "export ", + Short: "Export geoip country as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path") + commandGeoip.AddCommand(commandGeoipExport) +} + +func geoipExport(countryCode string) error { + networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks) + countryMap := make(map[string][]*net.IPNet) + var ( + ipNet *net.IPNet + nextCountryCode string + err error + ) + for networks.Next() { + ipNet, err = networks.Network(&nextCountryCode) + if err != nil { + return err + } + countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet) + } + ipNets := countryMap[strings.ToLower(countryCode)] + if len(ipNets) == 0 { + return E.New("country code not found: ", countryCode) + } + + var ( + outputFile *os.File + outputWriter io.Writer + ) + if flagGeoipExportOutput == "stdout" { + outputWriter = os.Stdout + } else if flagGeoipExportOutput == flagGeoipExportDefaultOutput { + outputFile, err = os.Create("geoip-" + countryCode + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(flagGeoipExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + headlessRule.IPCIDR = make([]string, 0, len(ipNets)) + for _, cidr := range ipNets { + headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) + } + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geoip_list.go b/cmd/sing-box/cmd_geoip_list.go new file mode 100644 index 0000000000..54dd426ea9 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_list.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandGeoipList = &cobra.Command{ + Use: "list", + Short: "List geoip country codes", + Run: func(cmd *cobra.Command, args []string) { + err := listGeoip() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipList) +} + +func listGeoip() error { + for _, code := range geoipReader.Metadata.Languages { + os.Stdout.WriteString(code + "\n") + } + return nil +} diff --git a/cmd/sing-box/cmd_geoip_lookup.go b/cmd/sing-box/cmd_geoip_lookup.go new file mode 100644 index 0000000000..d5157bb404 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_lookup.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/netip" + "os" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + + "github.com/spf13/cobra" +) + +var commandGeoipLookup = &cobra.Command{ + Use: "lookup
", + Short: "Lookup if an IP address is contained in the GeoIP database", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipLookup(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipLookup) +} + +func geoipLookup(address string) error { + addr, err := netip.ParseAddr(address) + if err != nil { + return E.Cause(err, "parse address") + } + if !N.IsPublicAddr(addr) { + os.Stdout.WriteString("private\n") + return nil + } + var code string + _ = geoipReader.Lookup(addr.AsSlice(), &code) + if code != "" { + os.Stdout.WriteString(code + "\n") + return nil + } + os.Stdout.WriteString("unknown\n") + return nil +} diff --git a/cmd/sing-box/cmd_geosite.go b/cmd/sing-box/cmd_geosite.go new file mode 100644 index 0000000000..95db935797 --- /dev/null +++ b/cmd/sing-box/cmd_geosite.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/sagernet/sing-box/common/geosite" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + commandGeoSiteFlagFile string + geositeReader *geosite.Reader + geositeCodeList []string +) + +var commandGeoSite = &cobra.Command{ + Use: "geosite", + Short: "Geosite tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geositePreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file") + mainCommand.AddCommand(commandGeoSite) +} + +func geositePreRun() error { + reader, codeList, err := geosite.Open(commandGeoSiteFlagFile) + if err != nil { + return E.Cause(err, "open geosite file") + } + geositeReader = reader + geositeCodeList = codeList + return nil +} diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go new file mode 100644 index 0000000000..71f1018d8f --- /dev/null +++ b/cmd/sing-box/cmd_geosite_export.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "io" + "os" + + "github.com/sagernet/sing-box/common/geosite" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var commandGeositeExportOutput string + +const commandGeositeExportDefaultOutput = "geosite-.json" + +var commandGeositeExport = &cobra.Command{ + Use: "export ", + Short: "Export geosite category as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geositeExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path") + commandGeoSite.AddCommand(commandGeositeExport) +} + +func geositeExport(category string) error { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + var ( + outputFile *os.File + outputWriter io.Writer + ) + if commandGeositeExportOutput == "stdout" { + outputWriter = os.Stdout + } else if commandGeositeExportOutput == commandGeositeExportDefaultOutput { + outputFile, err = os.Create("geosite-" + category + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(commandGeositeExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + defaultRule := geosite.Compile(sourceSet) + headlessRule.Domain = defaultRule.Domain + headlessRule.DomainSuffix = defaultRule.DomainSuffix + headlessRule.DomainKeyword = defaultRule.DomainKeyword + headlessRule.DomainRegex = defaultRule.DomainRegex + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geosite_list.go b/cmd/sing-box/cmd_geosite_list.go new file mode 100644 index 0000000000..cedb7adfd2 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_list.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + F "github.com/sagernet/sing/common/format" + + "github.com/spf13/cobra" +) + +var commandGeositeList = &cobra.Command{ + Use: "list ", + Short: "List geosite categories", + Run: func(cmd *cobra.Command, args []string) { + err := geositeList() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeList) +} + +func geositeList() error { + var geositeEntry []struct { + category string + items int + } + for _, category := range geositeCodeList { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + geositeEntry = append(geositeEntry, struct { + category string + items int + }{category, len(sourceSet)}) + } + sort.SliceStable(geositeEntry, func(i, j int) bool { + return geositeEntry[i].items < geositeEntry[j].items + }) + for _, entry := range geositeEntry { + os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n")) + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_lookup.go b/cmd/sing-box/cmd_geosite_lookup.go new file mode 100644 index 0000000000..f648ce62a4 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_lookup.go @@ -0,0 +1,97 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandGeositeLookup = &cobra.Command{ + Use: "lookup [category] ", + Short: "Check if a domain is in the geosite", + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + var ( + source string + target string + ) + switch len(args) { + case 1: + target = args[0] + case 2: + source = args[0] + target = args[1] + } + err := geositeLookup(source, target) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeLookup) +} + +func geositeLookup(source string, target string) error { + var sourceMatcherList []struct { + code string + matcher *searchGeositeMatcher + } + if source != "" { + sourceSet, err := geositeReader.Read(source) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+source) + } + sourceMatcherList = []struct { + code string + matcher *searchGeositeMatcher + }{ + { + code: source, + matcher: sourceMatcher, + }, + } + + } else { + for _, code := range geositeCodeList { + sourceSet, err := geositeReader.Read(code) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+code) + } + sourceMatcherList = append(sourceMatcherList, struct { + code string + matcher *searchGeositeMatcher + }{ + code: code, + matcher: sourceMatcher, + }) + } + } + sort.SliceStable(sourceMatcherList, func(i, j int) bool { + return sourceMatcherList[i].code < sourceMatcherList[j].code + }) + + for _, matcherItem := range sourceMatcherList { + if matchRule := matcherItem.matcher.Match(target); matchRule != "" { + os.Stdout.WriteString("Match code (") + os.Stdout.WriteString(matcherItem.code) + os.Stdout.WriteString(") ") + os.Stdout.WriteString(matchRule) + os.Stdout.WriteString("\n") + } + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_matcher.go b/cmd/sing-box/cmd_geosite_matcher.go new file mode 100644 index 0000000000..791dba2499 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_matcher.go @@ -0,0 +1,56 @@ +package main + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/common/geosite" +) + +type searchGeositeMatcher struct { + domainMap map[string]bool + suffixList []string + keywordList []string + regexList []string +} + +func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) { + options := geosite.Compile(items) + domainMap := make(map[string]bool) + for _, domain := range options.Domain { + domainMap[domain] = true + } + rule := &searchGeositeMatcher{ + domainMap: domainMap, + suffixList: options.DomainSuffix, + keywordList: options.DomainKeyword, + regexList: options.DomainRegex, + } + return rule, nil +} + +func (r *searchGeositeMatcher) Match(domain string) string { + if r.domainMap[domain] { + return "domain=" + domain + } + for _, suffix := range r.suffixList { + if strings.HasSuffix(domain, suffix) { + return "domain_suffix=" + suffix + } + } + for _, keyword := range r.keywordList { + if strings.Contains(domain, keyword) { + return "domain_keyword=" + keyword + } + } + for _, regexStr := range r.regexList { + regex, err := regexp.Compile(regexStr) + if err != nil { + continue + } + if regex.MatchString(domain) { + return "domain_regex=" + regexStr + } + } + return "" +} diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 0aff750182..4fb07b8688 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -18,7 +18,7 @@ import ( ) var commandMerge = &cobra.Command{ - Use: "merge [output]", + Use: "merge ", Short: "Merge configurations", Run: func(cmd *cobra.Command, args []string) { err := merge(args[0]) diff --git a/cmd/sing-box/cmd_rule_set.go b/cmd/sing-box/cmd_rule_set.go new file mode 100644 index 0000000000..f4112a087b --- /dev/null +++ b/cmd/sing-box/cmd_rule_set.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var commandRuleSet = &cobra.Command{ + Use: "rule-set", + Short: "Manage rule sets", +} + +func init() { + mainCommand.AddCommand(commandRuleSet) +} diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go new file mode 100644 index 0000000000..de318095ac --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -0,0 +1,80 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var flagRuleSetCompileOutput string + +const flagRuleSetCompileDefaultOutput = ".srs" + +var commandRuleSetCompile = &cobra.Command{ + Use: "compile [source-path]", + Short: "Compile rule-set json to binary", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := compileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetCompile) + commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func compileRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + decoder := json.NewDecoder(json.NewCommentFilter(reader)) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + var outputPath string + if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".json") { + outputPath = sourcePath[:len(sourcePath)-5] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetCompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + err = srs.Write(outputFile, ruleSet) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_format.go b/cmd/sing-box/cmd_rule_set_format.go new file mode 100644 index 0000000000..dc3ee6aabd --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_format.go @@ -0,0 +1,87 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandRuleSetFormatFlagWrite bool + +var commandRuleSetFormat = &cobra.Command{ + Use: "format ", + Short: "Format rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := formatRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetFormat) +} + +func formatRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(ruleSet) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(sourcePath) + if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" { + os.Stdout.WriteString(buffer.String() + "\n") + return nil + } + if bytes.Equal(content, buffer.Bytes()) { + return nil + } + output, err := os.Create(sourcePath) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index 460a50cd1c..c45f585576 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) { func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { if outboundTag == "" { - outbound := instance.Router().DefaultOutbound(N.NetworkName(network)) - if outbound == nil { - return nil, E.New("missing default outbound") - } - return outbound, nil + return instance.Router().DefaultOutbound(N.NetworkName(network)) } else { outbound, loaded := instance.Router().Outbound(outboundTag) if !loaded { diff --git a/cmd/sing-box/cmd_tools_connect.go b/cmd/sing-box/cmd_tools_connect.go index b904ebc9f5..3ea04bcd40 100644 --- a/cmd/sing-box/cmd_tools_connect.go +++ b/cmd/sing-box/cmd_tools_connect.go @@ -18,7 +18,7 @@ import ( var commandConnectFlagNetwork string var commandConnect = &cobra.Command{ - Use: "connect [address]", + Use: "connect
", Short: "Connect to an address", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { diff --git a/common/dialer/router.go b/common/dialer/router.go index 1d5586546e..2531607753 100644 --- a/common/dialer/router.go +++ b/common/dialer/router.go @@ -18,11 +18,19 @@ func NewRouter(router adapter.Router) N.Dialer { } func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return d.router.DefaultOutbound(network).DialContext(ctx, network, destination) + dialer, err := d.router.DefaultOutbound(network) + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, destination) } func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination) + dialer, err := d.router.DefaultOutbound(N.NetworkUDP) + if err != nil { + return nil, err + } + return dialer.ListenPacket(ctx, destination) } func (d *RouterDialer) Upstream() any { diff --git a/common/srs/binary.go b/common/srs/binary.go new file mode 100644 index 0000000000..dd994c2c29 --- /dev/null +++ b/common/srs/binary.go @@ -0,0 +1,485 @@ +package srs + +import ( + "compress/zlib" + "encoding/binary" + "io" + "net/netip" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS + +const ( + ruleItemQueryType uint8 = iota + ruleItemNetwork + ruleItemDomain + ruleItemDomainKeyword + ruleItemDomainRegex + ruleItemSourceIPCIDR + ruleItemIPCIDR + ruleItemSourcePort + ruleItemSourcePortRange + ruleItemPort + ruleItemPortRange + ruleItemProcessName + ruleItemProcessPath + ruleItemPackageName + ruleItemWIFISSID + ruleItemWIFIBSSID + ruleItemFinal uint8 = 0xFF +) + +func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { + var magicBytes [3]byte + _, err = io.ReadFull(reader, magicBytes[:]) + if err != nil { + return + } + if magicBytes != MagicBytes { + err = E.New("invalid sing-box rule set file") + return + } + var version uint8 + err = binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return ruleSet, err + } + if version != 1 { + return ruleSet, E.New("unsupported version: ", version) + } + zReader, err := zlib.NewReader(reader) + if err != nil { + return + } + length, err := rw.ReadUVariant(zReader) + if err != nil { + return + } + ruleSet.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + ruleSet.Rules[i], err = readRule(zReader, recovery) + if err != nil { + err = E.Cause(err, "read rule[", i, "]") + return + } + } + return +} + +func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { + _, err := writer.Write(MagicBytes[:]) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) + if err != nil { + return err + } + err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) + if err != nil { + return err + } + for _, rule := range ruleSet.Rules { + err = writeRule(zWriter, rule) + if err != nil { + return err + } + } + return zWriter.Close() +} + +func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { + var ruleType uint8 + err = binary.Read(reader, binary.BigEndian, &ruleType) + if err != nil { + return + } + switch ruleType { + case 0: + rule.DefaultOptions, err = readDefaultRule(reader, recovery) + case 1: + rule.LogicalOptions, err = readLogicalRule(reader, recovery) + default: + err = E.New("unknown rule type: ", ruleType) + } + return +} + +func writeRule(writer io.Writer, rule option.HeadlessRule) error { + switch rule.Type { + case C.RuleTypeDefault: + return writeDefaultRule(writer, rule.DefaultOptions) + case C.RuleTypeLogical: + return writeLogicalRule(writer, rule.LogicalOptions) + default: + panic("unknown rule type: " + rule.Type) + } +} + +func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { + var lastItemType uint8 + for { + var itemType uint8 + err = binary.Read(reader, binary.BigEndian, &itemType) + if err != nil { + return + } + switch itemType { + case ruleItemQueryType: + var rawQueryType []uint16 + rawQueryType, err = readRuleItemUint16(reader) + if err != nil { + return + } + rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType { + return option.DNSQueryType(it) + }) + case ruleItemNetwork: + rule.Network, err = readRuleItemString(reader) + case ruleItemDomain: + var matcher *domain.Matcher + matcher, err = domain.ReadMatcher(reader) + if err != nil { + return + } + rule.DomainMatcher = matcher + case ruleItemDomainKeyword: + rule.DomainKeyword, err = readRuleItemString(reader) + case ruleItemDomainRegex: + rule.DomainRegex, err = readRuleItemString(reader) + case ruleItemSourceIPCIDR: + rule.SourceIPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemIPCIDR: + rule.IPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemSourcePort: + rule.SourcePort, err = readRuleItemUint16(reader) + case ruleItemSourcePortRange: + rule.SourcePortRange, err = readRuleItemString(reader) + case ruleItemPort: + rule.Port, err = readRuleItemUint16(reader) + case ruleItemPortRange: + rule.PortRange, err = readRuleItemString(reader) + case ruleItemProcessName: + rule.ProcessName, err = readRuleItemString(reader) + case ruleItemProcessPath: + rule.ProcessPath, err = readRuleItemString(reader) + case ruleItemPackageName: + rule.PackageName, err = readRuleItemString(reader) + case ruleItemWIFISSID: + rule.WIFISSID, err = readRuleItemString(reader) + case ruleItemWIFIBSSID: + rule.WIFIBSSID, err = readRuleItemString(reader) + case ruleItemFinal: + err = binary.Read(reader, binary.BigEndian, &rule.Invert) + return + default: + err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType) + } + if err != nil { + return + } + lastItemType = itemType + } +} + +func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(0)) + if err != nil { + return err + } + if len(rule.QueryType) > 0 { + err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 { + return uint16(it) + })) + if err != nil { + return err + } + } + if len(rule.Network) > 0 { + err = writeRuleItemString(writer, ruleItemNetwork, rule.Network) + if err != nil { + return err + } + } + if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 { + err = binary.Write(writer, binary.BigEndian, ruleItemDomain) + if err != nil { + return err + } + err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer) + if err != nil { + return err + } + } + if len(rule.DomainKeyword) > 0 { + err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword) + if err != nil { + return err + } + } + if len(rule.DomainRegex) > 0 { + err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex) + if err != nil { + return err + } + } + if len(rule.SourceIPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR) + if err != nil { + return E.Cause(err, "source_ipcidr") + } + } + if len(rule.IPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR) + if err != nil { + return E.Cause(err, "ipcidr") + } + } + if len(rule.SourcePort) > 0 { + err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort) + if err != nil { + return err + } + } + if len(rule.SourcePortRange) > 0 { + err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange) + if err != nil { + return err + } + } + if len(rule.Port) > 0 { + err = writeRuleItemUint16(writer, ruleItemPort, rule.Port) + if err != nil { + return err + } + } + if len(rule.PortRange) > 0 { + err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange) + if err != nil { + return err + } + } + if len(rule.ProcessName) > 0 { + err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName) + if err != nil { + return err + } + } + if len(rule.ProcessPath) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath) + if err != nil { + return err + } + } + if len(rule.PackageName) > 0 { + err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) + if err != nil { + return err + } + } + if len(rule.WIFISSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) + if err != nil { + return err + } + } + if len(rule.WIFIBSSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, ruleItemFinal) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, rule.Invert) + if err != nil { + return err + } + return nil +} + +func readRuleItemString(reader io.Reader) ([]string, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]string, length) + for i := uint64(0); i < length; i++ { + value[i], err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = rw.WriteVString(writer, item) + if err != nil { + return err + } + } + return nil +} + +func readRuleItemUint16(reader io.Reader) ([]uint16, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]uint16, length) + for i := uint64(0); i < length; i++ { + err = binary.Read(reader, binary.BigEndian, &value[i]) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = binary.Write(writer, binary.BigEndian, item) + if err != nil { + return err + } + } + return nil +} + +func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { + var builder netipx.IPSetBuilder + for i, prefixString := range value { + prefix, err := netip.ParsePrefix(prefixString) + if err == nil { + builder.AddPrefix(prefix) + continue + } + addr, addrErr := netip.ParseAddr(prefixString) + if addrErr == nil { + builder.Add(addr) + continue + } + return E.Cause(err, "parse [", i, "]") + } + ipSet, err := builder.IPSet() + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + return writeIPSet(writer, ipSet) +} + +func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { + var mode uint8 + err = binary.Read(reader, binary.BigEndian, &mode) + if err != nil { + return + } + switch mode { + case 0: + logicalRule.Mode = C.LogicalTypeAnd + case 1: + logicalRule.Mode = C.LogicalTypeOr + default: + err = E.New("unknown logical mode: ", mode) + return + } + length, err := rw.ReadUVariant(reader) + if err != nil { + return + } + logicalRule.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + logicalRule.Rules[i], err = readRule(reader, recovery) + if err != nil { + err = E.Cause(err, "read logical rule [", i, "]") + return + } + } + err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert) + if err != nil { + return + } + return +} + +func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + switch logicalRule.Mode { + case C.LogicalTypeAnd: + err = binary.Write(writer, binary.BigEndian, uint8(0)) + case C.LogicalTypeOr: + err = binary.Write(writer, binary.BigEndian, uint8(1)) + default: + panic("unknown logical mode: " + logicalRule.Mode) + } + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) + if err != nil { + return err + } + for _, rule := range logicalRule.Rules { + err = writeRule(writer, rule) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, logicalRule.Invert) + if err != nil { + return err + } + return nil +} diff --git a/common/srs/ip_set.go b/common/srs/ip_set.go new file mode 100644 index 0000000000..b346da26f6 --- /dev/null +++ b/common/srs/ip_set.go @@ -0,0 +1,116 @@ +package srs + +import ( + "encoding/binary" + "io" + "net/netip" + "unsafe" + + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +type myIPSet struct { + rr []myIPRange +} + +type myIPRange struct { + from netip.Addr + to netip.Addr +} + +func readIPSet(reader io.Reader) (*netipx.IPSet, error) { + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return nil, err + } + var length uint64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + mySet := &myIPSet{ + rr: make([]myIPRange, length), + } + for i := uint64(0); i < length; i++ { + var ( + fromLen uint64 + toLen uint64 + fromAddr netip.Addr + toAddr netip.Addr + ) + fromLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + fromBytes := make([]byte, fromLen) + _, err = io.ReadFull(reader, fromBytes) + if err != nil { + return nil, err + } + err = fromAddr.UnmarshalBinary(fromBytes) + if err != nil { + return nil, err + } + toLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + toBytes := make([]byte, toLen) + _, err = io.ReadFull(reader, toBytes) + if err != nil { + return nil, err + } + err = toAddr.UnmarshalBinary(toBytes) + if err != nil { + return nil, err + } + mySet.rr[i] = myIPRange{fromAddr, toAddr} + } + return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil +} + +func writeIPSet(writer io.Writer, set *netipx.IPSet) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + mySet := (*myIPSet)(unsafe.Pointer(set)) + err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) + if err != nil { + return err + } + for _, rr := range mySet.rr { + var ( + fromBinary []byte + toBinary []byte + ) + fromBinary, err = rr.from.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(fromBinary))) + if err != nil { + return err + } + _, err = writer.Write(fromBinary) + if err != nil { + return err + } + toBinary, err = rr.to.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(toBinary))) + if err != nil { + return err + } + _, err = writer.Write(toBinary) + if err != nil { + return err + } + } + return nil +} diff --git a/constant/rule.go b/constant/rule.go index 3c741995f8..5a8eaf127f 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -9,3 +9,11 @@ const ( LogicalTypeAnd = "and" LogicalTypeOr = "or" ) + +const ( + RuleSetTypeLocal = "local" + RuleSetTypeRemote = "remote" + RuleSetVersion1 = 1 + RuleSetFormatSource = "source" + RuleSetFormatBinary = "binary" +) diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 262d1c1e4b..43b8456215 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -22,11 +22,13 @@ var ( bucketSelected = []byte("selected") bucketExpand = []byte("group_expand") bucketMode = []byte("clash_mode") + bucketRuleSet = []byte("rule_set") bucketNameList = []string{ string(bucketSelected), string(bucketExpand), string(bucketMode), + string(bucketRuleSet), } cacheIDDefault = []byte("default") @@ -257,3 +259,36 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } + +func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet { + var savedSet adapter.SavedRuleSet + err := c.DB.View(func(t *bbolt.Tx) error { + bucket := c.bucket(t, bucketRuleSet) + if bucket == nil { + return os.ErrNotExist + } + setBinary := bucket.Get([]byte(tag)) + if len(setBinary) == 0 { + return os.ErrInvalid + } + return savedSet.UnmarshalBinary(setBinary) + }) + if err != nil { + return nil + } + return &savedSet +} + +func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedRuleSet) error { + return c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := c.createBucket(t, bucketRuleSet) + if err != nil { + return err + } + setBinary, err := set.MarshalBinary() + if err != nil { + return err + } + return bucket.Put([]byte(tag), setBinary) + }) +} diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go index 2242342a36..e998ebb859 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -25,7 +25,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { err := c.DB.Batch(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { - return nil + return os.ErrNotExist } metadataBinary := bucket.Get(keyMetadata) if len(metadataBinary) == 0 { diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index 050efd8d17..cf96931a85 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -100,8 +100,10 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite allProxies = append(allProxies, detour.Tag()) } - defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag() - if defaultTag == "" { + var defaultTag string + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + defaultTag = defaultOutbound.Tag() + } else { defaultTag = allProxies[0] } diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index ad36641e05..d6d22b5390 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -51,7 +51,11 @@ func (s *Server) downloadExternalUI() error { } detour = outbound } else { - detour = s.router.DefaultOutbound(N.NetworkTCP) + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + detour = outbound } httpClient := &http.Client{ Transport: &http.Transport{ diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 3dc5a367e7..b7c20eb075 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -94,7 +94,9 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkTCP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } @@ -181,7 +183,9 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkUDP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } diff --git a/option/experimental.go b/option/experimental.go index 72751a590e..c685f51f54 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -23,16 +23,16 @@ type ClashAPIOptions struct { DefaultMode string `json:"default_mode,omitempty"` ModeList []string `json:"-"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` // 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 { diff --git a/option/route.go b/option/route.go index 43150576e2..e313fcf242 100644 --- a/option/route.go +++ b/option/route.go @@ -4,6 +4,7 @@ type RouteOptions struct { GeoIP *GeoIPOptions `json:"geoip,omitempty"` Geosite *GeositeOptions `json:"geosite,omitempty"` Rules []Rule `json:"rules,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` Final string `json:"final,omitempty"` FindProcess bool `json:"find_process,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` diff --git a/option/rule.go b/option/rule.go index 4f4042025f..bad605a0fc 100644 --- a/option/rule.go +++ b/option/rule.go @@ -65,34 +65,36 @@ func (r Rule) IsValid() bool { } type DefaultRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + GeoIP Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r DefaultRule) IsValid() bool { diff --git a/option/rule_dns.go b/option/rule_dns.go index fca3432293..c02d09f761 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -91,6 +91,7 @@ type DefaultDNSRule struct { ClashMode string `json:"clash_mode,omitempty"` WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` Invert bool `json:"invert,omitempty"` Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go new file mode 100644 index 0000000000..1b75814279 --- /dev/null +++ b/option/rule_set.go @@ -0,0 +1,230 @@ +package option + +import ( + "reflect" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + + "go4.org/netipx" +) + +type _RuleSet struct { + Type string `json:"type"` + Tag string `json:"tag"` + Format string `json:"format"` + LocalOptions LocalRuleSet `json:"-"` + RemoteOptions RemoteRuleSet `json:"-"` +} + +type RuleSet _RuleSet + +func (r RuleSet) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = r.LocalOptions + case C.RuleSetTypeRemote: + v = r.RemoteOptions + default: + return nil, E.New("unknown rule set type: " + r.Type) + } + return MarshallObjects((_RuleSet)(r), v) +} + +func (r *RuleSet) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RuleSet)(r)) + if err != nil { + return err + } + if r.Tag == "" { + return E.New("missing tag") + } + switch r.Format { + case "": + return E.New("missing format") + case C.RuleSetFormatSource, C.RuleSetFormatBinary: + default: + return E.New("unknown rule set format: " + r.Format) + } + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = &r.LocalOptions + case C.RuleSetTypeRemote: + v = &r.RemoteOptions + case "": + return E.New("missing type") + default: + return E.New("unknown rule set type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +type LocalRuleSet struct { + Path string `json:"path,omitempty"` +} + +type RemoteRuleSet struct { + URL string `json:"url"` + DownloadDetour string `json:"download_detour,omitempty"` + UpdateInterval Duration `json:"update_interval,omitempty"` +} + +type _HeadlessRule struct { + Type string `json:"type,omitempty"` + DefaultOptions DefaultHeadlessRule `json:"-"` + LogicalOptions LogicalHeadlessRule `json:"-"` +} + +type HeadlessRule _HeadlessRule + +func (r HeadlessRule) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleTypeDefault: + r.Type = "" + v = r.DefaultOptions + case C.RuleTypeLogical: + v = r.LogicalOptions + default: + return nil, E.New("unknown rule type: " + r.Type) + } + return MarshallObjects((_HeadlessRule)(r), v) +} + +func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_HeadlessRule)(r)) + if err != nil { + return err + } + var v any + switch r.Type { + case "", C.RuleTypeDefault: + r.Type = C.RuleTypeDefault + v = &r.DefaultOptions + case C.RuleTypeLogical: + v = &r.LogicalOptions + default: + return E.New("unknown rule type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) + if err != nil { + return E.Cause(err, "route rule-set rule") + } + return nil +} + +func (r HeadlessRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault, "": + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + +type DefaultHeadlessRule struct { + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` + + DomainMatcher *domain.Matcher `json:"-"` + SourceIPSet *netipx.IPSet `json:"-"` + IPSet *netipx.IPSet `json:"-"` +} + +func (r DefaultHeadlessRule) IsValid() bool { + var defaultValue DefaultHeadlessRule + defaultValue.Invert = r.Invert + return !reflect.DeepEqual(r, defaultValue) +} + +type LogicalHeadlessRule struct { + Mode string `json:"mode"` + Rules []HeadlessRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +func (r LogicalHeadlessRule) IsValid() bool { + return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid) +} + +type _PlainRuleSetCompat struct { + Version int `json:"version"` + Options PlainRuleSet `json:"-"` +} + +type PlainRuleSetCompat _PlainRuleSetCompat + +func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { + var v any + switch r.Version { + case C.RuleSetVersion1: + v = r.Options + default: + return nil, E.New("unknown rule set version: ", r.Version) + } + return MarshallObjects((_PlainRuleSetCompat)(r), v) +} + +func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r)) + if err != nil { + return err + } + var v any + switch r.Version { + case C.RuleSetVersion1: + v = &r.Options + case 0: + return E.New("missing rule set version") + default: + return E.New("unknown rule set version: ", r.Version) + } + err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { + var result PlainRuleSet + switch r.Version { + case C.RuleSetVersion1: + result = r.Options + default: + panic("unknown rule set version: " + F.ToString(r.Version)) + } + return result +} + +type PlainRuleSet struct { + Rules []HeadlessRule `json:"rules,omitempty"` +} diff --git a/option/time_unit.go b/option/time_unit.go new file mode 100644 index 0000000000..5e531dadf1 --- /dev/null +++ b/option/time_unit.go @@ -0,0 +1,226 @@ +package option + +import ( + "errors" + "time" +) + +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +const durationDay = 24 * time.Hour + +var unitMap = map[string]uint64{ + "ns": uint64(time.Nanosecond), + "us": uint64(time.Microsecond), + "µs": uint64(time.Microsecond), // U+00B5 = micro symbol + "μs": uint64(time.Microsecond), // U+03BC = Greek letter mu + "ms": uint64(time.Millisecond), + "s": uint64(time.Second), + "m": uint64(time.Minute), + "h": uint64(time.Hour), + "d": uint64(durationDay), +} + +// ParseDuration parses a duration string. +// A duration string is a possibly signed sequence of +// decimal numbers, each with optional fraction and a unit suffix, +// such as "300ms", "-1.5h" or "2h45m". +// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". +func ParseDuration(s string) (Duration, error) { + // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ + orig := s + var d uint64 + neg := false + + // Consume [-+]? + if s != "" { + c := s[0] + if c == '-' || c == '+' { + neg = c == '-' + s = s[1:] + } + } + // Special case: if all that is left is "0", this is zero. + if s == "0" { + return 0, nil + } + if s == "" { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + for s != "" { + var ( + v, f uint64 // integers before, after decimal point + scale float64 = 1 // value = v + f/scale + ) + + var err error + + // The next character must be [0-9.] + if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + // Consume [0-9]* + pl := len(s) + v, s, err = leadingInt(s) + if err != nil { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + pre := pl != len(s) // whether we consumed anything before a period + + // Consume (\.[0-9]*)? + post := false + if s != "" && s[0] == '.' { + s = s[1:] + pl := len(s) + f, scale, s = leadingFraction(s) + post = pl != len(s) + } + if !pre && !post { + // no digits (e.g. ".s" or "-.s") + return 0, errors.New("time: invalid duration " + quote(orig)) + } + + // Consume unit. + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c == '.' || '0' <= c && c <= '9' { + break + } + } + if i == 0 { + return 0, errors.New("time: missing unit in duration " + quote(orig)) + } + u := s[:i] + s = s[i:] + unit, ok := unitMap[u] + if !ok { + return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig)) + } + if v > 1<<63/unit { + // overflow + return 0, errors.New("time: invalid duration " + quote(orig)) + } + v *= unit + if f > 0 { + // float64 is needed to be nanosecond accurate for fractions of hours. + // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) + v += uint64(float64(f) * (float64(unit) / scale)) + if v > 1<<63 { + // overflow + return 0, errors.New("time: invalid duration " + quote(orig)) + } + } + d += v + if d > 1<<63 { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + } + if neg { + return -Duration(d), nil + } + if d > 1<<63-1 { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + return Duration(d), nil +} + +var errLeadingInt = errors.New("time: bad [0-9]*") // never printed + +// leadingInt consumes the leading [0-9]* from s. +func leadingInt[bytes []byte | string](s bytes) (x uint64, rem bytes, err error) { + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if x > 1<<63/10 { + // overflow + return 0, rem, errLeadingInt + } + x = x*10 + uint64(c) - '0' + if x > 1<<63 { + // overflow + return 0, rem, errLeadingInt + } + } + return x, s[i:], nil +} + +// leadingFraction consumes the leading [0-9]* from s. +// It is used only for fractions, so does not return an error on overflow, +// it just stops accumulating precision. +func leadingFraction(s string) (x uint64, scale float64, rem string) { + i := 0 + scale = 1 + overflow := false + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if overflow { + continue + } + if x > (1<<63-1)/10 { + // It's possible for overflow to give a positive number, so take care. + overflow = true + continue + } + y := x*10 + uint64(c) - '0' + if y > 1<<63 { + overflow = true + continue + } + x = y + scale *= 10 + } + return x, scale, s[i:] +} + +// These are borrowed from unicode/utf8 and strconv and replicate behavior in +// that package, since we can't take a dependency on either. +const ( + lowerhex = "0123456789abcdef" + runeSelf = 0x80 + runeError = '\uFFFD' +) + +func quote(s string) string { + buf := make([]byte, 1, len(s)+2) // slice will be at least len(s) + quotes + buf[0] = '"' + for i, c := range s { + if c >= runeSelf || c < ' ' { + // This means you are asking us to parse a time.Duration or + // time.Location with unprintable or non-ASCII characters in it. + // We don't expect to hit this case very often. We could try to + // reproduce strconv.Quote's behavior with full fidelity but + // given how rarely we expect to hit these edge cases, speed and + // conciseness are better. + var width int + if c == runeError { + width = 1 + if i+2 < len(s) && s[i:i+3] == string(runeError) { + width = 3 + } + } else { + width = len(string(c)) + } + for j := 0; j < width; j++ { + buf = append(buf, `\x`...) + buf = append(buf, lowerhex[s[i+j]>>4]) + buf = append(buf, lowerhex[s[i+j]&0xF]) + } + } else { + if c == '"' || c == '\\' { + buf = append(buf, '\\') + } + buf = append(buf, string(c)...) + } + } + buf = append(buf, '"') + return string(buf) +} diff --git a/option/types.go b/option/types.go index f2fed66309..2f029098af 100644 --- a/option/types.go +++ b/option/types.go @@ -164,7 +164,7 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { if err != nil { return err } - duration, err := time.ParseDuration(value) + duration, err := ParseDuration(value) if err != nil { return err } @@ -174,6 +174,14 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { type DNSQueryType uint16 +func (t DNSQueryType) String() string { + typeName, loaded := mDNS.TypeToString[uint16(t)] + if loaded { + return typeName + } + return F.ToString(uint16(t)) +} + func (t DNSQueryType) MarshalJSON() ([]byte, error) { typeName, loaded := mDNS.TypeToString[uint16(t)] if loaded { diff --git a/route/router.go b/route/router.go index dc98f5e22f..aa5a3eb479 100644 --- a/route/router.go +++ b/route/router.go @@ -39,6 +39,7 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" serviceNTP "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" @@ -64,9 +65,12 @@ type Router struct { geoIPReader *geoip.Reader geositeReader *geosite.Reader geositeCache map[string]adapter.Rule + needFindProcess bool dnsClient *dns.Client defaultDomainStrategy dns.DomainStrategy dnsRules []adapter.DNSRule + ruleSets []adapter.RuleSet + ruleSetMap map[string]adapter.RuleSet defaultTransport dns.Transport transports []dns.Transport transportMap map[string]dns.Transport @@ -107,11 +111,13 @@ func NewRouter( outboundByTag: make(map[string]adapter.Outbound), rules: make([]adapter.Rule, 0, len(options.Rules)), dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)), + ruleSetMap: make(map[string]adapter.RuleSet), needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule), needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule), geoIPOptions: common.PtrValueOrDefault(options.GeoIP), geositeOptions: common.PtrValueOrDefault(options.Geosite), geositeCache: make(map[string]adapter.Rule), + needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, defaultDetour: options.Final, defaultDomainStrategy: dns.DomainStrategy(dnsOptions.Strategy), autoDetectInterface: options.AutoDetectInterface, @@ -141,6 +147,17 @@ func NewRouter( } router.dnsRules = append(router.dnsRules, dnsRule) } + for i, ruleSetOptions := range options.RuleSet { + if _, exists := router.ruleSetMap[ruleSetOptions.Tag]; exists { + return nil, E.New("duplicate rule-set tag: ", ruleSetOptions.Tag) + } + ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions) + if err != nil { + return nil, E.Cause(err, "parse rule-set[", i, "]") + } + router.ruleSets = append(router.ruleSets, ruleSet) + router.ruleSetMap[ruleSetOptions.Tag] = ruleSet + } transports := make([]dns.Transport, len(dnsOptions.Servers)) dummyTransportMap := make(map[string]dns.Transport) @@ -296,34 +313,6 @@ func NewRouter( router.interfaceMonitor = interfaceMonitor } - needFindProcess := hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess - needPackageManager := C.IsAndroid && platformInterface == nil && (needFindProcess || common.Any(inbounds, func(inbound option.Inbound) bool { - return len(inbound.TunOptions.IncludePackage) > 0 || len(inbound.TunOptions.ExcludePackage) > 0 - })) - if needPackageManager { - packageManager, err := tun.NewPackageManager(router) - if err != nil { - return nil, E.Cause(err, "create package manager") - } - router.packageManager = packageManager - } - if needFindProcess { - if platformInterface != nil { - router.processSearcher = platformInterface - } else { - searcher, err := process.NewSearcher(process.Config{ - Logger: logFactory.NewLogger("router/process"), - PackageManager: router.packageManager, - }) - if err != nil { - if err != os.ErrInvalid { - router.logger.Warn(E.Cause(err, "create process searcher")) - } - } else { - router.processSearcher = searcher - } - } - } if ntpOptions.Enabled { timeService, err := ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions) if err != nil { @@ -332,11 +321,6 @@ func NewRouter( service.ContextWith[serviceNTP.TimeService](ctx, timeService) router.timeService = timeService } - if platformInterface != nil && router.interfaceMonitor != nil && router.needWIFIState { - router.interfaceMonitor.RegisterCallback(func(_ int) { - router.updateWIFIState() - }) - } return router, nil } @@ -451,12 +435,6 @@ func (r *Router) Start() error { return err } } - if r.packageManager != nil { - err := r.packageManager.Start() - if err != nil { - return err - } - } if r.needGeositeDatabase { for _, rule := range r.rules { err := rule.UpdateGeosite() @@ -477,9 +455,89 @@ func (r *Router) Start() error { r.geositeCache = nil r.geositeReader = nil } - if r.needWIFIState { + if r.fakeIPStore != nil { + err := r.fakeIPStore.Start() + if err != nil { + return err + } + } + if len(r.ruleSets) > 0 { + ruleSetStartContext := NewRuleSetStartContext() + var ruleSetStartGroup task.Group + for i, ruleSet := range r.ruleSets { + ruleSetInPlace := ruleSet + ruleSetStartGroup.Append0(func(ctx context.Context) error { + err := ruleSetInPlace.StartContext(ctx, ruleSetStartContext) + if err != nil { + return E.Cause(err, "initialize rule-set[", i, "]") + } + return nil + }) + } + ruleSetStartGroup.Concurrency(5) + ruleSetStartGroup.FastFail() + err := ruleSetStartGroup.Run(r.ctx) + if err != nil { + return err + } + ruleSetStartContext.Close() + } + + var ( + needProcessFromRuleSet bool + needWIFIStateFromRuleSet bool + ) + for _, ruleSet := range r.ruleSets { + metadata := ruleSet.Metadata() + if metadata.ContainsProcessRule { + needProcessFromRuleSet = true + } + if metadata.ContainsWIFIRule { + needWIFIStateFromRuleSet = true + } + } + if needProcessFromRuleSet || r.needFindProcess { + needPackageManager := C.IsAndroid && r.platformInterface == nil + + if needPackageManager { + packageManager, err := tun.NewPackageManager(r) + if err != nil { + return E.Cause(err, "create package manager") + } + if packageManager != nil { + err = packageManager.Start() + if err != nil { + return err + } + } + r.packageManager = packageManager + } + + if r.platformInterface != nil { + r.processSearcher = r.platformInterface + } else { + searcher, err := process.NewSearcher(process.Config{ + Logger: r.logger, + PackageManager: r.packageManager, + }) + if err != nil { + if err != os.ErrInvalid { + r.logger.Warn(E.Cause(err, "create process searcher")) + } + } else { + r.processSearcher = searcher + } + } + } + if needWIFIStateFromRuleSet || r.needWIFIState { + if r.platformInterface != nil && r.interfaceMonitor != nil { + r.interfaceMonitor.RegisterCallback(func(_ int) { + r.updateWIFIState() + }) + } r.updateWIFIState() } + for i, rule := range r.rules { err := rule.Start() if err != nil { @@ -492,12 +550,6 @@ func (r *Router) Start() error { return E.Cause(err, "initialize DNS rule[", i, "]") } } - if r.fakeIPStore != nil { - err := r.fakeIPStore.Start() - if err != nil { - return err - } - } for i, transport := range r.transports { err := transport.Start() if err != nil { @@ -573,6 +625,14 @@ func (r *Router) Close() error { } func (r *Router) PostStart() error { + if len(r.ruleSets) > 0 { + for i, ruleSet := range r.ruleSets { + err := ruleSet.PostStart() + if err != nil { + return E.Cause(err, "post start rule-set[", i, "]") + } + } + } r.started = true return nil } @@ -582,11 +642,17 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { return outbound, loaded } -func (r *Router) DefaultOutbound(network string) adapter.Outbound { +func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) { if network == N.NetworkTCP { - return r.defaultOutboundForConnection + if r.defaultOutboundForConnection == nil { + return nil, E.New("missing default outbound for TCP connections") + } + return r.defaultOutboundForConnection, nil } else { - return r.defaultOutboundForPacketConnection + if r.defaultOutboundForPacketConnection == nil { + return nil, E.New("missing default outbound for UDP connections") + } + return r.defaultOutboundForPacketConnection, nil } } @@ -594,6 +660,11 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore { return r.fakeIPStore } +func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSetMap[tag] + return ruleSet, loaded +} + func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { @@ -882,6 +953,7 @@ func (r *Router) match0(ctx context.Context, metadata *adapter.InboundContext, d } } for i, rule := range r.rules { + metadata.ResetRuleCache() if rule.Match(metadata) { detour := rule.Outbound() r.logger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour) diff --git a/route/router_dns.go b/route/router_dns.go index 1532df94f8..b52fa9cc87 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -43,6 +43,7 @@ func (r *Router) matchDNS(ctx context.Context) (context.Context, dns.Transport, panic("no context") } for i, rule := range r.dnsRules { + metadata.ResetRuleCache() if rule.Match(metadata) { detour := rule.Outbound() transport, loaded := r.transportMap[detour] diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index 638d00df62..e0a572c92f 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -13,8 +13,6 @@ import ( "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geosite" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/rw" @@ -243,71 +241,3 @@ func (r *Router) downloadGeositeDatabase(savePath string) error { } return err } - -func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool { - for _, rule := range rules { - switch rule.Type { - case C.RuleTypeDefault: - if cond(rule.DefaultOptions) { - return true - } - case C.RuleTypeLogical: - if hasRule(rule.LogicalOptions.Rules, cond) { - return true - } - } - } - return false -} - -func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool { - for _, rule := range rules { - switch rule.Type { - case C.RuleTypeDefault: - if cond(rule.DefaultOptions) { - return true - } - case C.RuleTypeLogical: - if hasDNSRule(rule.LogicalOptions.Rules, cond) { - return true - } - } - } - return false -} - -func isGeoIPRule(rule option.DefaultRule) bool { - return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode) -} - -func isGeoIPDNSRule(rule option.DefaultDNSRule) bool { - return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) -} - -func isGeositeRule(rule option.DefaultRule) bool { - return len(rule.Geosite) > 0 -} - -func isGeositeDNSRule(rule option.DefaultDNSRule) bool { - return len(rule.Geosite) > 0 -} - -func isProcessRule(rule option.DefaultRule) bool { - return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 -} - -func isProcessDNSRule(rule option.DefaultDNSRule) bool { - return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 -} - -func notPrivateNode(code string) bool { - return code != "private" -} - -func isWIFIRule(rule option.DefaultRule) bool { - return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 -} - -func isWIFIDNSRule(rule option.DefaultDNSRule) bool { - return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 -} diff --git a/route/router_rule.go b/route/router_rule.go new file mode 100644 index 0000000000..9850b5bc10 --- /dev/null +++ b/route/router_rule.go @@ -0,0 +1,99 @@ +package route + +import ( + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" +) + +func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if hasRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} + +func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if hasDNSRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} + +func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if hasHeadlessRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} + +func isGeoIPRule(rule option.DefaultRule) bool { + return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode) +} + +func isGeoIPDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) +} + +func isGeositeRule(rule option.DefaultRule) bool { + return len(rule.Geosite) > 0 +} + +func isGeositeDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.Geosite) > 0 +} + +func isProcessRule(rule option.DefaultRule) bool { + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 +} + +func isProcessDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 +} + +func isProcessHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 +} + +func notPrivateNode(code string) bool { + return code != "private" +} + +func isWIFIRule(rule option.DefaultRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} + +func isWIFIDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} + +func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} diff --git a/route/rule_abstract.go b/route/rule_abstract.go index 38d4d57d41..7a4a759e63 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -1,6 +1,7 @@ package route import ( + "io" "strings" "github.com/sagernet/sing-box/adapter" @@ -16,6 +17,7 @@ type abstractDefaultRule struct { destinationAddressItems []RuleItem destinationPortItems []RuleItem allItems []RuleItem + ruleSetItem RuleItem invert bool outbound string } @@ -61,64 +63,64 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { return true } - for _, item := range r.items { - if !item.Match(metadata) { - return r.invert - } - } - - if len(r.sourceAddressItems) > 0 { - var sourceAddressMatch bool + if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { for _, item := range r.sourceAddressItems { if item.Match(metadata) { - sourceAddressMatch = true + metadata.SourceAddressMatch = true break } } - if !sourceAddressMatch { - return r.invert - } } - if len(r.sourcePortItems) > 0 { - var sourcePortMatch bool + if len(r.sourcePortItems) > 0 && !metadata.SourceAddressMatch { for _, item := range r.sourcePortItems { if item.Match(metadata) { - sourcePortMatch = true + metadata.SourcePortMatch = true break } } - if !sourcePortMatch { - return r.invert - } } - if len(r.destinationAddressItems) > 0 { - var destinationAddressMatch bool + if len(r.destinationAddressItems) > 0 && !metadata.SourceAddressMatch { for _, item := range r.destinationAddressItems { if item.Match(metadata) { - destinationAddressMatch = true + metadata.DestinationAddressMatch = true break } } - if !destinationAddressMatch { - return r.invert - } } - if len(r.destinationPortItems) > 0 { - var destinationPortMatch bool + if len(r.destinationPortItems) > 0 && !metadata.SourceAddressMatch { for _, item := range r.destinationPortItems { if item.Match(metadata) { - destinationPortMatch = true + metadata.DestinationPortMatch = true break } } - if !destinationPortMatch { + } + + for _, item := range r.items { + if !item.Match(metadata) { return r.invert } } + if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { + return r.invert + } + + if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch { + return r.invert + } + + if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch { + return r.invert + } + + if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch { + return r.invert + } + return !r.invert } @@ -135,7 +137,7 @@ func (r *abstractDefaultRule) String() string { } type abstractLogicalRule struct { - rules []adapter.Rule + rules []adapter.HeadlessRule mode string invert bool outbound string @@ -146,7 +148,10 @@ func (r *abstractLogicalRule) Type() string { } func (r *abstractLogicalRule) UpdateGeosite() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (adapter.Rule, bool) { + rule, loaded := it.(adapter.Rule) + return rule, loaded + }) { err := rule.UpdateGeosite() if err != nil { return err @@ -156,7 +161,10 @@ func (r *abstractLogicalRule) UpdateGeosite() error { } func (r *abstractLogicalRule) Start() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) { + rule, loaded := it.(common.Starter) + return rule, loaded + }) { err := rule.Start() if err != nil { return err @@ -166,7 +174,10 @@ func (r *abstractLogicalRule) Start() error { } func (r *abstractLogicalRule) Close() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) { + rule, loaded := it.(io.Closer) + return rule, loaded + }) { err := rule.Close() if err != nil { return err @@ -177,11 +188,13 @@ func (r *abstractLogicalRule) Close() error { func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { if r.mode == C.LogicalTypeAnd { - return common.All(r.rules, func(it adapter.Rule) bool { + return common.All(r.rules, func(it adapter.HeadlessRule) bool { + metadata.ResetRuleCache() return it.Match(metadata) }) != r.invert } else { - return common.Any(r.rules, func(it adapter.Rule) bool { + return common.Any(r.rules, func(it adapter.HeadlessRule) bool { + metadata.ResetRuleCache() return it.Match(metadata) }) != r.invert } diff --git a/route/rule_default.go b/route/rule_default.go index 8c8473abf5..c0ef9eef60 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -194,6 +194,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -206,7 +211,7 @@ type LogicalRule struct { func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { r := &LogicalRule{ abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Outbound, }, diff --git a/route/rule_dns.go b/route/rule_dns.go index b444932582..f5f9fd3583 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -190,6 +190,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, false) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -212,7 +217,7 @@ type LogicalDNSRule struct { func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Server, }, diff --git a/route/rule_headless.go b/route/rule_headless.go new file mode 100644 index 0000000000..9df2ee3036 --- /dev/null +++ b/route/rule_headless.go @@ -0,0 +1,173 @@ +package route + +import ( + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func NewHeadlessRule(router adapter.Router, options option.HeadlessRule) (adapter.HeadlessRule, error) { + switch options.Type { + case "", C.RuleTypeDefault: + if !options.DefaultOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewDefaultHeadlessRule(router, options.DefaultOptions) + case C.RuleTypeLogical: + if !options.LogicalOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewLogicalHeadlessRule(router, options.LogicalOptions) + default: + return nil, E.New("unknown rule type: ", options.Type) + } +} + +var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil) + +type DefaultHeadlessRule struct { + abstractDefaultRule +} + +func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { + rule := &DefaultHeadlessRule{ + abstractDefaultRule{ + invert: options.Invert, + }, + } + if len(options.Network) > 0 { + item := NewNetworkItem(options.Network) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { + item := NewDomainItem(options.Domain, options.DomainSuffix) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.DomainMatcher != nil { + item := NewRawDomainItem(options.DomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainKeyword) > 0 { + item := NewDomainKeywordItem(options.DomainKeyword) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, E.Cause(err, "domain_regex") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceIPCIDR) > 0 { + item, err := NewIPCIDRItem(true, options.SourceIPCIDR) + if err != nil { + return nil, E.Cause(err, "source_ipcidr") + } + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.SourceIPSet != nil { + item := NewRawIPCIDRItem(true, options.SourceIPSet) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ipcidr") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.IPSet != nil { + item := NewRawIPCIDRItem(false, options.IPSet) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePort) > 0 { + item := NewPortItem(true, options.SourcePort) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePortRange) > 0 { + item, err := NewPortRangeItem(true, options.SourcePortRange) + if err != nil { + return nil, E.Cause(err, "source_port_range") + } + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Port) > 0 { + item := NewPortItem(false, options.Port) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PortRange) > 0 { + item, err := NewPortRangeItem(false, options.PortRange) + if err != nil { + return nil, E.Cause(err, "port_range") + } + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessName) > 0 { + item := NewProcessItem(options.ProcessName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPath) > 0 { + item := NewProcessPathItem(options.ProcessPath) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageName) > 0 { + item := NewPackageNameItem(options.PackageName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + return rule, nil +} + +var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil) + +type LogicalHeadlessRule struct { + abstractLogicalRule +} + +func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { + r := &LogicalHeadlessRule{ + abstractLogicalRule{ + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + }, + } + switch options.Mode { + case C.LogicalTypeAnd: + r.mode = C.LogicalTypeAnd + case C.LogicalTypeOr: + r.mode = C.LogicalTypeOr + default: + return nil, E.New("unknown logical mode: ", options.Mode) + } + for i, subRule := range options.Rules { + rule, err := NewHeadlessRule(router, subRule) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + r.rules[i] = rule + } + return r, nil +} diff --git a/route/rule_item_cidr.go b/route/rule_item_cidr.go index b72d1e10b1..e17d87def8 100644 --- a/route/rule_item_cidr.go +++ b/route/rule_item_cidr.go @@ -31,7 +31,7 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { builder.Add(addr) continue } - return nil, E.Cause(err, "parse ip_cidr [", i, "]") + return nil, E.Cause(err, "parse [", i, "]") } var description string if isSource { @@ -57,8 +57,23 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { }, nil } +func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { + var description string + if isSource { + description = "source_ipcidr=" + } else { + description = "ipcidr=" + } + description += "" + return &IPCIDRItem{ + ipSet: ipSet, + isSource: isSource, + description: description, + } +} + func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { - if r.isSource { + if r.isSource || metadata.QueryType != 0 || metadata.IPCIDRMatchSource { return r.ipSet.Contains(metadata.Source.Addr) } else { if metadata.Destination.IsIP() { diff --git a/route/rule_item_domain.go b/route/rule_item_domain.go index 6602441deb..d2a11181b0 100644 --- a/route/rule_item_domain.go +++ b/route/rule_item_domain.go @@ -43,6 +43,13 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { } } +func NewRawDomainItem(matcher *domain.Matcher) *DomainItem { + return &DomainItem{ + matcher, + "domain/domain_suffix=", + } +} + func (r *DomainItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go new file mode 100644 index 0000000000..959b2f6110 --- /dev/null +++ b/route/rule_item_rule_set.go @@ -0,0 +1,55 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*RuleSetItem)(nil) + +type RuleSetItem struct { + router adapter.Router + tagList []string + setList []adapter.HeadlessRule + ipcidrMatchSource bool +} + +func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem { + return &RuleSetItem{ + router: router, + tagList: tagList, + ipcidrMatchSource: ipCIDRMatchSource, + } +} + +func (r *RuleSetItem) Start() error { + for _, tag := range r.tagList { + ruleSet, loaded := r.router.RuleSet(tag) + if !loaded { + return E.New("rule-set not found: ", tag) + } + r.setList = append(r.setList, ruleSet) + } + return nil +} + +func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { + metadata.IPCIDRMatchSource = r.ipcidrMatchSource + for _, ruleSet := range r.setList { + if ruleSet.Match(metadata) { + return true + } + } + return false +} + +func (r *RuleSetItem) String() string { + if len(r.tagList) == 1 { + return F.ToString("rule_set=", r.tagList[0]) + } else { + return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]") + } +} diff --git a/route/rule_set.go b/route/rule_set.go new file mode 100644 index 0000000000..f644fb406f --- /dev/null +++ b/route/rule_set.go @@ -0,0 +1,67 @@ +package route + +import ( + "context" + "net" + "net/http" + "sync" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { + switch options.Type { + case C.RuleSetTypeLocal: + return NewLocalRuleSet(router, options) + case C.RuleSetTypeRemote: + return NewRemoteRuleSet(ctx, router, logger, options), nil + default: + return nil, E.New("unknown rule set type: ", options.Type) + } +} + +var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil) + +type RuleSetStartContext struct { + access sync.Mutex + httpClientCache map[string]*http.Client +} + +func NewRuleSetStartContext() *RuleSetStartContext { + return &RuleSetStartContext{ + httpClientCache: make(map[string]*http.Client), + } +} + +func (c *RuleSetStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client { + c.access.Lock() + defer c.access.Unlock() + if httpClient, loaded := c.httpClientCache[detour]; loaded { + return httpClient + } + httpClient := &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + c.httpClientCache[detour] = httpClient + return httpClient +} + +func (c *RuleSetStartContext) Close() { + c.access.Lock() + defer c.access.Unlock() + for _, client := range c.httpClientCache { + client.CloseIdleConnections() + } +} diff --git a/route/rule_set_local.go b/route/rule_set_local.go new file mode 100644 index 0000000000..d89d78c36b --- /dev/null +++ b/route/rule_set_local.go @@ -0,0 +1,82 @@ +package route + +import ( + "context" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ adapter.RuleSet = (*LocalRuleSet)(nil) + +type LocalRuleSet struct { + rules []adapter.HeadlessRule + metadata adapter.RuleSetMetadata +} + +func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { + setFile, err := os.Open(options.LocalOptions.Path) + if err != nil { + return nil, err + } + var plainRuleSet option.PlainRuleSet + switch options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(setFile)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return nil, err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(setFile, false) + if err != nil { + return nil, err + } + default: + return nil, E.New("unknown rule set format: ", options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(router, ruleOptions) + if err != nil { + return nil, E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + var metadata adapter.RuleSetMetadata + metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) + metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) + return &LocalRuleSet{rules, metadata}, nil +} + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { + return nil +} + +func (s *LocalRuleSet) PostStart() error { + return nil +} + +func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata { + return s.metadata +} + +func (s *LocalRuleSet) Close() error { + return nil +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go new file mode 100644 index 0000000000..957e712df1 --- /dev/null +++ b/route/rule_set_remote.go @@ -0,0 +1,264 @@ +package route + +import ( + "bytes" + "context" + "io" + "net" + "net/http" + "runtime" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" +) + +var _ adapter.RuleSet = (*RemoteRuleSet)(nil) + +type RemoteRuleSet struct { + ctx context.Context + cancel context.CancelFunc + router adapter.Router + logger logger.ContextLogger + options option.RuleSet + metadata adapter.RuleSetMetadata + updateInterval time.Duration + dialer N.Dialer + rules []adapter.HeadlessRule + lastUpdated time.Time + lastEtag string + updateTicker *time.Ticker + pauseManager pause.Manager +} + +func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { + ctx, cancel := context.WithCancel(ctx) + var updateInterval time.Duration + if options.RemoteOptions.UpdateInterval > 0 { + updateInterval = time.Duration(options.RemoteOptions.UpdateInterval) + } else { + updateInterval = 24 * time.Hour + } + return &RemoteRuleSet{ + ctx: ctx, + cancel: cancel, + router: router, + logger: logger, + options: options, + updateInterval: updateInterval, + pauseManager: pause.ManagerFromContext(ctx), + } +} + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { + var dialer N.Dialer + if s.options.RemoteOptions.DownloadDetour != "" { + outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour) + if !loaded { + return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour) + } + dialer = outbound + } else { + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + dialer = outbound + } + s.dialer = dialer + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil { + err := s.loadBytes(savedSet.Content) + if err != nil { + return E.Cause(err, "restore cached rule-set") + } + s.lastUpdated = savedSet.LastUpdated + s.lastEtag = savedSet.LastEtag + } + } + if s.lastUpdated.IsZero() { + err := s.fetchOnce(ctx, startContext) + if err != nil { + return E.Cause(err, "initial rule-set: ", s.options.Tag) + } + } + s.updateTicker = time.NewTicker(s.updateInterval) + go s.loopUpdate() + return nil +} + +func (s *RemoteRuleSet) PostStart() error { + if s.lastUpdated.IsZero() { + err := s.fetchOnce(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + return nil +} + +func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { + return s.metadata +} + +func (s *RemoteRuleSet) loadBytes(content []byte) error { + var ( + plainRuleSet option.PlainRuleSet + err error + ) + switch s.options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(bytes.NewReader(content), false) + if err != nil { + return err + } + default: + return E.New("unknown rule set format: ", s.options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(s.router, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) + s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) + s.rules = rules + return nil +} + +func (s *RemoteRuleSet) loopUpdate() { + if time.Since(s.lastUpdated) > s.updateInterval { + err := s.fetchOnce(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + for { + runtime.GC() + select { + case <-s.ctx.Done(): + return + case <-s.updateTicker.C: + s.pauseManager.WaitActive() + err := s.fetchOnce(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + } +} + +func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.RuleSetStartContext) error { + s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) + var httpClient *http.Client + if startContext != nil { + httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer) + } else { + httpClient = &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + } + request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) + if err != nil { + return err + } + if s.lastEtag != "" { + request.Header.Set("If-None-Match", s.lastEtag) + } + response, err := httpClient.Do(request.WithContext(ctx)) + if err != nil { + return err + } + switch response.StatusCode { + case http.StatusOK: + case http.StatusNotModified: + s.lastUpdated = time.Now() + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + savedRuleSet := cacheFile.LoadRuleSet(s.options.Tag) + if savedRuleSet != nil { + savedRuleSet.LastUpdated = s.lastUpdated + err = cacheFile.SaveRuleSet(s.options.Tag, savedRuleSet) + if err != nil { + s.logger.Error("save rule-set updated time: ", err) + return nil + } + } + } + s.logger.Info("update rule-set ", s.options.Tag, ": not modified") + return nil + default: + return E.New("unexpected status: ", response.Status) + } + content, err := io.ReadAll(response.Body) + if err != nil { + response.Body.Close() + return err + } + err = s.loadBytes(content) + if err != nil { + response.Body.Close() + return err + } + response.Body.Close() + eTagHeader := response.Header.Get("Etag") + if eTagHeader != "" { + s.lastEtag = eTagHeader + } + s.lastUpdated = time.Now() + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{ + LastUpdated: s.lastUpdated, + Content: content, + LastEtag: s.lastEtag, + }) + if err != nil { + s.logger.Error("save rule-set cache: ", err) + } + } + s.logger.Info("updated rule-set ", s.options.Tag) + return nil +} + +func (s *RemoteRuleSet) Close() error { + s.updateTicker.Stop() + s.cancel() + return nil +} From 88c88f327912f920df9907469b9d33b3d6689b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Dec 2023 13:24:39 +0800 Subject: [PATCH 05/15] Independent `source_ip_is_private` and `ip_is_private` rules --- option/rule.go | 2 + option/rule_dns.go | 63 ++++++++++++++++---------------- route/rule_default.go | 10 +++++ route/rule_dns.go | 5 +++ route/rule_item_ip_is_private.go | 44 ++++++++++++++++++++++ 5 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 route/rule_item_ip_is_private.go diff --git a/option/rule.go b/option/rule.go index bad605a0fc..1201d123ed 100644 --- a/option/rule.go +++ b/option/rule.go @@ -78,7 +78,9 @@ type DefaultRule struct { SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` GeoIP Listable[string] `json:"geoip,omitempty"` SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` SourcePort Listable[uint16] `json:"source_port,omitempty"` SourcePortRange Listable[string] `json:"source_port_range,omitempty"` Port Listable[uint16] `json:"port,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index c02d09f761..50d9e61266 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -65,37 +65,38 @@ func (r DNSRule) IsValid() bool { } type DefaultDNSRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - Outbound Listable[string] `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet Listable[string] `json:"rule_set,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + Outbound Listable[string] `json:"outbound,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r DefaultDNSRule) IsValid() bool { diff --git a/route/rule_default.go b/route/rule_default.go index c0ef9eef60..1a190ce04b 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -120,6 +120,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.SourceIPIsPrivate { + item := NewIPIsPrivateItem(true) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.IPCIDR) > 0 { item, err := NewIPCIDRItem(false, options.IPCIDR) if err != nil { @@ -128,6 +133,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.IPIsPrivate { + item := NewIPIsPrivateItem(false) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) diff --git a/route/rule_dns.go b/route/rule_dns.go index f5f9fd3583..1f55d50edb 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -119,6 +119,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.SourceIPIsPrivate { + item := NewIPIsPrivateItem(true) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) diff --git a/route/rule_item_ip_is_private.go b/route/rule_item_ip_is_private.go new file mode 100644 index 0000000000..4d511fdfb1 --- /dev/null +++ b/route/rule_item_ip_is_private.go @@ -0,0 +1,44 @@ +package route + +import ( + "net/netip" + + "github.com/sagernet/sing-box/adapter" + N "github.com/sagernet/sing/common/network" +) + +var _ RuleItem = (*IPIsPrivateItem)(nil) + +type IPIsPrivateItem struct { + isSource bool +} + +func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem { + return &IPIsPrivateItem{isSource} +} + +func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool { + var destination netip.Addr + if r.isSource { + destination = metadata.Source.Addr + } else { + destination = metadata.Destination.Addr + } + if destination.IsValid() && !N.IsPublicAddr(destination) { + return true + } + for _, destinationAddress := range metadata.DestinationAddresses { + if !N.IsPublicAddr(destinationAddress) { + return true + } + } + return false +} + +func (r *IPIsPrivateItem) String() string { + if r.isSource { + return "source_ip_is_private=true" + } else { + return "ip_is_private=true" + } +} From e35f99ec8a422a52dc1a9520c84b41a81db7019f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Dec 2023 13:24:18 +0800 Subject: [PATCH 06/15] Update documentation --- docs/configuration/dns/rule.md | 40 +++- docs/configuration/dns/rule.zh.md | 40 +++- docs/configuration/experimental/cache-file.md | 34 +++ docs/configuration/experimental/clash-api.md | 121 ++++++++++ docs/configuration/experimental/index.md | 145 ++---------- docs/configuration/experimental/index.zh.md | 137 ------------ docs/configuration/experimental/v2ray-api.md | 50 +++++ docs/configuration/route/geoip.md | 8 + docs/configuration/route/geoip.zh.md | 33 --- docs/configuration/route/geosite.md | 8 + docs/configuration/route/geosite.zh.md | 33 --- docs/configuration/route/index.md | 30 ++- docs/configuration/route/index.zh.md | 32 ++- docs/configuration/route/rule.md | 62 +++++- docs/configuration/route/rule.zh.md | 60 ++++- docs/configuration/rule-set/headless-rule.md | 207 ++++++++++++++++++ docs/configuration/rule-set/index.md | 97 ++++++++ docs/configuration/rule-set/source-format.md | 34 +++ docs/manual/proxy/client.md | 184 ++++++++++++++++ docs/migration.md | 195 +++++++++++++++++ mkdocs.yml | 28 ++- 21 files changed, 1220 insertions(+), 358 deletions(-) create mode 100644 docs/configuration/experimental/cache-file.md create mode 100644 docs/configuration/experimental/clash-api.md delete mode 100644 docs/configuration/experimental/index.zh.md create mode 100644 docs/configuration/experimental/v2ray-api.md delete mode 100644 docs/configuration/route/geoip.zh.md delete mode 100644 docs/configuration/route/geosite.zh.md create mode 100644 docs/configuration/rule-set/headless-rule.md create mode 100644 docs/configuration/rule-set/index.md create mode 100644 docs/configuration/rule-set/source-format.md create mode 100644 docs/migration.md diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 18e352b407..513da60e03 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -1,3 +1,14 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -46,6 +57,7 @@ "10.0.0.0/24", "192.168.0.1" ], + "source_ip_is_private": false, "source_port": [ 12345 ], @@ -85,6 +97,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -166,15 +182,29 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + Match source geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. + +#### source_ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public source IP. #### source_port @@ -250,6 +280,12 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + #### invert Invert match result. @@ -286,4 +322,4 @@ Rewrite TTL in DNS responses. #### rules -Included default rules. \ No newline at end of file +Included rules. \ No newline at end of file diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 98bfa8ab9a..f6c1f0ffc2 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -1,3 +1,14 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -45,6 +56,7 @@ "source_ip_cidr": [ "10.0.0.0/24" ], + "source_ip_is_private": false, "source_port": [ 12345 ], @@ -84,6 +96,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -163,16 +179,30 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-sets)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 + 匹配源 GeoIP。 #### source_ip_cidr 匹配源 IP CIDR。 +#### source_ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开源 IP。 + #### source_port 匹配源端口。 @@ -245,6 +275,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + #### invert 反选匹配结果。 @@ -281,4 +317,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### rules -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md new file mode 100644 index 0000000000..66e30ef9b0 --- /dev/null +++ b/docs/configuration/experimental/cache-file.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false +} +``` + +### Fields + +#### enabled + +Enable cache file. + +#### path + +Path to the cache file. + +`cache.db` will be used if empty. + +#### cache_id + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md new file mode 100644 index 0000000000..a06fe15480 --- /dev/null +++ b/docs/configuration/experimental/clash-api.md @@ -0,0 +1,121 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + + +!!! quote "" + + Clash API is not included by default, see [Installation](./#installation). + +### Structure + +```json +{ + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" +} +``` + +### Fields + +#### external_controller + +RESTful web API listening address. Clash API will be disabled if empty. + +#### external_ui + +A relative path to the configuration directory or an absolute path to a +directory in which you put some static web resource. sing-box will then +serve it at `http://{{external-controller}}/ui`. + + + +#### external_ui_download_url + +ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. + +`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. + +#### external_ui_download_detour + +The tag of the outbound to download the external UI. + +Default outbound will be used if empty. + +#### secret + +Secret for the RESTful API (optional) +Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` +ALWAYS set a secret if RESTful API is listening on 0.0.0.0 + +#### default_mode + +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. + +#### store_mode + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +Store Clash mode in cache file. + +#### store_selected + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +!!! note "" + + The tag must be set for target outbounds. + +Store selected outbound for the `Selector` outbound in cache file. + +#### store_fakeip + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`. + +Store fakeip in cache file. + +#### cache_file + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`. + +Cache file path, `cache.db` will be used if empty. + +#### cache_id + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`. + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. \ No newline at end of file diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index 308e851c3b..1057e59b36 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -1,139 +1,30 @@ +--- +icon: material/alert-decagram +--- + # Experimental +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + ### Structure ```json { "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} } } ``` -!!! note "" - - Traffic statistics and connection management can degrade performance. - -### Clash API Fields - -!!! quote "" - - Clash API is not included by default, see [Installation](./#installation). - -#### external_controller - -RESTful web API listening address. Clash API will be disabled if empty. - -#### external_ui - -A relative path to the configuration directory or an absolute path to a -directory in which you put some static web resource. sing-box will then -serve it at `http://{{external-controller}}/ui`. - -#### external_ui_download_url - -ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. - -`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. - -#### external_ui_download_detour - -The tag of the outbound to download the external UI. - -Default outbound will be used if empty. - -#### secret - -Secret for the RESTful API (optional) -Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` -ALWAYS set a secret if RESTful API is listening on 0.0.0.0 - -#### default_mode - -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. - -#### store_mode - -Store Clash mode in cache file. - -#### store_selected - -!!! note "" - - The tag must be set for target outbounds. - -Store selected outbound for the `Selector` outbound in cache file. - -#### store_fakeip - -Store fakeip in cache file. - -#### cache_file - -Cache file path, `cache.db` will be used if empty. - -#### cache_id - -Cache ID. - -If not empty, `store_selected` will use a separate store keyed by it. - -### V2Ray API Fields - -!!! quote "" - - V2Ray API is not included by default, see [Installation](./#installation). - -#### listen - -gRPC API listening address. V2Ray API will be disabled if empty. - -#### stats - -Traffic statistics service settings. - -#### stats.enabled - -Enable statistics service. - -#### stats.inbounds - -Inbound list to count traffic. - -#### stats.outbounds - -Outbound list to count traffic. - -#### stats.users +### Fields -User list to count traffic. \ No newline at end of file +| Key | Format | +|--------------|----------------------------| +| `cache_file` | [Cache File](./cache-file) | +| `clash_api` | [Clash API](./clash-api) | +| `v2ray_api` | [V2Ray API](./v2ray-api) | \ No newline at end of file diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md deleted file mode 100644 index 88a95852c9..0000000000 --- a/docs/configuration/experimental/index.zh.md +++ /dev/null @@ -1,137 +0,0 @@ -# 实验性 - -### 结构 - -```json -{ - "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } - } -} -``` - -!!! note "" - - 流量统计和连接管理会降低性能。 - -### Clash API 字段 - -!!! quote "" - - 默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。 - -#### external_controller - -RESTful web API 监听地址。如果为空,则禁用 Clash API。 - -#### external_ui - -到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 - -#### external_ui_download_url - -静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 - -默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 - -#### external_ui_download_detour - -用于下载静态网页资源的出站的标签。 - -如果为空,将使用默认出站。 - -#### secret - -RESTful API 的密钥(可选) -通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 -如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 - -#### default_mode - -Clash 中的默认模式,默认使用 `Rule`。 - -此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 - -#### store_mode - -将 Clash 模式存储在缓存文件中。 - -#### store_selected - -!!! note "" - - 必须为目标出站设置标签。 - -将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 - -#### store_fakeip - -将 fakeip 存储在缓存文件中。 - -#### cache_file - -缓存文件路径,默认使用`cache.db`。 - -#### cache_id - -缓存 ID。 - -如果不为空,`store_selected` 将会使用以此为键的独立存储。 - -### V2Ray API 字段 - -!!! quote "" - - 默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。 - -#### listen - -gRPC API 监听地址。如果为空,则禁用 V2Ray API。 - -#### stats - -流量统计服务设置。 - -#### stats.enabled - -启用统计服务。 - -#### stats.inbounds - -统计流量的入站列表。 - -#### stats.outbounds - -统计流量的出站列表。 - -#### stats.users - -统计流量的用户列表。 \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.md b/docs/configuration/experimental/v2ray-api.md new file mode 100644 index 0000000000..398884242e --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.md @@ -0,0 +1,50 @@ +### Structure + +!!! quote "" + + V2Ray API is not included by default, see [Installation](./#installation). + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### Fields + +#### listen + +gRPC API listening address. V2Ray API will be disabled if empty. + +#### stats + +Traffic statistics service settings. + +#### stats.enabled + +Enable statistics service. + +#### stats.inbounds + +Inbound list to count traffic. + +#### stats.outbounds + +Outbound list to count traffic. + +#### stats.users + +User list to count traffic. \ No newline at end of file diff --git a/docs/configuration/route/geoip.md b/docs/configuration/route/geoip.md index b966a292a5..8a2ed1d4a1 100644 --- a/docs/configuration/route/geoip.md +++ b/docs/configuration/route/geoip.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + ### Structure ```json diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md deleted file mode 100644 index 3ee7042728..0000000000 --- a/docs/configuration/route/geoip.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geoip": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoIP 资源的路径。 - -默认 `geoip.db`。 - -#### download_url - -指定 GeoIP 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`。 - -#### download_detour - -用于下载 GeoIP 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/geosite.md b/docs/configuration/route/geosite.md index db700c6af1..0463057153 100644 --- a/docs/configuration/route/geosite.md +++ b/docs/configuration/route/geosite.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + ### Structure ```json diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md deleted file mode 100644 index bee81fbf71..0000000000 --- a/docs/configuration/route/geosite.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geosite": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoSite 资源的路径。 - -默认 `geosite.db`。 - -#### download_url - -指定 GeoSite 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`。 - -#### download_detour - -用于下载 GeoSite 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 846d497330..7c1787eaea 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # Route +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -8,6 +18,7 @@ "geoip": {}, "geosite": {}, "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -19,11 +30,20 @@ ### Fields -| Key | Format | -|------------|------------------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [Geosite](./geosite) | -| `rules` | List of [Route Rule](./rule) | +| Key | Format | +|-----------|----------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + +#### rules + +List of [Route Rule](./rule) + +#### rule_set + +!!! question "Since sing-box 1.8.0" + +List of [Rule Set](/configuration/rule-set) #### final diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 8bef5bea9a..b5302727a3 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # 路由 +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -7,8 +17,8 @@ "route": { "geoip": {}, "geosite": {}, - "ip_rules": [], "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -20,11 +30,21 @@ ### 字段 -| 键 | 格式 | -|------------|-------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [GeoSite](./geosite) | -| `rules` | 一组 [路由规则](./rule) | +| 键 | 格式 | +|------------|-----------------------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + + +#### rule + +一组 [路由规则](./rule)。 + +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +一组 [规则集](/configuration/rule-set)。 #### final diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index abbfde6fdc..aefc607c24 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,3 +1,17 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -46,10 +60,12 @@ "10.0.0.0/24", "192.168.0.1" ], + "source_ip_is_private": false, "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], + "ip_is_private": false, "source_port": [ 12345 ], @@ -89,6 +105,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -160,23 +180,47 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + Match source geoip. #### geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + Match geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. + +#### ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public IP. #### ip_cidr -Match ip cidr. +Match IP CIDR. + +#### source_ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public source IP. #### source_port @@ -250,6 +294,18 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + +#### rule_set_ipcidr_match_source + +!!! question "Since sing-box 1.8.0" + +Make `ipcidr` in rule sets match the source IP. + #### invert Invert match result. @@ -276,4 +332,4 @@ Tag of the target outbound. ==Required== -Included default rules. +Included rules. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index f4ab7890a7..f735de4836 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,3 +1,17 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -45,9 +59,11 @@ "source_ip_cidr": [ "10.0.0.0/24" ], + "source_ip_is_private": false, "ip_cidr": [ "10.0.0.0/24" ], + "ip_is_private": false, "source_port": [ 12345 ], @@ -87,6 +103,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -158,24 +178,48 @@ #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-sets)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 + 匹配源 GeoIP。 #### geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 + 匹配 GeoIP。 #### source_ip_cidr 匹配源 IP CIDR。 +#### source_ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开源 IP。 + #### ip_cidr 匹配 IP CIDR。 +#### ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开 IP。 + #### source_port 匹配源端口。 @@ -248,6 +292,18 @@ 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.8.0 起" + +使规则集中的 `ipcidr` 规则匹配源 IP。 + #### invert 反选匹配结果。 @@ -274,4 +330,4 @@ ==必填== -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md new file mode 100644 index 0000000000..6ab62eb2e3 --- /dev/null +++ b/docs/configuration/rule-set/headless-rule.md @@ -0,0 +1,207 @@ +--- +icon: material/new-box +--- + +### Structure + +!!! question "Since sing-box 1.8.0" + +```json +{ + "rules": [ + { + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": [ + "tcp" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "package_name": [ + "com.termux" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "invert": false + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false + } + ] +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Default Fields + +!!! note "" + + The default rule uses the following matching logic: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && + (`port` || `port_range`) && + (`source_port` || `source_port_range`) && + `other fields` + +#### query_type + +DNS query type. Values can be integers or type name strings. + +#### network + +`tcp` or `udp`. + +#### domain + +Match full domain. + +#### domain_suffix + +Match domain suffix. + +#### domain_keyword + +Match domain using keyword. + +#### domain_regex + +Match domain using regular expression. + +#### source_ip_cidr + +Match source IP CIDR. + +#### ip_cidr + +!!! info "" + + `ip_cidr` is an alias for `source_ip_cidr` when the Rule Set is used in DNS rules or `rule_set_ipcidr_match_source` enabled in route rules. + +Match IP CIDR. + +#### source_port + +Match source port. + +#### source_port_range + +Match source port range. + +#### port + +Match port. + +#### port_range + +Match port range. + +#### process_name + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process name. + +#### process_path + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path. + +#### package_name + +Match android package name. + +#### wifi_ssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi BSSID. + +#### invert + +Invert match result. + +### Logical Fields + +#### type + +`logical` + +#### mode + +==Required== + +`and` or `or` + +#### rules + +==Required== + +Included rules. diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md new file mode 100644 index 0000000000..5aff55b371 --- /dev/null +++ b/docs/configuration/rule-set/index.md @@ -0,0 +1,97 @@ +--- +icon: material/new-box +--- + +# Rule Set + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "type": "", + "tag": "", + "format": "", + + ... // Typed Fields +} +``` + +#### Local Structure + +```json +{ + "type": "local", + + ... + + "path": "" +} +``` + +#### Remote Structure + +!!! info "" + + Remote rule-set will be cached if `experimental.cache_file.enabled`. + +```json +{ + "type": "remote", + + ..., + + "url": "", + "download_detour": "", + "update_interval": "" +} +``` + +### Fields + +#### type + +==Required== + +Type of Rule Set, `local` or `remote`. + +#### tag + +==Required== + +Tag of Rule Set. + +#### format + +==Required== + +Format of Rule Set, `source` or `binary`. + +### Local Fields + +#### path + +==Required== + +File path of Rule Set. + +### Remote Fields + +#### url + +==Required== + +Download URL of Rule Set. + +#### download_detour + +Tag of the outbound to download rule-set. + +Default outbound will be used if empty. + +#### update_interval + +Update interval of Rule Set. + +`1d` will be used if empty. diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md new file mode 100644 index 0000000000..116c1ee66f --- /dev/null +++ b/docs/configuration/rule-set/source-format.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +# Source Format + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "version": 1, + "rules": [] +} +``` + +### Compile + +Use `sing-box rule-set compile [--output .srs] .json` to compile source to binary rule-set. + +### Fields + +#### version + +==Required== + +Version of Rule Set, must be `1`. + +#### rules + +==Required== + +List of [Headless Rule](./headless-rule.md). diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 60db02deb6..11bc40ceb4 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -343,6 +343,83 @@ flowchart TB } ``` +=== ":material-dns: DNS rules (1.8.0+)" + + !!! info + + DNS rules are optional if FakeIP is used. + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "clash_mode": "Global", + "server": "google" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": [ + "geosite-cn", + "geosite-category-companies@cn" + ] + } + ], + "server": "local" + } + ] + }, + "route": { + "rule_set": [ + { + "type": "remote", + "tag": "geosite-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, + { + "type": "remote", + "tag": "geosite-category-companies@cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs" + } + ] + } + } + ``` + === ":material-router-network: Route rules" ```json @@ -422,4 +499,111 @@ flowchart TB ] } } + ``` + +=== ":material-router-network: Route rules (1.8.0+)" + + ```json + { + "outbounds": [ + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + "type": "logical", + "mode": "or", + "rules": [ + { + "protocol": "dns" + }, + { + "port": 53 + } + ], + "outbound": "dns" + }, + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "clash_mode": "Direct", + "outbound": "direct" + }, + { + "clash_mode": "Global", + "outbound": "default" + }, + { + "type": "logical", + "mode": "or", + "rules": [ + { + "port": 853 + }, + { + "network": "udp", + "port": 443 + }, + { + "protocol": "stun" + } + ], + "outbound": "block" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": [ + "geoip-cn", + "geosite-cn", + "geosite-category-companies@cn" + ] + } + ], + "outbound": "direct" + } + ], + "rule_set": [ + { + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, + { + "type": "remote", + "tag": "geosite-category-companies@cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs" + } + ] + } + } ``` \ No newline at end of file diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000000..aec9b36007 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,195 @@ +--- +icon: material/arrange-bring-forward +--- + +# Migration + +## 1.8.0 + +!!! warning "Unstable" + + This version is still under development, and the following migration guide may be changed in the future. + +### :material-close-box: Migrate cache file from Clash API to independent options + +!!! info "Reference" + + [Clash API](/configuration/experimental/clash-api) / + [Cache File](/configuration/experimental/cache-file) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // default value + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // default value + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate GeoIP to rule sets + +!!! info "Reference" + + [GeoIP](/configuration/route/geoip) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geoip` commands can help you convert custom GeoIP into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geoip": "private", + "outbound": "direct" + }, + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate Geosite to rule sets + +!!! info "Reference" + + [Geosite](/configuration/route/geosite) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geosite` commands can help you convert custom Geosite into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1d4b1d8b0e..c5dd7df335 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,12 +32,16 @@ theme: - content.code.copy - content.code.select - content.code.annotate + icon: + admonition: + question: material/new-box nav: - Home: - index.md + - Change Log: changelog.md + - Migration: migration.md - Deprecated: deprecated.md - Support: support.md - - Change Log: changelog.md - Installation: - Package Manager: installation/package-manager.md - Docker: installation/docker.md @@ -56,7 +60,7 @@ nav: - Proxy: - Server: manual/proxy/server.md - Client: manual/proxy/client.md -# - TUN: manual/proxy/tun.md + # - TUN: manual/proxy/tun.md - Proxy Protocol: - Shadowsocks: manual/proxy-protocol/shadowsocks.md - Trojan: manual/proxy-protocol/trojan.md @@ -79,8 +83,15 @@ nav: - Geosite: configuration/route/geosite.md - Route Rule: configuration/route/rule.md - Protocol Sniff: configuration/route/sniff.md + - Rule Set: + - configuration/rule-set/index.md + - Source Format: configuration/rule-set/source-format.md + - Headless Rule: configuration/rule-set/headless-rule.md - Experimental: - configuration/experimental/index.md + - Cache File: configuration/experimental/cache-file.md + - Clash API: configuration/experimental/clash-api.md + - V2Ray API: configuration/experimental/v2ray-api.md - Shared: - Listen Fields: configuration/shared/listen.md - Dial Fields: configuration/shared/dial.md @@ -180,9 +191,10 @@ plugins: name: 简体中文 nav_translations: Home: 开始 + Change Log: 更新日志 + Migration: 迁移指南 Deprecated: 废弃功能列表 Support: 支持 - Change Log: 更新日志 Installation: 安装 Package Manager: 包管理器 @@ -203,6 +215,10 @@ plugins: Route Rule: 路由规则 Protocol Sniff: 协议探测 + Rule Set: 规则集 + Source Format: 源文件格式 + Headless Rule: 无头规则 + Experimental: 实验性 Shared: 通用 @@ -215,10 +231,6 @@ plugins: Inbound: 入站 Outbound: 出站 - FAQ: 常见问题 - Known Issues: 已知问题 - Examples: 示例 - Linux Server Installation: Linux 服务器安装 - DNS Hijack: DNS 劫持 + Manual: 手册 reconfigure_material: true reconfigure_search: true \ No newline at end of file From 76b26fa76dafd31aa4588c0aff362deee7499611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Dec 2023 20:15:11 +0800 Subject: [PATCH 07/15] Migrate contentjson and badjson to library & Add omitempty in format --- cmd/sing-box/cmd_format.go | 7 +- cmd/sing-box/cmd_geoip_export.go | 2 +- cmd/sing-box/cmd_merge.go | 2 +- cmd/sing-box/cmd_rule_set_compile.go | 2 +- cmd/sing-box/cmd_rule_set_format.go | 2 +- cmd/sing-box/cmd_run.go | 4 +- common/badjson/array.go | 46 --------- common/badjson/json.go | 54 ---------- common/badjson/object.go | 79 --------------- common/badjsonmerge/merge.go | 80 --------------- common/badjsonmerge/merge_test.go | 59 ----------- common/badversion/version_json.go | 2 +- common/json/comment.go | 128 ------------------------ common/json/std.go | 18 ---- debug_http.go | 4 +- experimental/clashapi/api_meta.go | 2 +- experimental/clashapi/api_meta_group.go | 2 +- experimental/clashapi/connections.go | 2 +- experimental/clashapi/proxies.go | 2 +- experimental/clashapi/server.go | 2 +- option/config.go | 2 +- option/inbound.go | 2 +- option/json.go | 4 +- option/outbound.go | 2 +- option/platform.go | 2 +- option/rule.go | 2 +- option/rule_dns.go | 2 +- option/rule_set.go | 2 +- option/tls_acme.go | 2 +- option/types.go | 2 +- option/udp_over_tcp.go | 2 +- option/v2ray_transport.go | 2 +- route/rule_set_local.go | 2 +- route/rule_set_remote.go | 2 +- 34 files changed, 35 insertions(+), 494 deletions(-) delete mode 100644 common/badjson/array.go delete mode 100644 common/badjson/json.go delete mode 100644 common/badjson/object.go delete mode 100644 common/badjsonmerge/merge.go delete mode 100644 common/badjsonmerge/merge_test.go delete mode 100644 common/json/comment.go delete mode 100644 common/json/std.go diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index c5e939e4a9..fc47c5a8dd 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -5,9 +5,10 @@ import ( "os" "path/filepath" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" "github.com/spf13/cobra" ) @@ -37,6 +38,10 @@ func format() error { return err } for _, optionsEntry := range optionsList { + optionsEntry.options, err = badjson.Omitempty(optionsEntry.options) + if err != nil { + return err + } buffer := new(bytes.Buffer) encoder := json.NewEncoder(buffer) encoder.SetIndent("", " ") diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go index d170d10b72..5787d2e5ad 100644 --- a/cmd/sing-box/cmd_geoip_export.go +++ b/cmd/sing-box/cmd_geoip_export.go @@ -6,11 +6,11 @@ import ( "os" "strings" - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" "github.com/oschwald/maxminddb-golang" "github.com/spf13/cobra" diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 4fb07b8688..b5b0838763 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -6,12 +6,12 @@ import ( "path/filepath" "strings" - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" "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/rw" "github.com/spf13/cobra" diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go index de318095ac..e34f5d2b67 100644 --- a/cmd/sing-box/cmd_rule_set_compile.go +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -5,10 +5,10 @@ import ( "os" "strings" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/json" "github.com/spf13/cobra" ) diff --git a/cmd/sing-box/cmd_rule_set_format.go b/cmd/sing-box/cmd_rule_set_format.go index dc3ee6aabd..a3f98bd26d 100644 --- a/cmd/sing-box/cmd_rule_set_format.go +++ b/cmd/sing-box/cmd_rule_set_format.go @@ -6,10 +6,10 @@ import ( "os" "path/filepath" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" "github.com/spf13/cobra" ) diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go index 46f3495fc9..9c54d61e71 100644 --- a/cmd/sing-box/cmd_run.go +++ b/cmd/sing-box/cmd_run.go @@ -13,10 +13,10 @@ import ( "time" "github.com/sagernet/sing-box" - "github.com/sagernet/sing-box/common/badjsonmerge" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badjson" "github.com/spf13/cobra" ) @@ -108,7 +108,7 @@ func readConfigAndMerge() (option.Options, error) { } var mergedOptions option.Options for _, options := range optionsList { - mergedOptions, err = badjsonmerge.MergeOptions(options.options, mergedOptions) + mergedOptions, err = badjson.Merge(options.options, mergedOptions) if err != nil { return option.Options{}, E.Cause(err, "merge config at ", options.path) } diff --git a/common/badjson/array.go b/common/badjson/array.go deleted file mode 100644 index 2a731687dd..0000000000 --- a/common/badjson/array.go +++ /dev/null @@ -1,46 +0,0 @@ -package badjson - -import ( - "bytes" - - "github.com/sagernet/sing-box/common/json" - E "github.com/sagernet/sing/common/exceptions" -) - -type JSONArray []any - -func (a JSONArray) MarshalJSON() ([]byte, error) { - return json.Marshal([]any(a)) -} - -func (a *JSONArray) UnmarshalJSON(content []byte) error { - decoder := json.NewDecoder(bytes.NewReader(content)) - arrayStart, err := decoder.Token() - if err != nil { - return err - } else if arrayStart != json.Delim('[') { - return E.New("excepted array start, but got ", arrayStart) - } - err = a.decodeJSON(decoder) - if err != nil { - return err - } - arrayEnd, err := decoder.Token() - if err != nil { - return err - } else if arrayEnd != json.Delim(']') { - return E.New("excepted array end, but got ", arrayEnd) - } - return nil -} - -func (a *JSONArray) decodeJSON(decoder *json.Decoder) error { - for decoder.More() { - item, err := decodeJSON(decoder) - if err != nil { - return err - } - *a = append(*a, item) - } - return nil -} diff --git a/common/badjson/json.go b/common/badjson/json.go deleted file mode 100644 index e2185b8669..0000000000 --- a/common/badjson/json.go +++ /dev/null @@ -1,54 +0,0 @@ -package badjson - -import ( - "bytes" - - "github.com/sagernet/sing-box/common/json" - E "github.com/sagernet/sing/common/exceptions" -) - -func Decode(content []byte) (any, error) { - decoder := json.NewDecoder(bytes.NewReader(content)) - return decodeJSON(decoder) -} - -func decodeJSON(decoder *json.Decoder) (any, error) { - rawToken, err := decoder.Token() - if err != nil { - return nil, err - } - switch token := rawToken.(type) { - case json.Delim: - switch token { - case '{': - var object JSONObject - err = object.decodeJSON(decoder) - if err != nil { - return nil, err - } - rawToken, err = decoder.Token() - if err != nil { - return nil, err - } else if rawToken != json.Delim('}') { - return nil, E.New("excepted object end, but got ", rawToken) - } - return &object, nil - case '[': - var array JSONArray - err = array.decodeJSON(decoder) - if err != nil { - return nil, err - } - rawToken, err = decoder.Token() - if err != nil { - return nil, err - } else if rawToken != json.Delim(']') { - return nil, E.New("excepted array end, but got ", rawToken) - } - return array, nil - default: - return nil, E.New("excepted object or array end: ", token) - } - } - return rawToken, nil -} diff --git a/common/badjson/object.go b/common/badjson/object.go deleted file mode 100644 index d9c2a36ec0..0000000000 --- a/common/badjson/object.go +++ /dev/null @@ -1,79 +0,0 @@ -package badjson - -import ( - "bytes" - "strings" - - "github.com/sagernet/sing-box/common/json" - E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/x/linkedhashmap" -) - -type JSONObject struct { - linkedhashmap.Map[string, any] -} - -func (m JSONObject) MarshalJSON() ([]byte, error) { - buffer := new(bytes.Buffer) - buffer.WriteString("{") - items := m.Entries() - iLen := len(items) - for i, entry := range items { - keyContent, err := json.Marshal(entry.Key) - if err != nil { - return nil, err - } - buffer.WriteString(strings.TrimSpace(string(keyContent))) - buffer.WriteString(": ") - valueContent, err := json.Marshal(entry.Value) - if err != nil { - return nil, err - } - buffer.WriteString(strings.TrimSpace(string(valueContent))) - if i < iLen-1 { - buffer.WriteString(", ") - } - } - buffer.WriteString("}") - return buffer.Bytes(), nil -} - -func (m *JSONObject) UnmarshalJSON(content []byte) error { - decoder := json.NewDecoder(bytes.NewReader(content)) - m.Clear() - objectStart, err := decoder.Token() - if err != nil { - return err - } else if objectStart != json.Delim('{') { - return E.New("expected json object start, but starts with ", objectStart) - } - err = m.decodeJSON(decoder) - if err != nil { - return E.Cause(err, "decode json object content") - } - objectEnd, err := decoder.Token() - if err != nil { - return err - } else if objectEnd != json.Delim('}') { - return E.New("expected json object end, but ends with ", objectEnd) - } - return nil -} - -func (m *JSONObject) decodeJSON(decoder *json.Decoder) error { - for decoder.More() { - var entryKey string - keyToken, err := decoder.Token() - if err != nil { - return err - } - entryKey = keyToken.(string) - var entryValue any - entryValue, err = decodeJSON(decoder) - if err != nil { - return E.Cause(err, "decode value for ", entryKey) - } - m.Put(entryKey, entryValue) - } - return nil -} diff --git a/common/badjsonmerge/merge.go b/common/badjsonmerge/merge.go deleted file mode 100644 index 39635e6632..0000000000 --- a/common/badjsonmerge/merge.go +++ /dev/null @@ -1,80 +0,0 @@ -package badjsonmerge - -import ( - "encoding/json" - "reflect" - - "github.com/sagernet/sing-box/common/badjson" - "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" -) - -func MergeOptions(source option.Options, destination option.Options) (option.Options, error) { - rawSource, err := json.Marshal(source) - if err != nil { - return option.Options{}, E.Cause(err, "marshal source") - } - rawDestination, err := json.Marshal(destination) - if err != nil { - return option.Options{}, E.Cause(err, "marshal destination") - } - rawMerged, err := MergeJSON(rawSource, rawDestination) - if err != nil { - return option.Options{}, E.Cause(err, "merge options") - } - var merged option.Options - err = json.Unmarshal(rawMerged, &merged) - if err != nil { - return option.Options{}, E.Cause(err, "unmarshal merged options") - } - return merged, nil -} - -func MergeJSON(rawSource json.RawMessage, rawDestination json.RawMessage) (json.RawMessage, error) { - source, err := badjson.Decode(rawSource) - if err != nil { - return nil, E.Cause(err, "decode source") - } - destination, err := badjson.Decode(rawDestination) - if err != nil { - return nil, E.Cause(err, "decode destination") - } - merged, err := mergeJSON(source, destination) - if err != nil { - return nil, err - } - return json.Marshal(merged) -} - -func mergeJSON(anySource any, anyDestination any) (any, error) { - switch destination := anyDestination.(type) { - case badjson.JSONArray: - switch source := anySource.(type) { - case badjson.JSONArray: - destination = append(destination, source...) - default: - destination = append(destination, source) - } - return destination, nil - case *badjson.JSONObject: - switch source := anySource.(type) { - case *badjson.JSONObject: - for _, entry := range source.Entries() { - oldValue, loaded := destination.Get(entry.Key) - if loaded { - var err error - entry.Value, err = mergeJSON(entry.Value, oldValue) - if err != nil { - return nil, E.Cause(err, "merge object item ", entry.Key) - } - } - destination.Put(entry.Key, entry.Value) - } - default: - return nil, E.New("cannot merge json object into ", reflect.TypeOf(destination)) - } - return destination, nil - default: - return destination, nil - } -} diff --git a/common/badjsonmerge/merge_test.go b/common/badjsonmerge/merge_test.go deleted file mode 100644 index be4481b5fa..0000000000 --- a/common/badjsonmerge/merge_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package badjsonmerge - -import ( - "testing" - - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/option" - N "github.com/sagernet/sing/common/network" - - "github.com/stretchr/testify/require" -) - -func TestMergeJSON(t *testing.T) { - t.Parallel() - options := option.Options{ - Log: &option.LogOptions{ - Level: "info", - }, - Route: &option.RouteOptions{ - Rules: []option.Rule{ - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - Network: []string{N.NetworkTCP}, - Outbound: "direct", - }, - }, - }, - }, - } - anotherOptions := option.Options{ - Outbounds: []option.Outbound{ - { - Type: C.TypeDirect, - Tag: "direct", - }, - }, - } - thirdOptions := option.Options{ - Route: &option.RouteOptions{ - Rules: []option.Rule{ - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - Network: []string{N.NetworkUDP}, - Outbound: "direct", - }, - }, - }, - }, - } - mergeOptions, err := MergeOptions(options, anotherOptions) - require.NoError(t, err) - mergeOptions, err = MergeOptions(thirdOptions, mergeOptions) - require.NoError(t, err) - require.Equal(t, "info", mergeOptions.Log.Level) - require.Equal(t, 2, len(mergeOptions.Route.Rules)) - require.Equal(t, C.TypeDirect, mergeOptions.Outbounds[0].Type) -} diff --git a/common/badversion/version_json.go b/common/badversion/version_json.go index 0647b2bfe0..7ec19663a2 100644 --- a/common/badversion/version_json.go +++ b/common/badversion/version_json.go @@ -1,6 +1,6 @@ package badversion -import "github.com/sagernet/sing-box/common/json" +import "github.com/sagernet/sing/common/json" func (v Version) MarshalJSON() ([]byte, error) { return json.Marshal(v.String()) diff --git a/common/json/comment.go b/common/json/comment.go deleted file mode 100644 index 6f3be26233..0000000000 --- a/common/json/comment.go +++ /dev/null @@ -1,128 +0,0 @@ -package json - -import ( - "bufio" - "io" -) - -// kanged from v2ray - -type commentFilterState = byte - -const ( - commentFilterStateContent commentFilterState = iota - commentFilterStateEscape - commentFilterStateDoubleQuote - commentFilterStateDoubleQuoteEscape - commentFilterStateSingleQuote - commentFilterStateSingleQuoteEscape - commentFilterStateComment - commentFilterStateSlash - commentFilterStateMultilineComment - commentFilterStateMultilineCommentStar -) - -type CommentFilter struct { - br *bufio.Reader - state commentFilterState -} - -func NewCommentFilter(reader io.Reader) io.Reader { - return &CommentFilter{br: bufio.NewReader(reader)} -} - -func (v *CommentFilter) Read(b []byte) (int, error) { - p := b[:0] - for len(p) < len(b)-2 { - x, err := v.br.ReadByte() - if err != nil { - if len(p) == 0 { - return 0, err - } - return len(p), nil - } - switch v.state { - case commentFilterStateContent: - switch x { - case '"': - v.state = commentFilterStateDoubleQuote - p = append(p, x) - case '\'': - v.state = commentFilterStateSingleQuote - p = append(p, x) - case '\\': - v.state = commentFilterStateEscape - case '#': - v.state = commentFilterStateComment - case '/': - v.state = commentFilterStateSlash - default: - p = append(p, x) - } - case commentFilterStateEscape: - p = append(p, '\\', x) - v.state = commentFilterStateContent - case commentFilterStateDoubleQuote: - switch x { - case '"': - v.state = commentFilterStateContent - p = append(p, x) - case '\\': - v.state = commentFilterStateDoubleQuoteEscape - default: - p = append(p, x) - } - case commentFilterStateDoubleQuoteEscape: - p = append(p, '\\', x) - v.state = commentFilterStateDoubleQuote - case commentFilterStateSingleQuote: - switch x { - case '\'': - v.state = commentFilterStateContent - p = append(p, x) - case '\\': - v.state = commentFilterStateSingleQuoteEscape - default: - p = append(p, x) - } - case commentFilterStateSingleQuoteEscape: - p = append(p, '\\', x) - v.state = commentFilterStateSingleQuote - case commentFilterStateComment: - if x == '\n' { - v.state = commentFilterStateContent - p = append(p, '\n') - } - case commentFilterStateSlash: - switch x { - case '/': - v.state = commentFilterStateComment - case '*': - v.state = commentFilterStateMultilineComment - default: - p = append(p, '/', x) - } - case commentFilterStateMultilineComment: - switch x { - case '*': - v.state = commentFilterStateMultilineCommentStar - case '\n': - p = append(p, '\n') - } - case commentFilterStateMultilineCommentStar: - switch x { - case '/': - v.state = commentFilterStateContent - case '*': - // Stay - case '\n': - p = append(p, '\n') - default: - v.state = commentFilterStateMultilineComment - } - default: - panic("Unknown state.") - } - } - return len(p), nil -} diff --git a/common/json/std.go b/common/json/std.go deleted file mode 100644 index edc3502bb7..0000000000 --- a/common/json/std.go +++ /dev/null @@ -1,18 +0,0 @@ -package json - -import "encoding/json" - -var ( - Marshal = json.Marshal - Unmarshal = json.Unmarshal - NewEncoder = json.NewEncoder - NewDecoder = json.NewDecoder -) - -type ( - Encoder = json.Encoder - Decoder = json.Decoder - Token = json.Token - Delim = json.Delim - SyntaxError = json.SyntaxError -) diff --git a/debug_http.go b/debug_http.go index 7c675eba54..09f015e7a0 100644 --- a/debug_http.go +++ b/debug_http.go @@ -6,12 +6,12 @@ import ( "runtime" "runtime/debug" - "github.com/sagernet/sing-box/common/badjson" "github.com/sagernet/sing-box/common/humanize" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" "github.com/go-chi/chi/v5" ) diff --git a/experimental/clashapi/api_meta.go b/experimental/clashapi/api_meta.go index 876f98695d..29add8ae94 100644 --- a/experimental/clashapi/api_meta.go +++ b/experimental/clashapi/api_meta.go @@ -6,8 +6,8 @@ import ( "net/http" "time" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing/common/json" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" diff --git a/experimental/clashapi/api_meta_group.go b/experimental/clashapi/api_meta_group.go index 763d3801b7..396dee7f60 100644 --- a/experimental/clashapi/api_meta_group.go +++ b/experimental/clashapi/api_meta_group.go @@ -9,11 +9,11 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/badjson" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" + "github.com/sagernet/sing/common/json/badjson" "github.com/go-chi/chi/v5" "github.com/go-chi/render" diff --git a/experimental/clashapi/connections.go b/experimental/clashapi/connections.go index 042bdd3645..c9471207e8 100644 --- a/experimental/clashapi/connections.go +++ b/experimental/clashapi/connections.go @@ -7,8 +7,8 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing/common/json" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index cf96931a85..7a807c1fae 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -9,12 +9,12 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/badjson" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badjson" N "github.com/sagernet/sing/common/network" "github.com/go-chi/chi/v5" diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index c40ff93846..1eec8448af 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -11,7 +11,6 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" @@ -21,6 +20,7 @@ import ( "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" diff --git a/option/config.go b/option/config.go index ec471112e7..3cec5520e7 100644 --- a/option/config.go +++ b/option/config.go @@ -4,8 +4,8 @@ import ( "bytes" "strings" - "github.com/sagernet/sing-box/common/json" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type _Options struct { diff --git a/option/inbound.go b/option/inbound.go index c61428adb5..76815b5f66 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -1,9 +1,9 @@ package option import ( - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type _Inbound struct { diff --git a/option/json.go b/option/json.go index d010da32f1..07580e9f7b 100644 --- a/option/json.go +++ b/option/json.go @@ -3,10 +3,10 @@ package option import ( "bytes" - "github.com/sagernet/sing-box/common/badjson" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" ) func ToMap(v any) (*badjson.JSONObject, error) { diff --git a/option/outbound.go b/option/outbound.go index 2985319ea5..e01ffed665 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -1,9 +1,9 @@ package option import ( - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" M "github.com/sagernet/sing/common/metadata" ) diff --git a/option/platform.go b/option/platform.go index fd3d73f0c5..a43cbf230f 100644 --- a/option/platform.go +++ b/option/platform.go @@ -1,8 +1,8 @@ package option import ( - "github.com/sagernet/sing-box/common/json" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type OnDemandOptions struct { diff --git a/option/rule.go b/option/rule.go index 1201d123ed..4eb614969e 100644 --- a/option/rule.go +++ b/option/rule.go @@ -3,10 +3,10 @@ package option import ( "reflect" - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type _Rule struct { diff --git a/option/rule_dns.go b/option/rule_dns.go index 50d9e61266..077ec1c884 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -3,10 +3,10 @@ package option import ( "reflect" - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type _DNSRule struct { diff --git a/option/rule_set.go b/option/rule_set.go index 1b75814279..2002be65fe 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -3,12 +3,12 @@ package option import ( "reflect" - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/domain" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" "go4.org/netipx" ) diff --git a/option/tls_acme.go b/option/tls_acme.go index 1068237e6b..5febd09ec1 100644 --- a/option/tls_acme.go +++ b/option/tls_acme.go @@ -1,9 +1,9 @@ package option import ( - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type InboundACMEOptions struct { diff --git a/option/types.go b/option/types.go index 2f029098af..6f0129af6c 100644 --- a/option/types.go +++ b/option/types.go @@ -6,10 +6,10 @@ import ( "strings" "time" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" N "github.com/sagernet/sing/common/network" mDNS "github.com/miekg/dns" diff --git a/option/udp_over_tcp.go b/option/udp_over_tcp.go index 79529624f0..e8a7a9726e 100644 --- a/option/udp_over_tcp.go +++ b/option/udp_over_tcp.go @@ -1,7 +1,7 @@ package option import ( - "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/uot" ) diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go index 63af28a371..ba0332114e 100644 --- a/option/v2ray_transport.go +++ b/option/v2ray_transport.go @@ -1,9 +1,9 @@ package option import ( - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type _V2RayTransportOptions struct { diff --git a/route/rule_set_local.go b/route/rule_set_local.go index d89d78c36b..b6424ab3f4 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -5,11 +5,11 @@ import ( "os" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) var _ adapter.RuleSet = (*LocalRuleSet)(nil) diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 957e712df1..ac939a454a 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -10,11 +10,11 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" From 822c4fa498404fbb7f81506cdd1d27926981e89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Dec 2023 21:48:21 +0800 Subject: [PATCH 08/15] Skip internal fake-ip queries --- route/router.go | 3 +++ route/router_dns.go | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/route/router.go b/route/router.go index aa5a3eb479..98942d44b8 100644 --- a/route/router.go +++ b/route/router.go @@ -261,6 +261,9 @@ func NewRouter( } defaultTransport = transports[0] } + if _, isFakeIP := defaultTransport.(adapter.FakeIPTransport); isFakeIP { + return nil, E.New("default DNS server cannot be fakeip") + } router.defaultTransport = defaultTransport router.transports = transports router.transportMap = transportMap diff --git a/route/router_dns.go b/route/router_dns.go index b52fa9cc87..8ae9171002 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -37,7 +37,7 @@ func (m *DNSReverseMapping) Query(address netip.Addr) (string, bool) { return domain, loaded } -func (r *Router) matchDNS(ctx context.Context) (context.Context, dns.Transport, dns.DomainStrategy) { +func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool) (context.Context, dns.Transport, dns.DomainStrategy) { metadata := adapter.ContextFrom(ctx) if metadata == nil { panic("no context") @@ -51,7 +51,7 @@ func (r *Router) matchDNS(ctx context.Context) (context.Context, dns.Transport, r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour) continue } - if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && metadata.FakeIP { + if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && !allowFakeIP { continue } r.dnsLogger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour) @@ -97,7 +97,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er } metadata.Domain = fqdnToDomain(message.Question[0].Name) } - ctx, transport, strategy := r.matchDNS(ctx) + ctx, transport, strategy := r.matchDNS(ctx, true) ctx, cancel := context.WithTimeout(ctx, C.DNSTimeout) defer cancel() response, err = r.dnsClient.Exchange(ctx, transport, message, strategy) @@ -125,7 +125,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS r.dnsLogger.DebugContext(ctx, "lookup domain ", domain) ctx, metadata := adapter.AppendContext(ctx) metadata.Domain = domain - ctx, transport, transportStrategy := r.matchDNS(ctx) + ctx, transport, transportStrategy := r.matchDNS(ctx, false) if strategy == dns.DomainStrategyAsIS { strategy = transportStrategy } From e07b5059c0729bd9042132971f17bb973e2bb0f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 3 Dec 2023 11:57:53 +0800 Subject: [PATCH 09/15] Add `idle_timeout` for URLTest outbound --- constant/timeout.go | 15 +++++---- docs/configuration/outbound/urltest.md | 13 +++++--- docs/configuration/outbound/urltest.zh.md | 11 +++++-- option/group.go | 1 + outbound/urltest.go | 39 +++++++++++++++++++++-- 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/constant/timeout.go b/constant/timeout.go index db0379a447..756028caba 100644 --- a/constant/timeout.go +++ b/constant/timeout.go @@ -3,11 +3,12 @@ package constant import "time" const ( - TCPTimeout = 5 * time.Second - ReadPayloadTimeout = 300 * time.Millisecond - DNSTimeout = 10 * time.Second - QUICTimeout = 30 * time.Second - STUNTimeout = 15 * time.Second - UDPTimeout = 5 * time.Minute - DefaultURLTestInterval = 1 * time.Minute + TCPTimeout = 5 * time.Second + ReadPayloadTimeout = 300 * time.Millisecond + DNSTimeout = 10 * time.Second + QUICTimeout = 30 * time.Second + STUNTimeout = 15 * time.Second + UDPTimeout = 5 * time.Minute + DefaultURLTestInterval = 3 * time.Minute + DefaultURLTestIdleTimeout = 30 * time.Minute ) diff --git a/docs/configuration/outbound/urltest.md b/docs/configuration/outbound/urltest.md index d905068dfe..f4b3b0aa8e 100644 --- a/docs/configuration/outbound/urltest.md +++ b/docs/configuration/outbound/urltest.md @@ -10,9 +10,10 @@ "proxy-b", "proxy-c" ], - "url": "https://www.gstatic.com/generate_204", - "interval": "1m", - "tolerance": 50, + "url": "", + "interval": "", + "tolerance": 0, + "idle_timeout": "", "interrupt_exist_connections": false } ``` @@ -31,12 +32,16 @@ The URL to test. `https://www.gstatic.com/generate_204` will be used if empty. #### interval -The test interval. `1m` will be used if empty. +The test interval. `3m` will be used if empty. #### tolerance The test tolerance in milliseconds. `50` will be used if empty. +#### idle_timeout + +The idle timeout. `30m` will be used if empty. + #### interrupt_exist_connections Interrupt existing connections when the selected outbound has changed. diff --git a/docs/configuration/outbound/urltest.zh.md b/docs/configuration/outbound/urltest.zh.md index 0ad891f6bd..4372298afc 100644 --- a/docs/configuration/outbound/urltest.zh.md +++ b/docs/configuration/outbound/urltest.zh.md @@ -10,9 +10,10 @@ "proxy-b", "proxy-c" ], - "url": "https://www.gstatic.com/generate_204", - "interval": "1m", + "url": "", + "interval": "", "tolerance": 50, + "idle_timeout": "", "interrupt_exist_connections": false } ``` @@ -31,12 +32,16 @@ #### interval -测试间隔。 默认使用 `1m`。 +测试间隔。 默认使用 `3m`。 #### tolerance 以毫秒为单位的测试容差。 默认使用 `50`。 +#### idle_timeout + +空闲超时。默认使用 `30m`。 + #### interrupt_exist_connections 当选定的出站发生更改时,中断现有连接。 diff --git a/option/group.go b/option/group.go index 58824e808b..72a0f63702 100644 --- a/option/group.go +++ b/option/group.go @@ -11,5 +11,6 @@ type URLTestOutboundOptions struct { URL string `json:"url,omitempty"` Interval Duration `json:"interval,omitempty"` Tolerance uint16 `json:"tolerance,omitempty"` + IdleTimeout Duration `json:"idle_timeout,omitempty"` InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` } diff --git a/outbound/urltest.go b/outbound/urltest.go index 3b14d95bbb..5cae52367e 100644 --- a/outbound/urltest.go +++ b/outbound/urltest.go @@ -35,6 +35,7 @@ type URLTest struct { link string interval time.Duration tolerance uint16 + idleTimeout time.Duration group *URLTestGroup interruptExternalConnections bool } @@ -53,6 +54,7 @@ func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLo link: options.URL, interval: time.Duration(options.Interval), tolerance: options.Tolerance, + idleTimeout: time.Duration(options.IdleTimeout), interruptExternalConnections: options.InterruptExistConnections, } if len(outbound.tags) == 0 { @@ -77,7 +79,21 @@ func (s *URLTest) Start() error { } outbounds = append(outbounds, detour) } - s.group = NewURLTestGroup(s.ctx, s.router, s.logger, outbounds, s.link, s.interval, s.tolerance, s.interruptExternalConnections) + group, err := NewURLTestGroup( + s.ctx, + s.router, + s.logger, + outbounds, + s.link, + s.interval, + s.tolerance, + s.idleTimeout, + s.interruptExternalConnections, + ) + if err != nil { + return err + } + s.group = group return nil } @@ -155,6 +171,7 @@ type URLTestGroup struct { link string interval time.Duration tolerance uint16 + idleTimeout time.Duration history *urltest.HistoryStorage checking atomic.Bool pauseManager pause.Manager @@ -178,14 +195,21 @@ func NewURLTestGroup( link string, interval time.Duration, tolerance uint16, + idleTimeout time.Duration, interruptExternalConnections bool, -) *URLTestGroup { +) (*URLTestGroup, error) { if interval == 0 { interval = C.DefaultURLTestInterval } if tolerance == 0 { tolerance = 50 } + if idleTimeout == 0 { + idleTimeout = C.DefaultURLTestIdleTimeout + } + if interval > idleTimeout { + return nil, E.New("interval must be less or equal than idle_timeout") + } var history *urltest.HistoryStorage if history = service.PtrFromContext[urltest.HistoryStorage](ctx); history != nil { } else if clashServer := router.ClashServer(); clashServer != nil { @@ -201,12 +225,13 @@ func NewURLTestGroup( link: link, interval: interval, tolerance: tolerance, + idleTimeout: idleTimeout, history: history, close: make(chan struct{}), pauseManager: pause.ManagerFromContext(ctx), interruptGroup: interrupt.NewGroup(), interruptExternalConnections: interruptExternalConnections, - } + }, nil } func (g *URLTestGroup) PostStart() { @@ -273,6 +298,7 @@ func (g *URLTestGroup) Select(network string) adapter.Outbound { func (g *URLTestGroup) loopCheck() { if time.Now().Sub(g.lastActive.Load()) > g.interval { + g.lastActive.Store(time.Now()) g.CheckOutbounds(false) } for { @@ -281,6 +307,13 @@ func (g *URLTestGroup) loopCheck() { return case <-g.ticker.C: } + if time.Now().Sub(g.lastActive.Load()) > g.idleTimeout { + g.access.Lock() + g.ticker.Stop() + g.ticker = nil + g.access.Unlock() + return + } g.pauseManager.WaitActive() g.CheckOutbounds(false) } From 90d9efe62c3bf36ba94ba50da6e21e15719a577b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 3 Dec 2023 14:53:22 +0800 Subject: [PATCH 10/15] Update documentation --- docs/changelog.md | 1 - docs/configuration/dns/rule.zh.md | 4 +- docs/configuration/dns/server.md | 10 +- docs/configuration/dns/server.zh.md | 10 +- .../experimental/cache-file.zh.md | 32 +++ docs/configuration/experimental/clash-api.md | 9 +- .../experimental/clash-api.zh.md | 112 ++++++++++ docs/configuration/experimental/index.zh.md | 30 +++ docs/configuration/experimental/v2ray-api.md | 6 +- .../experimental/v2ray-api.zh.md | 50 +++++ docs/configuration/inbound/hysteria.md | 4 - docs/configuration/inbound/hysteria.zh.md | 4 - docs/configuration/inbound/hysteria2.md | 4 - docs/configuration/inbound/hysteria2.zh.md | 4 - docs/configuration/inbound/naive.md | 4 - docs/configuration/inbound/naive.zh.md | 4 - docs/configuration/inbound/tuic.md | 4 - docs/configuration/inbound/tuic.zh.md | 4 - docs/configuration/inbound/tun.md | 2 +- docs/configuration/inbound/tun.zh.md | 2 +- docs/configuration/outbound/hysteria.md | 4 - docs/configuration/outbound/hysteria.zh.md | 4 - docs/configuration/outbound/hysteria2.md | 4 - docs/configuration/outbound/hysteria2.zh.md | 4 - docs/configuration/outbound/index.md | 1 - docs/configuration/outbound/index.zh.md | 1 - docs/configuration/outbound/shadowsocksr.md | 106 ---------- .../configuration/outbound/shadowsocksr.zh.md | 106 ---------- docs/configuration/outbound/tor.md | 2 +- docs/configuration/outbound/tor.zh.md | 2 +- docs/configuration/outbound/tuic.md | 4 - docs/configuration/outbound/tuic.zh.md | 4 - docs/configuration/outbound/wireguard.md | 8 - docs/configuration/outbound/wireguard.zh.md | 8 - docs/configuration/route/geoip.zh.md | 41 ++++ docs/configuration/route/geosite.zh.md | 41 ++++ docs/configuration/route/rule.zh.md | 6 +- docs/configuration/shared/tls.md | 20 -- docs/configuration/shared/tls.zh.md | 21 -- docs/configuration/shared/v2ray-transport.md | 6 +- .../shared/v2ray-transport.zh.md | 6 +- docs/deprecated.md | 32 ++- docs/deprecated.zh.md | 29 ++- docs/installation/build-from-source.md | 2 +- docs/installation/build-from-source.zh.md | 63 ++++++ docs/installation/docker.zh.md | 31 +++ docs/installation/package-manager.zh.md | 90 ++++++++ docs/migration.md | 8 +- docs/migration.zh.md | 193 ++++++++++++++++++ mkdocs.yml | 1 + 50 files changed, 764 insertions(+), 384 deletions(-) create mode 100644 docs/configuration/experimental/cache-file.zh.md create mode 100644 docs/configuration/experimental/clash-api.zh.md create mode 100644 docs/configuration/experimental/index.zh.md create mode 100644 docs/configuration/experimental/v2ray-api.zh.md delete mode 100644 docs/configuration/outbound/shadowsocksr.md delete mode 100644 docs/configuration/outbound/shadowsocksr.zh.md create mode 100644 docs/configuration/route/geoip.zh.md create mode 100644 docs/configuration/route/geosite.zh.md create mode 100644 docs/installation/build-from-source.zh.md create mode 100644 docs/installation/docker.zh.md create mode 100644 docs/installation/package-manager.zh.md create mode 100644 docs/migration.zh.md diff --git a/docs/changelog.md b/docs/changelog.md index c6209e1ad7..44cbe4d6e0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,6 @@ icon: material/alert-decagram --- -# ChangeLog #### 1.7.2 diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index f6c1f0ffc2..6fac585177 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -181,7 +181,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! failure "已在 sing-box 1.8.0 废弃" - Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-sets)。 + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。 匹配 Geosite。 @@ -189,7 +189,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! failure "已在 sing-box 1.8.0 废弃" - GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 匹配源 GeoIP。 diff --git a/docs/configuration/dns/server.md b/docs/configuration/dns/server.md index 2ce50b38e1..8123f4d1c2 100644 --- a/docs/configuration/dns/server.md +++ b/docs/configuration/dns/server.md @@ -45,20 +45,12 @@ The address of the dns server. !!! warning "" - To ensure that system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time. - -!!! warning "" - - QUIC and HTTP3 transport is not included by default, see [Installation](./#installation). + To ensure that Android system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time. !!! info "" the RCode transport is often used to block queries. Use with rules and the `disable_cache` rule option. -!!! warning "" - - DHCP transport is not included by default, see [Installation](./#installation). - | RCode | Description | |-------------------|-----------------------| | `success` | `No error` | diff --git a/docs/configuration/dns/server.zh.md b/docs/configuration/dns/server.zh.md index 36d0fb6344..728590de80 100644 --- a/docs/configuration/dns/server.zh.md +++ b/docs/configuration/dns/server.zh.md @@ -45,20 +45,12 @@ DNS 服务器的地址。 !!! warning "" - 为了确保系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。 - -!!! warning "" - - 默认安装不包含 QUIC 和 HTTP3 传输层,请参阅 [安装](/zh/#_2)。 + 为了确保 Android 系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。 !!! info "" RCode 传输层传输层常用于屏蔽请求. 与 DNS 规则和 `disable_cache` 规则选项一起使用。 -!!! warning "" - - 默认安装不包含 DHCP 传输层,请参阅 [安装](/zh/#_2)。 - | RCode | 描述 | |-------------------|----------| | `success` | `无错误` | diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md new file mode 100644 index 0000000000..f4417ede45 --- /dev/null +++ b/docs/configuration/experimental/cache-file.zh.md @@ -0,0 +1,32 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.8.0 起" + +### 结构 + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false +} +``` + +### 字段 + +#### enabled + +启用缓存文件。 + +#### path + +缓存文件路径,默认使用`cache.db`。 + +#### cache_id + +缓存文件中的标识符。 + +如果不为空,配置特定的数据将使用由其键控的单独存储。 diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md index a06fe15480..0525d14d64 100644 --- a/docs/configuration/experimental/clash-api.md +++ b/docs/configuration/experimental/clash-api.md @@ -10,11 +10,6 @@ icon: material/alert-decagram :material-delete-alert: [cache_file](#cache_file) :material-delete-alert: [cache_id](#cache_id) - -!!! quote "" - - Clash API is not included by default, see [Installation](./#installation). - ### Structure ```json @@ -48,8 +43,6 @@ A relative path to the configuration directory or an absolute path to a directory in which you put some static web resource. sing-box will then serve it at `http://{{external-controller}}/ui`. - - #### external_ui_download_url ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. @@ -118,4 +111,4 @@ Cache file path, `cache.db` will be used if empty. Identifier in cache file. -If not empty, configuration specified data will use a separate store keyed by it. \ No newline at end of file +If not empty, configuration specified data will use a separate store keyed by it. diff --git a/docs/configuration/experimental/clash-api.zh.md b/docs/configuration/experimental/clash-api.zh.md new file mode 100644 index 0000000000..5a490e587b --- /dev/null +++ b/docs/configuration/experimental/clash-api.zh.md @@ -0,0 +1,112 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + +### 结构 + +```json +{ + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" +} +``` + +### Fields + +#### external_controller + +RESTful web API 监听地址。如果为空,则禁用 Clash API。 + +#### external_ui + +到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 + +#### external_ui_download_url + +静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 + +默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 + +#### external_ui_download_detour + +用于下载静态网页资源的出站的标签。 + +如果为空,将使用默认出站。 + +#### secret + +RESTful API 的密钥(可选) +通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 +如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 + +#### default_mode + +Clash 中的默认模式,默认使用 `Rule`。 + +此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 + +#### store_mode + +!!! failure "已在 sing-box 1.8.0 废弃" + + `store_mode` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。 + +将 Clash 模式存储在缓存文件中。 + +#### store_selected + +!!! failure "已在 sing-box 1.8.0 废弃" + + `store_selected` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。 + +!!! note "" + + 必须为目标出站设置标签。 + +将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 + +#### store_fakeip + +!!! failure "已在 sing-box 1.8.0 废弃" + + `store_selected` 已在 Clash API 中废弃,且已迁移到 `cache_file.store_fakeip`。 + +将 fakeip 存储在缓存文件中。 + +#### cache_file + +!!! failure "已在 sing-box 1.8.0 废弃" + + `cache_file` 已在 Clash API 中废弃,且已迁移到 `cache_file.enabled` 和 `cache_file.path`。 + +缓存文件路径,默认使用`cache.db`。 + +#### cache_id + +!!! failure "已在 sing-box 1.8.0 废弃" + + `cache_id` 已在 Clash API 中废弃,且已迁移到 `cache_file.cache_id`。 + +缓存 ID。 + +如果不为空,配置特定的数据将使用由其键控的单独存储。 diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md new file mode 100644 index 0000000000..36fce5f6f4 --- /dev/null +++ b/docs/configuration/experimental/index.zh.md @@ -0,0 +1,30 @@ +--- +icon: material/alert-decagram +--- + +# 实验性 + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + +### 结构 + +```json +{ + "experimental": { + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} + } +} +``` + +### 字段 + +| 键 | 格式 | +|--------------|--------------------------| +| `cache_file` | [缓存文件](./cache-file) | +| `clash_api` | [Clash API](./clash-api) | +| `v2ray_api` | [V2Ray API](./v2ray-api) | \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.md b/docs/configuration/experimental/v2ray-api.md index 398884242e..4e23dea92d 100644 --- a/docs/configuration/experimental/v2ray-api.md +++ b/docs/configuration/experimental/v2ray-api.md @@ -1,8 +1,8 @@ -### Structure - !!! quote "" - V2Ray API is not included by default, see [Installation](./#installation). + V2Ray API is not included by default, see [Installation](/installation/build-from-source/#build-tags). + +### Structure ```json { diff --git a/docs/configuration/experimental/v2ray-api.zh.md b/docs/configuration/experimental/v2ray-api.zh.md new file mode 100644 index 0000000000..81fc842787 --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.zh.md @@ -0,0 +1,50 @@ +!!! quote "" + + 默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#_5)。 + +### 结构 + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### 字段 + +#### listen + +gRPC API 监听地址。如果为空,则禁用 V2Ray API。 + +#### stats + +流量统计服务设置。 + +#### stats.enabled + +启用统计服务。 + +#### stats.inbounds + +统计流量的入站列表。 + +#### stats.outbounds + +统计流量的出站列表。 + +#### stats.users + +统计流量的用户列表。 \ No newline at end of file diff --git a/docs/configuration/inbound/hysteria.md b/docs/configuration/inbound/hysteria.md index f027a05650..789dffeada 100644 --- a/docs/configuration/inbound/hysteria.md +++ b/docs/configuration/inbound/hysteria.md @@ -29,10 +29,6 @@ } ``` -!!! warning "" - - QUIC, which is required by hysteria is not included by default, see [Installation](./#installation). - ### Listen Fields See [Listen Fields](/configuration/shared/listen) for details. diff --git a/docs/configuration/inbound/hysteria.zh.md b/docs/configuration/inbound/hysteria.zh.md index b7534058a9..b75660523e 100644 --- a/docs/configuration/inbound/hysteria.zh.md +++ b/docs/configuration/inbound/hysteria.zh.md @@ -29,10 +29,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 Hysteria 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 4427b6517e..7d9d504f39 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -26,10 +26,6 @@ } ``` -!!! warning "" - - QUIC, which is required by Hysteria2 is not included by default, see [Installation](./#installation). - !!! warning "Difference from official Hysteria2" The official program supports an authentication method called **userpass**, diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index 4d5a94157c..c936aae8f8 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -26,10 +26,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - !!! warning "与官方 Hysteria2 的区别" 官方程序支持一种名为 **userpass** 的验证方式, diff --git a/docs/configuration/inbound/naive.md b/docs/configuration/inbound/naive.md index 562a70703d..83f2656682 100644 --- a/docs/configuration/inbound/naive.md +++ b/docs/configuration/inbound/naive.md @@ -18,10 +18,6 @@ } ``` -!!! warning "" - - HTTP3 transport is not included by default, see [Installation](./#installation). - ### Listen Fields See [Listen Fields](/configuration/shared/listen) for details. diff --git a/docs/configuration/inbound/naive.zh.md b/docs/configuration/inbound/naive.zh.md index 083d64296f..5707e65361 100644 --- a/docs/configuration/inbound/naive.zh.md +++ b/docs/configuration/inbound/naive.zh.md @@ -18,10 +18,6 @@ } ``` -!!! warning "" - - 默认安装不包含 HTTP3 传输层, 参阅 [安装](/zh/#_2)。 - ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/inbound/tuic.md b/docs/configuration/inbound/tuic.md index d4d9aafdb2..04624a895d 100644 --- a/docs/configuration/inbound/tuic.md +++ b/docs/configuration/inbound/tuic.md @@ -22,10 +22,6 @@ } ``` -!!! warning "" - - QUIC, which is required by TUIC is not included by default, see [Installation](./#installation). - ### Listen Fields See [Listen Fields](/configuration/shared/listen) for details. diff --git a/docs/configuration/inbound/tuic.zh.md b/docs/configuration/inbound/tuic.zh.md index 60a7ccf6fd..99252056e1 100644 --- a/docs/configuration/inbound/tuic.zh.md +++ b/docs/configuration/inbound/tuic.zh.md @@ -22,10 +22,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 TUI 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 1d493d58e8..00c5e05069 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -171,7 +171,7 @@ TCP/IP stack. !!! warning "" - gVisor and LWIP stacks is not included by default, see [Installation](./#installation). + LWIP stacks is not included by default, see [Installation](/installation/build-from-source/#build-tags). #### include_interface diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 7ea3a6a030..d22009c293 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -167,7 +167,7 @@ TCP/IP 栈。 !!! warning "" - 默认安装不包含 gVisor 和 LWIP 栈,请参阅 [安装](/zh/#_2)。 + 默认安装不包含 LWIP 栈,参阅 [安装](/zh/installation/build-from-source/#_5)。 #### include_interface diff --git a/docs/configuration/outbound/hysteria.md b/docs/configuration/outbound/hysteria.md index ff9974de61..8caa9248c2 100644 --- a/docs/configuration/outbound/hysteria.md +++ b/docs/configuration/outbound/hysteria.md @@ -24,10 +24,6 @@ } ``` -!!! warning "" - - QUIC, which is required by hysteria is not included by default, see [Installation](./#installation). - ### Fields #### server diff --git a/docs/configuration/outbound/hysteria.zh.md b/docs/configuration/outbound/hysteria.zh.md index 8f0eb3d986..c6ee2313f0 100644 --- a/docs/configuration/outbound/hysteria.zh.md +++ b/docs/configuration/outbound/hysteria.zh.md @@ -24,10 +24,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 Hysteria 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - ### 字段 #### server diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index 90860c1c4f..26d5b72800 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -22,10 +22,6 @@ } ``` -!!! warning "" - - QUIC, which is required by Hysteria2 is not included by default, see [Installation](./#installation). - !!! warning "Difference from official Hysteria2" The official Hysteria2 supports an authentication method called **userpass**, diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index 5d20802794..7176b9a6f7 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -22,10 +22,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - !!! warning "与官方 Hysteria2 的区别" 官方程序支持一种名为 **userpass** 的验证方式, diff --git a/docs/configuration/outbound/index.md b/docs/configuration/outbound/index.md index 3fcd636d9d..2fa8f09a9f 100644 --- a/docs/configuration/outbound/index.md +++ b/docs/configuration/outbound/index.md @@ -26,7 +26,6 @@ | `trojan` | [Trojan](./trojan) | | `wireguard` | [Wireguard](./wireguard) | | `hysteria` | [Hysteria](./hysteria) | -| `shadowsocksr` | [ShadowsocksR](./shadowsocksr) | | `vless` | [VLESS](./vless) | | `shadowtls` | [ShadowTLS](./shadowtls) | | `tuic` | [TUIC](./tuic) | diff --git a/docs/configuration/outbound/index.zh.md b/docs/configuration/outbound/index.zh.md index 3b950e4dab..2f1406f639 100644 --- a/docs/configuration/outbound/index.zh.md +++ b/docs/configuration/outbound/index.zh.md @@ -26,7 +26,6 @@ | `trojan` | [Trojan](./trojan) | | `wireguard` | [Wireguard](./wireguard) | | `hysteria` | [Hysteria](./hysteria) | -| `shadowsocksr` | [ShadowsocksR](./shadowsocksr) | | `vless` | [VLESS](./vless) | | `shadowtls` | [ShadowTLS](./shadowtls) | | `tuic` | [TUIC](./tuic) | diff --git a/docs/configuration/outbound/shadowsocksr.md b/docs/configuration/outbound/shadowsocksr.md deleted file mode 100644 index 0c7f1b32b7..0000000000 --- a/docs/configuration/outbound/shadowsocksr.md +++ /dev/null @@ -1,106 +0,0 @@ -### Structure - -```json -{ - "type": "shadowsocksr", - "tag": "ssr-out", - - "server": "127.0.0.1", - "server_port": 1080, - "method": "aes-128-cfb", - "password": "8JCsPssfgS8tiRwiMlhARg==", - "obfs": "plain", - "obfs_param": "", - "protocol": "origin", - "protocol_param": "", - "network": "udp", - - ... // Dial Fields -} -``` - -!!! warning "" - - The ShadowsocksR protocol is obsolete and unmaintained. This outbound is provided for compatibility only. - -!!! warning "" - - ShadowsocksR is not included by default, see [Installation](./#installation). - -### Fields - -#### server - -==Required== - -The server address. - -#### server_port - -==Required== - -The server port. - -#### method - -==Required== - -Encryption methods: - -* `aes-128-ctr` -* `aes-192-ctr` -* `aes-256-ctr` -* `aes-128-cfb` -* `aes-192-cfb` -* `aes-256-cfb` -* `rc4-md5` -* `chacha20-ietf` -* `xchacha20` - -#### password - -==Required== - -The shadowsocks password. - -#### obfs - -The ShadowsocksR obfuscate. - -* plain -* http_simple -* http_post -* random_head -* tls1.2_ticket_auth - -#### obfs_param - -The ShadowsocksR obfuscate parameter. - -#### protocol - -The ShadowsocksR protocol. - -* origin -* verify_sha1 -* auth_sha1_v4 -* auth_aes128_md5 -* auth_aes128_sha1 -* auth_chain_a -* auth_chain_b - -#### protocol_param - -The ShadowsocksR protocol parameter. - -#### network - -Enabled network - -One of `tcp` `udp`. - -Both is enabled by default. - -### Dial Fields - -See [Dial Fields](/configuration/shared/dial) for details. diff --git a/docs/configuration/outbound/shadowsocksr.zh.md b/docs/configuration/outbound/shadowsocksr.zh.md deleted file mode 100644 index ced3756ad6..0000000000 --- a/docs/configuration/outbound/shadowsocksr.zh.md +++ /dev/null @@ -1,106 +0,0 @@ -### 结构 - -```json -{ - "type": "shadowsocksr", - "tag": "ssr-out", - - "server": "127.0.0.1", - "server_port": 1080, - "method": "aes-128-cfb", - "password": "8JCsPssfgS8tiRwiMlhARg==", - "obfs": "plain", - "obfs_param": "", - "protocol": "origin", - "protocol_param": "", - "network": "udp", - - ... // 拨号字段 -} -``` - -!!! warning "" - - ShadowsocksR 协议已过时且无人维护。 提供此出站仅出于兼容性目的。 - -!!! warning "" - - 默认安装不包含被 ShadowsocksR,参阅 [安装](/zh/#_2)。 - -### 字段 - -#### server - -==必填== - -服务器地址。 - -#### server_port - -==必填== - -服务器端口。 - -#### method - -==必填== - -加密方法: - -* `aes-128-ctr` -* `aes-192-ctr` -* `aes-256-ctr` -* `aes-128-cfb` -* `aes-192-cfb` -* `aes-256-cfb` -* `rc4-md5` -* `chacha20-ietf` -* `xchacha20` - -#### password - -==必填== - -Shadowsocks 密码。 - -#### obfs - -ShadowsocksR 混淆。 - -* plain -* http_simple -* http_post -* random_head -* tls1.2_ticket_auth - -#### obfs_param - -ShadowsocksR 混淆参数。 - -#### protocol - -ShadowsocksR 协议。 - -* origin -* verify_sha1 -* auth_sha1_v4 -* auth_aes128_md5 -* auth_aes128_sha1 -* auth_chain_a -* auth_chain_b - -#### protocol_param - -ShadowsocksR 协议参数。 - -#### network - -启用的网络协议 - -`tcp` 或 `udp`。 - -默认所有。 - -### 拨号字段 - -参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/tor.md b/docs/configuration/outbound/tor.md index fe7e4ff6f9..e3188ea330 100644 --- a/docs/configuration/outbound/tor.md +++ b/docs/configuration/outbound/tor.md @@ -18,7 +18,7 @@ !!! info "" - Embedded tor is not included by default, see [Installation](./#installation). + Embedded Tor is not included by default, see [Installation](/installation/build-from-source/#build-tags). ### Fields diff --git a/docs/configuration/outbound/tor.zh.md b/docs/configuration/outbound/tor.zh.md index 2ddf832b8e..be5059642f 100644 --- a/docs/configuration/outbound/tor.zh.md +++ b/docs/configuration/outbound/tor.zh.md @@ -18,7 +18,7 @@ !!! info "" - 默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/#_2)。 + 默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#_5)。 ### 字段 diff --git a/docs/configuration/outbound/tuic.md b/docs/configuration/outbound/tuic.md index 522e78924a..69a1a6d63c 100644 --- a/docs/configuration/outbound/tuic.md +++ b/docs/configuration/outbound/tuic.md @@ -21,10 +21,6 @@ } ``` -!!! warning "" - - QUIC, which is required by TUIC is not included by default, see [Installation](./#installation). - ### Fields #### server diff --git a/docs/configuration/outbound/tuic.zh.md b/docs/configuration/outbound/tuic.zh.md index 11d44448af..aca0274543 100644 --- a/docs/configuration/outbound/tuic.zh.md +++ b/docs/configuration/outbound/tuic.zh.md @@ -21,10 +21,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 TUI 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - ### 字段 #### server diff --git a/docs/configuration/outbound/wireguard.md b/docs/configuration/outbound/wireguard.md index c9c49c79b8..3fefb7a4fb 100644 --- a/docs/configuration/outbound/wireguard.md +++ b/docs/configuration/outbound/wireguard.md @@ -36,14 +36,6 @@ } ``` -!!! warning "" - - WireGuard is not included by default, see [Installation](./#installation). - -!!! warning "" - - gVisor, which is required by the unprivileged WireGuard is not included by default, see [Installation](./#installation). - ### Fields #### server diff --git a/docs/configuration/outbound/wireguard.zh.md b/docs/configuration/outbound/wireguard.zh.md index 2648247b16..150dda6d68 100644 --- a/docs/configuration/outbound/wireguard.zh.md +++ b/docs/configuration/outbound/wireguard.zh.md @@ -24,14 +24,6 @@ } ``` -!!! warning "" - - 默认安装不包含 WireGuard, 参阅 [安装](/zh/#_2)。 - -!!! warning "" - - 默认安装不包含被非特权 WireGuard 需要的 gVisor, 参阅 [安装](/zh/#_2)。 - ### 字段 #### server diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md new file mode 100644 index 0000000000..fb2481e220 --- /dev/null +++ b/docs/configuration/route/geoip.zh.md @@ -0,0 +1,41 @@ +--- +icon: material/delete-clock +--- + +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 + +### 结构 + +```json +{ + "route": { + "geoip": { + "path": "", + "download_url": "", + "download_detour": "" + } + } +} +``` + +### 字段 + +#### path + +指定 GeoIP 资源的路径。 + +默认 `geoip.db`。 + +#### download_url + +指定 GeoIP 资源的下载链接。 + +默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`。 + +#### download_detour + +用于下载 GeoIP 资源的出站的标签。 + +如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md new file mode 100644 index 0000000000..eeee38ff17 --- /dev/null +++ b/docs/configuration/route/geosite.zh.md @@ -0,0 +1,41 @@ +--- +icon: material/delete-clock +--- + +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。 + +### 结构 + +```json +{ + "route": { + "geosite": { + "path": "", + "download_url": "", + "download_detour": "" + } + } +} +``` + +### 字段 + +#### path + +指定 GeoSite 资源的路径。 + +默认 `geosite.db`。 + +#### download_url + +指定 GeoSite 资源的下载链接。 + +默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`。 + +#### download_detour + +用于下载 GeoSite 资源的出站的标签。 + +如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index f735de4836..06c202b3bf 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -180,7 +180,7 @@ icon: material/alert-decagram !!! failure "已在 sing-box 1.8.0 废弃" - Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-sets)。 + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。 匹配 Geosite。 @@ -188,7 +188,7 @@ icon: material/alert-decagram !!! failure "已在 sing-box 1.8.0 废弃" - GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 匹配源 GeoIP。 @@ -196,7 +196,7 @@ icon: material/alert-decagram !!! failure "已在 sing-box 1.8.0 废弃" - GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 匹配 GeoIP。 diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 9a02bbff2f..9ac0f7c021 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -199,10 +199,6 @@ The path to the server private key, in PEM format. ==Client only== -!!! warning "" - - uTLS is not included by default, see [Installation](./#installation). - !!! note "" uTLS is poorly maintained and the effect may be unproven, use at your own risk. @@ -226,10 +222,6 @@ Chrome fingerprint will be used if empty. ### ECH Fields -!!! warning "" - - ECH is not included by default, see [Installation](./#installation). - ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello message. @@ -278,10 +270,6 @@ If empty, load from DNS will be attempted. ### ACME Fields -!!! warning "" - - ACME is not included by default, see [Installation](./#installation). - #### domain List of domain. @@ -357,14 +345,6 @@ See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge) for details. ### Reality Fields -!!! warning "" - - reality server is not included by default, see [Installation](./#installation). - -!!! warning "" - - uTLS, which is required by reality client is not included by default, see [Installation](./#installation). - #### handshake ==Server only== diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index bbb0871963..f3a7a1a307 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -193,10 +193,6 @@ TLS 版本值: ==仅客户端== -!!! warning "" - - 默认安装不包含 uTLS, 参阅 [安装](/zh/#_2)。 - !!! note "" uTLS 维护不善且其效果可能未经证实,使用风险自负。 @@ -220,14 +216,9 @@ uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻 ## ECH 字段 -!!! warning "" - - 默认安装不包含 ECH, 参阅 [安装](/zh/#_2)。 - ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其 ClientHello 的第一部分 信息。 - ECH 配置和密钥可以通过 `sing-box generate ech-keypair [--pq-signature-schemes-enabled]` 生成。 #### pq_signature_schemes_enabled @@ -273,10 +264,6 @@ ECH PEM 配置路径 ### ACME 字段 -!!! warning "" - - 默认安装不包含 ACME,参阅 [安装](/zh/#_2)。 - #### domain 一组域名。 @@ -348,14 +335,6 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。 ### Reality 字段 -!!! warning "" - - 默认安装不包含 reality 服务器,参阅 [安装](/zh/#_2)。 - -!!! warning "" - - 默认安装不包含被 reality 客户端需要的 uTLS, 参阅 [安装](/zh/#_2)。 - #### handshake ==仅服务器== diff --git a/docs/configuration/shared/v2ray-transport.md b/docs/configuration/shared/v2ray-transport.md index b078bac8af..d29572f6e1 100644 --- a/docs/configuration/shared/v2ray-transport.md +++ b/docs/configuration/shared/v2ray-transport.md @@ -129,10 +129,6 @@ It needs to be consistent with the server. } ``` -!!! warning "" - - QUIC is not included by default, see [Installation](./#installation). - !!! warning "Difference from v2ray-core" No additional encryption support: @@ -142,7 +138,7 @@ It needs to be consistent with the server. !!! note "" - standard gRPC has good compatibility but poor performance and is not included by default, see [Installation](./#installation). + standard gRPC has good compatibility but poor performance and is not included by default, see [Installation](/installation/build-from-source/#build-tags). ```json { diff --git a/docs/configuration/shared/v2ray-transport.zh.md b/docs/configuration/shared/v2ray-transport.zh.md index 2ea93562d6..d0b4775da1 100644 --- a/docs/configuration/shared/v2ray-transport.zh.md +++ b/docs/configuration/shared/v2ray-transport.zh.md @@ -128,10 +128,6 @@ HTTP 请求的额外标头。 } ``` -!!! warning "" - - 默认安装不包含 QUIC, 参阅 [安装](/zh/#_2)。 - !!! warning "与 v2ray-core 的区别" 没有额外的加密支持: @@ -141,7 +137,7 @@ HTTP 请求的额外标头。 !!! note "" - 默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/#_2)。 + 默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#_5)。 ```json { diff --git a/docs/deprecated.md b/docs/deprecated.md index 91a3b25e58..2243acd351 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -4,7 +4,37 @@ icon: material/delete-alert # Deprecated Feature List -### 1.6.0 +## 1.8.0 + +#### Cache file and related features in Clash API + +`cache_file` and related features in Clash API is migrated to independent `cache_file` options, +check [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). + +#### GeoIP + +GeoIP is deprecated and may be removed in the future. + +The maxmind GeoIP National Database, as an IP classification database, +is not entirely suitable for traffic bypassing, +and all existing implementations suffer from high memory usage and difficult management. + +sing-box 1.8.0 introduces [Rule Set](/configuration/rule_set), which can completely replace GeoIP, +check [Migration](/migration/#migrate-geoip-to-rule-sets). + +#### Geosite + +Geosite is deprecated and may be removed in the future. + +Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution, +suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management. + +sing-box 1.8.0 introduces [Rule Set](/configuration/rule_set), which can completely replace Geosite, +check [Migration](/migration/#migrate-geosite-to-rule-sets). + +Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案,存在着大量问题,包括缺少维护、规则不准确、管理困难。 + +## 1.6.0 The following features will be marked deprecated in 1.5.0 and removed entirely in 1.6.0. diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index f1125565f9..c8a61d049d 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -4,7 +4,34 @@ icon: material/delete-alert # 废弃功能列表 -### 1.6.0 +## 1.8.0 + +#### Clash API 中的 Cache file 及相关功能 + +Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 `cache_file` 设置, +参阅 [迁移指南](/zh/migration/#clash-api)。 + +#### GeoIP + +GeoIP 已废弃且可能在不久的将来移除。 + +maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过, +且现有的实现均存在内存使用大与管理困难的问题。 + +sing-box 1.8.0 引入了[规则集](/configuration/rule_set), +可以完全替代 GeoIP, 参阅 [迁移指南](/zh/migration/#geoip)。 + +#### Geosite + +Geosite 已废弃且可能在不久的将来移除。 + +Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案, +存在着包括缺少维护、规则不准确和管理困难内的大量问题。 + +sing-box 1.8.0 引入了[规则集](/configuration/rule_set), +可以完全替代 Geosite,参阅 [迁移指南](/zh/migration/#geosite)。 + +## 1.6.0 下列功能已在 1.5.0 中标记为已弃用,并在 1.6.0 中完全删除。 diff --git a/docs/installation/build-from-source.md b/docs/installation/build-from-source.md index 0ac7169a14..3b098317e8 100644 --- a/docs/installation/build-from-source.md +++ b/docs/installation/build-from-source.md @@ -23,7 +23,7 @@ You can download and install Go from: https://go.dev/doc/install, latest version make ``` -Or build and install binary to `GOBIN`: +Or build and install binary to `$GOBIN`: ```bash make install diff --git a/docs/installation/build-from-source.zh.md b/docs/installation/build-from-source.zh.md new file mode 100644 index 0000000000..69e41925aa --- /dev/null +++ b/docs/installation/build-from-source.zh.md @@ -0,0 +1,63 @@ +--- +icon: material/file-code +--- + +# 从源代码构建 + +## :material-graph: 要求 + +sing-box 1.4.0 前: + +* Go 1.18.5 - 1.20.x + +从 sing-box 1.4.0: + +* Go 1.18.5 - ~ +* Go 1.20.0 - ~ 如果启用构建标记 `with_quic` + +您可以从 https://go.dev/doc/install 下载并安装 Go,推荐使用最新版本。 + +## :material-fast-forward: 快速开始 + +```bash +make +``` + +或者构建二进制文件并将其安装到 `$GOBIN`: + +```bash +make install +``` + +## :material-cog: 自定义构建 + +```bash +TAGS="tag_a tag_b" make +``` + +or + +```bash +go build -tags "tag_a tag_b" ./cmd/sing-box +``` + +## :material-folder-settings: 构建标记 + +| 构建标记 | 默认启动 | 说明 | +|------------------------------------|------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `with_quic` | ✔ | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server), [Naive inbound](/configuration/inbound/naive), [Hysteria Inbound](/configuration/inbound/hysteria), [Hysteria Outbound](/configuration/outbound/hysteria) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). | +| `with_grpc` | ✖️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). | +| `with_dhcp` | ✔ | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server). | +| `with_wireguard` | ✔ | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard). | +| `with_ech` | ✔ | Build with TLS ECH extension support for TLS outbound, see [TLS](/configuration/shared/tls#ech). | +| `with_utls` | ✔ | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | +| `with_reality_server` | ✔ | Build with reality TLS server support, see [TLS](/configuration/shared/tls). | +| `with_acme` | ✔ | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls). | +| `with_clash_api` | ✔ | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | +| `with_v2ray_api` | ✖️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | +| `with_gvisor` | ✔ | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | +| `with_embedded_tor` (CGO required) | ✖️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor). | +| `with_lwip` (CGO required) | ✖️ | Build with LWIP Tun stack support, see [Tun inbound](/configuration/inbound/tun#stack). | + + +除非您确实知道您正在启用什么,否则不建议更改默认构建标签列表。 diff --git a/docs/installation/docker.zh.md b/docs/installation/docker.zh.md new file mode 100644 index 0000000000..ecc66e3a0b --- /dev/null +++ b/docs/installation/docker.zh.md @@ -0,0 +1,31 @@ +--- +icon: material/docker +--- + +# Docker + +## :material-console: 命令 + +```bash +docker run -d \ + -v /etc/sing-box:/etc/sing-box/ \ + --name=sing-box \ + --restart=always \ + ghcr.io/sagernet/sing-box \ + -D /var/lib/sing-box \ + -C /etc/sing-box/ run +``` + +## :material-box-shadow: Compose + +```yaml +version: "3.8" +services: + sing-box: + image: ghcr.io/sagernet/sing-box + container_name: sing-box + restart: always + volumes: + - /etc/sing-box:/etc/sing-box/ + command: -D /var/lib/sing-box -C /etc/sing-box/ run +``` diff --git a/docs/installation/package-manager.zh.md b/docs/installation/package-manager.zh.md new file mode 100644 index 0000000000..3d2aee08e6 --- /dev/null +++ b/docs/installation/package-manager.zh.md @@ -0,0 +1,90 @@ +--- +icon: material/package +--- + +# 包管理器 + +## :material-download-box: 手动安装 + +=== ":material-debian: Debian / DEB" + + ```bash + bash <(curl -fsSL https://sing-box.app/deb-install.sh) + ``` + +=== ":material-redhat: Redhat / RPM" + + ```bash + bash <(curl -fsSL https://sing-box.app/rpm-install.sh) + ``` + +=== ":simple-archlinux: Archlinux / PKG" + + ```bash + bash <(curl -fsSL https://sing-box.app/arch-install.sh) + ``` + +## :material-book-lock-open: 托管安装 + +=== ":material-linux: Linux" + + | 类型 | 平台 | 链接 | 命令 | 活跃维护 | + |----------|--------------------|---------------------|------------------------------|------------------| + | AUR | (Linux) Arch Linux | [sing-box][aur] ᴬᵁᴿ | `? -S sing-box` | :material-check: | + | nixpkgs | (Linux) NixOS | [sing-box][nixpkgs] | `nix-env -iA nixos.sing-box` | :material-check: | + | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: | + | Alpine | (Linux) Alpine | [sing-box][alpine] | `apk add sing-box` | :material-alert: | + +=== ":material-apple: macOS" + + | 类型 | 平台 | 链接 | 命令 | 活跃维护 | + |----------|---------------|------------------|-------------------------|------------------| + | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: | + +=== ":material-microsoft-windows: Windows" + + | 类型 | 平台 | 链接 | 命令 | 活跃维护 | + |------------|---------|--------------------|---------------------------|------------------| + | Scoop | Windows | [sing-box][scoop] | `scoop install sing-box` | :material-check: | + | Chocolatey | Windows | [sing-box][choco] | `choco install sing-box` | :material-check: | + | winget | Windows | [sing-box][winget] | `winget install sing-box` | :material-alert: | + +=== ":material-android: Android" + + | 类型 | 平台 | 链接 | 命令 | 活跃维护 | + |--------|---------|--------------------|--------------------|------------------| + | Termux | Android | [sing-box][termux] | `pkg add sing-box` | :material-check: | + +## :material-book-multiple: 服务管理 + +对于带有 [systemd][systemd] 的 Linux 系统,通常安装已经包含 sing-box 服务, +您可以使用以下命令管理服务: + +| 行动 | 命令 | +|------|-----------------------------------------------| +| 启用 | `sudo systemctl enable sing-box` | +| 禁用 | `sudo systemctl disable sing-box` | +| 启动 | `sudo systemctl start sing-box` | +| 停止 | `sudo systemctl stop sing-box` | +| 强行停止 | `sudo systemctl kill sing-box` | +| 重新启动 | `sudo systemctl restart sing-box` | +| 查看日志 | `sudo journalctl -u sing-box --output cat -e` | +| 实时日志 | `sudo journalctl -u sing-box --output cat -f` | + +[alpine]: https://pkgs.alpinelinux.org/packages?name=sing-box + +[aur]: https://aur.archlinux.org/packages/sing-box + +[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/tools/networking/sing-box/default.nix + +[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box + +[brew]: https://formulae.brew.sh/formula/sing-box + +[choco]: https://chocolatey.org/packages/sing-box + +[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/sing-box.json + +[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/SagerNet/sing-box + +[systemd]: https://systemd.io/ \ No newline at end of file diff --git a/docs/migration.md b/docs/migration.md index aec9b36007..ea2875265a 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,8 +2,6 @@ icon: material/arrange-bring-forward --- -# Migration - ## 1.8.0 !!! warning "Unstable" @@ -12,7 +10,7 @@ icon: material/arrange-bring-forward ### :material-close-box: Migrate cache file from Clash API to independent options -!!! info "Reference" +!!! info "References" [Clash API](/configuration/experimental/clash-api) / [Cache File](/configuration/experimental/cache-file) @@ -50,7 +48,7 @@ icon: material/arrange-bring-forward ### :material-checkbox-intermediate: Migrate GeoIP to rule sets -!!! info "Reference" +!!! info "References" [GeoIP](/configuration/route/geoip) / [Route](/configuration/route) / @@ -135,7 +133,7 @@ icon: material/arrange-bring-forward ### :material-checkbox-intermediate: Migrate Geosite to rule sets -!!! info "Reference" +!!! info "References" [Geosite](/configuration/route/geosite) / [Route](/configuration/route) / diff --git a/docs/migration.zh.md b/docs/migration.zh.md new file mode 100644 index 0000000000..cdde2a6377 --- /dev/null +++ b/docs/migration.zh.md @@ -0,0 +1,193 @@ +--- +icon: material/arrange-bring-forward +--- + +## 1.8.0 + +!!! warning "不稳定的" + + 该版本仍在开发中,迁移指南可能将在未来更改。 + +### :material-close-box: 将缓存文件从 Clash API 迁移到独立选项 + +!!! info "参考" + + [Clash API](/zh/configuration/experimental/clash-api) / + [Cache File](/zh/configuration/experimental/cache-file) + +=== ":material-card-remove: 弃用的" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // 默认值 + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // 默认值 + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: 迁移 GeoIP 到规则集 + +!!! info "参考" + + [GeoIP](/zh/configuration/route/geoip) / + [路由](/zh/configuration/route) / + [路由规则](/zh/configuration/route/rule) / + [DNS 规则](/zh/configuration/dns/rule) / + [规则集](/zh/configuration/rule-set) + +!!! tip + + `sing-box geoip` 命令可以帮助您将自定义 GeoIP 转换为规则集。 + +=== ":material-card-remove: 弃用的" + + ```json + { + "route": { + "rules": [ + { + "geoip": "private", + "outbound": "direct" + }, + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` + +### :material-checkbox-intermediate: 迁移 Geosite 到规则集 + +!!! info "参考" + + [Geosite](/zh/configuration/route/geosite) / + [路由](/zh/configuration/route) / + [路由规则](/zh/configuration/route/rule) / + [DNS 规则](/zh/configuration/dns/rule) / + [规则集](/zh/configuration/rule-set) + +!!! tip + + `sing-box geosite` 命令可以帮助您将自定义 Geosite 转换为规则集。 + +=== ":material-card-remove: 弃用的" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index c5dd7df335..9547a580ef 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -220,6 +220,7 @@ plugins: Headless Rule: 无头规则 Experimental: 实验性 + Cache File: 缓存文件 Shared: 通用 Listen Fields: 监听字段 From 7e2dc5fc5c69c48e4ce634d26aa9fec0d18d7f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 4 Dec 2023 11:47:25 +0800 Subject: [PATCH 11/15] Avoid opening log output before start & Replace tracing logs with task monitor --- box.go | 49 +++++++----- box_outbound.go | 6 +- cmd/sing-box/cmd_run.go | 3 +- cmd/sing-box/main.go | 3 +- common/taskmonitor/monitor.go | 31 ++++++++ constant/timeout.go | 3 + inbound/tun.go | 9 ++- log/default.go | 137 ---------------------------------- log/export.go | 9 ++- log/factory.go | 3 +- log/log.go | 66 +++------------- log/nop.go | 12 ++- log/observable.go | 89 +++++++++++++++------- route/router.go | 59 ++++++++++++--- 14 files changed, 222 insertions(+), 257 deletions(-) create mode 100644 common/taskmonitor/monitor.go delete mode 100644 log/default.go diff --git a/box.go b/box.go index 8c1e3d4b5a..72189d9443 100644 --- a/box.go +++ b/box.go @@ -9,6 +9,8 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/libbox/platform" @@ -230,25 +232,34 @@ func (s *Box) Start() error { } func (s *Box) preStart() error { + monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout) + monitor.Start("start logger") + err := s.logFactory.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start logger") + } for serviceName, service := range s.preServices1 { if preService, isPreService := service.(adapter.PreStarter); isPreService { - s.logger.Trace("pre-start ", serviceName) + monitor.Start("pre-start ", serviceName) err := preService.PreStart() + monitor.Finish() if err != nil { - return E.Cause(err, "pre-starting ", serviceName) + return E.Cause(err, "pre-start ", serviceName) } } } for serviceName, service := range s.preServices2 { if preService, isPreService := service.(adapter.PreStarter); isPreService { - s.logger.Trace("pre-start ", serviceName) + monitor.Start("pre-start ", serviceName) err := preService.PreStart() + monitor.Finish() if err != nil { - return E.Cause(err, "pre-starting ", serviceName) + return E.Cause(err, "pre-start ", serviceName) } } } - err := s.startOutbounds() + err = s.startOutbounds() if err != nil { return err } @@ -261,14 +272,12 @@ func (s *Box) start() error { return err } 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) err = service.Start() if err != nil { return E.Cause(err, "start ", serviceName) @@ -281,7 +290,6 @@ func (s *Box) start() error { } else { tag = in.Tag() } - s.logger.Trace("initializing inbound/", in.Type(), "[", tag, "]") err = in.Start() if err != nil { return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") @@ -292,7 +300,6 @@ func (s *Box) start() error { func (s *Box) postStart() error { for serviceName, service := range s.postServices { - s.logger.Trace("starting ", service) err := service.Start() if err != nil { return E.Cause(err, "start ", serviceName) @@ -300,14 +307,12 @@ func (s *Box) postStart() error { } for _, outbound := range s.outbounds { if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound { - s.logger.Trace("post-starting outbound/", outbound.Tag()) err := lateOutbound.PostStart() if err != nil { return E.Cause(err, "post-start outbound/", outbound.Tag()) } } } - s.logger.Trace("post-starting router") err := s.router.PostStart() if err != nil { return E.Cause(err, "post-start router") @@ -322,47 +327,53 @@ func (s *Box) Close() error { default: close(s.done) } + monitor := taskmonitor.New(s.logger, C.DefaultStopTimeout) var errors error for serviceName, service := range s.postServices { - s.logger.Trace("closing ", serviceName) + monitor.Start("close ", serviceName) errors = E.Append(errors, service.Close(), func(err error) error { return E.Cause(err, "close ", serviceName) }) + monitor.Finish() } for i, in := range s.inbounds { - s.logger.Trace("closing inbound/", in.Type(), "[", i, "]") + monitor.Start("close inbound/", in.Type(), "[", i, "]") errors = E.Append(errors, in.Close(), func(err error) error { return E.Cause(err, "close inbound/", in.Type(), "[", i, "]") }) + monitor.Finish() } for i, out := range s.outbounds { - s.logger.Trace("closing outbound/", out.Type(), "[", i, "]") + monitor.Start("close outbound/", out.Type(), "[", i, "]") errors = E.Append(errors, common.Close(out), func(err error) error { return E.Cause(err, "close outbound/", out.Type(), "[", i, "]") }) + monitor.Finish() } - s.logger.Trace("closing router") + monitor.Start("close router") if err := common.Close(s.router); err != nil { errors = E.Append(errors, err, func(err error) error { return E.Cause(err, "close router") }) } + monitor.Finish() for serviceName, service := range s.preServices1 { - s.logger.Trace("closing ", serviceName) + monitor.Start("close ", serviceName) errors = E.Append(errors, service.Close(), func(err error) error { return E.Cause(err, "close ", serviceName) }) + monitor.Finish() } for serviceName, service := range s.preServices2 { - s.logger.Trace("closing ", serviceName) + monitor.Start("close ", serviceName) errors = E.Append(errors, service.Close(), func(err error) error { return E.Cause(err, "close ", serviceName) }) + monitor.Finish() } - s.logger.Trace("closing log factory") if err := common.Close(s.logFactory); err != nil { errors = E.Append(errors, err, func(err error) error { - return E.Cause(err, "close log factory") + return E.Cause(err, "close logger") }) } return errors diff --git a/box_outbound.go b/box_outbound.go index 676ae7af72..95ba950ad4 100644 --- a/box_outbound.go +++ b/box_outbound.go @@ -4,12 +4,15 @@ import ( "strings" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) func (s *Box) startOutbounds() error { + monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout) outboundTags := make(map[adapter.Outbound]string) outbounds := make(map[string]adapter.Outbound) for i, outboundToStart := range s.outbounds { @@ -43,8 +46,9 @@ func (s *Box) startOutbounds() error { started[outboundTag] = true canContinue = true if starter, isStarter := outboundToStart.(common.Starter); isStarter { - s.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]") + monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") err := starter.Start() + monitor.Finish() if err != nil { return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") } diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go index 9c54d61e71..d76d3b9c00 100644 --- a/cmd/sing-box/cmd_run.go +++ b/cmd/sing-box/cmd_run.go @@ -13,6 +13,7 @@ import ( "time" "github.com/sagernet/sing-box" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" @@ -193,7 +194,7 @@ func run() error { } func closeMonitor(ctx context.Context) { - time.Sleep(3 * time.Second) + time.Sleep(C.DefaultStopFatalTimeout) select { case <-ctx.Done(): return diff --git a/cmd/sing-box/main.go b/cmd/sing-box/main.go index f7974d2cc6..1880d1cbff 100644 --- a/cmd/sing-box/main.go +++ b/cmd/sing-box/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "time" @@ -37,7 +38,7 @@ func main() { func preRun(cmd *cobra.Command, args []string) { if disableColor { - log.SetStdLogger(log.NewFactory(log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, nil).Logger()) + log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger()) } if workingDir != "" { _, err := os.Stat(workingDir) diff --git a/common/taskmonitor/monitor.go b/common/taskmonitor/monitor.go new file mode 100644 index 0000000000..a23990fa79 --- /dev/null +++ b/common/taskmonitor/monitor.go @@ -0,0 +1,31 @@ +package taskmonitor + +import ( + "time" + + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" +) + +type Monitor struct { + logger logger.Logger + timeout time.Duration + timer *time.Timer +} + +func New(logger logger.Logger, timeout time.Duration) *Monitor { + return &Monitor{ + logger: logger, + timeout: timeout, + } +} + +func (m *Monitor) Start(taskName ...any) { + m.timer = time.AfterFunc(m.timeout, func() { + m.logger.Warn(F.ToString(taskName...), " take too much time to finish!") + }) +} + +func (m *Monitor) Finish() { + m.timer.Stop() +} diff --git a/constant/timeout.go b/constant/timeout.go index 756028caba..8d9ffbba7d 100644 --- a/constant/timeout.go +++ b/constant/timeout.go @@ -11,4 +11,7 @@ const ( UDPTimeout = 5 * time.Minute DefaultURLTestInterval = 3 * time.Minute DefaultURLTestIdleTimeout = 30 * time.Minute + DefaultStartTimeout = 10 * time.Second + DefaultStopTimeout = 5 * time.Second + DefaultStopFatalTimeout = 10 * time.Second ) diff --git a/inbound/tun.go b/inbound/tun.go index 7d1f519934..f37e082534 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/log" @@ -141,7 +142,6 @@ func (t *Tun) Tag() string { func (t *Tun) Start() error { if C.IsAndroid && t.platformInterface == nil { - t.logger.Trace("building android rules") t.tunOptions.BuildAndroidRules(t.router.PackageManager(), t) } if t.tunOptions.Name == "" { @@ -151,12 +151,14 @@ func (t *Tun) Start() error { tunInterface tun.Tun err error ) - t.logger.Trace("opening interface") + monitor := taskmonitor.New(t.logger, C.DefaultStartTimeout) + monitor.Start("open tun interface") if t.platformInterface != nil { tunInterface, err = t.platformInterface.OpenTun(&t.tunOptions, t.platformOptions) } else { tunInterface, err = tun.New(t.tunOptions) } + monitor.Finish() if err != nil { return E.Cause(err, "configure tun interface") } @@ -179,8 +181,9 @@ func (t *Tun) Start() error { if err != nil { return err } - t.logger.Trace("starting stack") + monitor.Start("initiating tun stack") err = t.tunStack.Start() + monitor.Finish() if err != nil { return err } diff --git a/log/default.go b/log/default.go deleted file mode 100644 index c2e1c7452e..0000000000 --- a/log/default.go +++ /dev/null @@ -1,137 +0,0 @@ -package log - -import ( - "context" - "io" - "os" - "time" - - C "github.com/sagernet/sing-box/constant" - F "github.com/sagernet/sing/common/format" -) - -var _ Factory = (*simpleFactory)(nil) - -type simpleFactory struct { - formatter Formatter - platformFormatter Formatter - writer io.Writer - platformWriter PlatformWriter - level Level -} - -func NewFactory(formatter Formatter, writer io.Writer, platformWriter PlatformWriter) Factory { - return &simpleFactory{ - formatter: formatter, - platformFormatter: Formatter{ - BaseTime: formatter.BaseTime, - DisableColors: C.IsDarwin || C.IsIos, - DisableLineBreak: true, - }, - writer: writer, - platformWriter: platformWriter, - level: LevelTrace, - } -} - -func (f *simpleFactory) Level() Level { - return f.level -} - -func (f *simpleFactory) SetLevel(level Level) { - f.level = level -} - -func (f *simpleFactory) Logger() ContextLogger { - return f.NewLogger("") -} - -func (f *simpleFactory) NewLogger(tag string) ContextLogger { - return &simpleLogger{f, tag} -} - -func (f *simpleFactory) Close() error { - return nil -} - -var _ ContextLogger = (*simpleLogger)(nil) - -type simpleLogger struct { - *simpleFactory - tag string -} - -func (l *simpleLogger) Log(ctx context.Context, level Level, args []any) { - level = OverrideLevelFromContext(level, ctx) - if level > l.level { - return - } - nowTime := time.Now() - message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime) - if level == LevelPanic { - panic(message) - } - l.writer.Write([]byte(message)) - if level == LevelFatal { - os.Exit(1) - } - if l.platformWriter != nil { - l.platformWriter.WriteMessage(level, l.platformFormatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime)) - } -} - -func (l *simpleLogger) Trace(args ...any) { - l.TraceContext(context.Background(), args...) -} - -func (l *simpleLogger) Debug(args ...any) { - l.DebugContext(context.Background(), args...) -} - -func (l *simpleLogger) Info(args ...any) { - l.InfoContext(context.Background(), args...) -} - -func (l *simpleLogger) Warn(args ...any) { - l.WarnContext(context.Background(), args...) -} - -func (l *simpleLogger) Error(args ...any) { - l.ErrorContext(context.Background(), args...) -} - -func (l *simpleLogger) Fatal(args ...any) { - l.FatalContext(context.Background(), args...) -} - -func (l *simpleLogger) Panic(args ...any) { - l.PanicContext(context.Background(), args...) -} - -func (l *simpleLogger) TraceContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelTrace, args) -} - -func (l *simpleLogger) DebugContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelDebug, args) -} - -func (l *simpleLogger) InfoContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelInfo, args) -} - -func (l *simpleLogger) WarnContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelWarn, args) -} - -func (l *simpleLogger) ErrorContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelError, args) -} - -func (l *simpleLogger) FatalContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelFatal, args) -} - -func (l *simpleLogger) PanicContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelPanic, args) -} diff --git a/log/export.go b/log/export.go index 743fce93ec..60a0abbbfd 100644 --- a/log/export.go +++ b/log/export.go @@ -9,7 +9,14 @@ import ( var std ContextLogger func init() { - std = NewFactory(Formatter{BaseTime: time.Now()}, os.Stderr, nil).Logger() + std = NewDefaultFactory( + context.Background(), + Formatter{BaseTime: time.Now()}, + os.Stderr, + "", + nil, + false, + ).Logger() } func StdLogger() ContextLogger { diff --git a/log/factory.go b/log/factory.go index 739aa0f2d7..54a88228fc 100644 --- a/log/factory.go +++ b/log/factory.go @@ -11,11 +11,12 @@ type ( ) type Factory interface { + Start() error + Close() error Level() Level SetLevel(level Level) Logger() ContextLogger NewLogger(tag string) ContextLogger - Close() error } type ObservableFactory interface { diff --git a/log/log.go b/log/log.go index 19dbbbd912..7b8f284350 100644 --- a/log/log.go +++ b/log/log.go @@ -7,35 +7,9 @@ import ( "time" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/service/filemanager" ) -type factoryWithFile struct { - Factory - file *os.File -} - -func (f *factoryWithFile) Close() error { - return common.Close( - f.Factory, - common.PtrOrNil(f.file), - ) -} - -type observableFactoryWithFile struct { - ObservableFactory - file *os.File -} - -func (f *observableFactoryWithFile) Close() error { - return common.Close( - f.ObservableFactory, - common.PtrOrNil(f.file), - ) -} - type Options struct { Context context.Context Options option.LogOptions @@ -52,8 +26,8 @@ func New(options Options) (Factory, error) { return NewNOPFactory(), nil } - var logFile *os.File var logWriter io.Writer + var logFilePath string switch logOptions.Output { case "": @@ -66,26 +40,23 @@ func New(options Options) (Factory, error) { case "stdout": logWriter = os.Stdout default: - var err error - logFile, err = filemanager.OpenFile(options.Context, logOptions.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return nil, err - } - logWriter = logFile + logFilePath = logOptions.Output } logFormatter := Formatter{ BaseTime: options.BaseTime, - DisableColors: logOptions.DisableColor || logFile != nil, - DisableTimestamp: !logOptions.Timestamp && logFile != nil, + DisableColors: logOptions.DisableColor || logFilePath != "", + DisableTimestamp: !logOptions.Timestamp && logFilePath != "", FullTimestamp: logOptions.Timestamp, TimestampFormat: "-0700 2006-01-02 15:04:05", } - var factory Factory - if options.Observable { - factory = NewObservableFactory(logFormatter, logWriter, options.PlatformWriter) - } else { - factory = NewFactory(logFormatter, logWriter, options.PlatformWriter) - } + factory := NewDefaultFactory( + options.Context, + logFormatter, + logWriter, + logFilePath, + options.PlatformWriter, + options.Observable, + ) if logOptions.Level != "" { logLevel, err := ParseLevel(logOptions.Level) if err != nil { @@ -95,18 +66,5 @@ func New(options Options) (Factory, error) { } else { factory.SetLevel(LevelTrace) } - if logFile != nil { - if options.Observable { - factory = &observableFactoryWithFile{ - ObservableFactory: factory.(ObservableFactory), - file: logFile, - } - } else { - factory = &factoryWithFile{ - Factory: factory, - file: logFile, - } - } - } return factory, nil } diff --git a/log/nop.go b/log/nop.go index 06f0b87241..6369e99b1c 100644 --- a/log/nop.go +++ b/log/nop.go @@ -15,6 +15,14 @@ func NewNOPFactory() ObservableFactory { return (*nopFactory)(nil) } +func (f *nopFactory) Start() error { + return nil +} + +func (f *nopFactory) Close() error { + return nil +} + func (f *nopFactory) Level() Level { return LevelTrace } @@ -72,10 +80,6 @@ func (f *nopFactory) FatalContext(ctx context.Context, args ...any) { func (f *nopFactory) PanicContext(ctx context.Context, args ...any) { } -func (f *nopFactory) Close() error { - return nil -} - func (f *nopFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { return nil, nil, os.ErrInvalid } diff --git a/log/observable.go b/log/observable.go index c12cbbbe24..859d45b469 100644 --- a/log/observable.go +++ b/log/observable.go @@ -9,29 +9,44 @@ import ( "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/service/filemanager" ) -var _ Factory = (*observableFactory)(nil) +var _ Factory = (*defaultFactory)(nil) -type observableFactory struct { +type defaultFactory struct { + ctx context.Context formatter Formatter platformFormatter Formatter writer io.Writer + file *os.File + filePath string platformWriter PlatformWriter + needObservable bool level Level subscriber *observable.Subscriber[Entry] observer *observable.Observer[Entry] } -func NewObservableFactory(formatter Formatter, writer io.Writer, platformWriter PlatformWriter) ObservableFactory { - factory := &observableFactory{ +func NewDefaultFactory( + ctx context.Context, + formatter Formatter, + writer io.Writer, + filePath string, + platformWriter PlatformWriter, + needObservable bool, +) ObservableFactory { + factory := &defaultFactory{ + ctx: ctx, formatter: formatter, platformFormatter: Formatter{ BaseTime: formatter.BaseTime, DisableLineBreak: true, }, writer: writer, + filePath: filePath, platformWriter: platformWriter, + needObservable: needObservable, level: LevelTrace, subscriber: observable.NewSubscriber[Entry](128), } @@ -42,40 +57,53 @@ func NewObservableFactory(formatter Formatter, writer io.Writer, platformWriter return factory } -func (f *observableFactory) Level() Level { +func (f *defaultFactory) Start() error { + if f.filePath != "" { + logFile, err := filemanager.OpenFile(f.ctx, f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + f.writer = logFile + f.file = logFile + } + return nil +} + +func (f *defaultFactory) Close() error { + return common.Close( + common.PtrOrNil(f.file), + f.observer, + ) +} + +func (f *defaultFactory) Level() Level { return f.level } -func (f *observableFactory) SetLevel(level Level) { +func (f *defaultFactory) SetLevel(level Level) { f.level = level } -func (f *observableFactory) Logger() ContextLogger { +func (f *defaultFactory) Logger() ContextLogger { return f.NewLogger("") } -func (f *observableFactory) NewLogger(tag string) ContextLogger { +func (f *defaultFactory) NewLogger(tag string) ContextLogger { return &observableLogger{f, tag} } -func (f *observableFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { +func (f *defaultFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { return f.observer.Subscribe() } -func (f *observableFactory) UnSubscribe(sub observable.Subscription[Entry]) { +func (f *defaultFactory) UnSubscribe(sub observable.Subscription[Entry]) { f.observer.UnSubscribe(sub) } -func (f *observableFactory) Close() error { - return common.Close( - f.observer, - ) -} - var _ ContextLogger = (*observableLogger)(nil) type observableLogger struct { - *observableFactory + *defaultFactory tag string } @@ -85,15 +113,26 @@ func (l *observableLogger) Log(ctx context.Context, level Level, args []any) { return } nowTime := time.Now() - message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), nowTime) - if level == LevelPanic { - panic(message) - } - l.writer.Write([]byte(message)) - if level == LevelFatal { - os.Exit(1) + if l.needObservable { + message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), nowTime) + if level == LevelPanic { + panic(message) + } + l.writer.Write([]byte(message)) + if level == LevelFatal { + os.Exit(1) + } + l.subscriber.Emit(Entry{level, messageSimple}) + } else { + message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime) + if level == LevelPanic { + panic(message) + } + l.writer.Write([]byte(message)) + if level == LevelFatal { + os.Exit(1) + } } - l.subscriber.Emit(Entry{level, messageSimple}) if l.platformWriter != nil { l.platformWriter.WriteMessage(level, l.platformFormatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime)) } diff --git a/route/router.go b/route/router.go index 98942d44b8..6c793b70f9 100644 --- a/route/router.go +++ b/route/router.go @@ -18,6 +18,7 @@ import ( "github.com/sagernet/sing-box/common/geosite" "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/sniff" + "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/log" @@ -414,26 +415,35 @@ func (r *Router) Outbounds() []adapter.Outbound { } func (r *Router) Start() error { + monitor := taskmonitor.New(r.logger, C.DefaultStartTimeout) if r.needGeoIPDatabase { + monitor.Start("initialize geoip database") err := r.prepareGeoIPDatabase() + monitor.Finish() if err != nil { return err } } if r.needGeositeDatabase { + monitor.Start("initialize geosite database") err := r.prepareGeositeDatabase() + monitor.Finish() if err != nil { return err } } if r.interfaceMonitor != nil { + monitor.Start("initialize interface monitor") err := r.interfaceMonitor.Start() + monitor.Finish() if err != nil { return err } } if r.networkMonitor != nil { + monitor.Start("initialize network monitor") err := r.networkMonitor.Start() + monitor.Finish() if err != nil { return err } @@ -459,12 +469,15 @@ func (r *Router) Start() error { r.geositeReader = nil } if r.fakeIPStore != nil { + monitor.Start("initialize fakeip store") err := r.fakeIPStore.Start() + monitor.Finish() if err != nil { return err } } if len(r.ruleSets) > 0 { + monitor.Start("initialize rule-set") ruleSetStartContext := NewRuleSetStartContext() var ruleSetStartGroup task.Group for i, ruleSet := range r.ruleSets { @@ -480,12 +493,12 @@ func (r *Router) Start() error { ruleSetStartGroup.Concurrency(5) ruleSetStartGroup.FastFail() err := ruleSetStartGroup.Run(r.ctx) + monitor.Finish() if err != nil { return err } ruleSetStartContext.Close() } - var ( needProcessFromRuleSet bool needWIFIStateFromRuleSet bool @@ -503,12 +516,16 @@ func (r *Router) Start() error { needPackageManager := C.IsAndroid && r.platformInterface == nil if needPackageManager { + monitor.Start("initialize package manager") packageManager, err := tun.NewPackageManager(r) + monitor.Finish() if err != nil { return E.Cause(err, "create package manager") } if packageManager != nil { + monitor.Start("start package manager") err = packageManager.Start() + monitor.Finish() if err != nil { return err } @@ -519,10 +536,12 @@ func (r *Router) Start() error { if r.platformInterface != nil { r.processSearcher = r.platformInterface } else { + monitor.Start("initialize process searcher") searcher, err := process.NewSearcher(process.Config{ Logger: r.logger, PackageManager: r.packageManager, }) + monitor.Finish() if err != nil { if err != os.ErrInvalid { r.logger.Warn(E.Cause(err, "create process searcher")) @@ -533,34 +552,44 @@ func (r *Router) Start() error { } } if needWIFIStateFromRuleSet || r.needWIFIState { + monitor.Start("initialize WIFI state") if r.platformInterface != nil && r.interfaceMonitor != nil { r.interfaceMonitor.RegisterCallback(func(_ int) { r.updateWIFIState() }) } r.updateWIFIState() + monitor.Finish() } for i, rule := range r.rules { + monitor.Start("initialize rule[", i, "]") err := rule.Start() + monitor.Finish() if err != nil { return E.Cause(err, "initialize rule[", i, "]") } } for i, rule := range r.dnsRules { + monitor.Start("initialize DNS rule[", i, "]") err := rule.Start() + monitor.Finish() if err != nil { return E.Cause(err, "initialize DNS rule[", i, "]") } } for i, transport := range r.transports { + monitor.Start("initialize DNS transport[", i, "]") err := transport.Start() + monitor.Finish() if err != nil { return E.Cause(err, "initialize DNS server[", i, "]") } } if r.timeService != nil { + monitor.Start("initialize time service") err := r.timeService.Start() + monitor.Finish() if err != nil { return E.Cause(err, "initialize time service") } @@ -569,60 +598,70 @@ func (r *Router) Start() error { } func (r *Router) Close() error { + monitor := taskmonitor.New(r.logger, C.DefaultStopTimeout) var err error for i, rule := range r.rules { - r.logger.Trace("closing rule[", i, "]") + monitor.Start("close rule[", i, "]") err = E.Append(err, rule.Close(), func(err error) error { return E.Cause(err, "close rule[", i, "]") }) + monitor.Finish() } for i, rule := range r.dnsRules { - r.logger.Trace("closing dns rule[", i, "]") + monitor.Start("close dns rule[", i, "]") err = E.Append(err, rule.Close(), func(err error) error { return E.Cause(err, "close dns rule[", i, "]") }) + monitor.Finish() } for i, transport := range r.transports { - r.logger.Trace("closing transport[", i, "] ") + monitor.Start("close dns transport[", i, "]") err = E.Append(err, transport.Close(), func(err error) error { return E.Cause(err, "close dns transport[", i, "]") }) + monitor.Finish() } if r.geoIPReader != nil { - r.logger.Trace("closing geoip reader") + monitor.Start("close geoip reader") err = E.Append(err, common.Close(r.geoIPReader), func(err error) error { return E.Cause(err, "close geoip reader") }) + monitor.Finish() } if r.interfaceMonitor != nil { - r.logger.Trace("closing interface monitor") + monitor.Start("close interface monitor") err = E.Append(err, r.interfaceMonitor.Close(), func(err error) error { return E.Cause(err, "close interface monitor") }) + monitor.Finish() } if r.networkMonitor != nil { - r.logger.Trace("closing network monitor") + monitor.Start("close network monitor") err = E.Append(err, r.networkMonitor.Close(), func(err error) error { return E.Cause(err, "close network monitor") }) + monitor.Finish() } if r.packageManager != nil { - r.logger.Trace("closing package manager") + monitor.Start("close package manager") err = E.Append(err, r.packageManager.Close(), func(err error) error { return E.Cause(err, "close package manager") }) + monitor.Finish() } if r.timeService != nil { - r.logger.Trace("closing time service") + monitor.Start("close time service") err = E.Append(err, r.timeService.Close(), func(err error) error { return E.Cause(err, "close time service") }) + monitor.Finish() } if r.fakeIPStore != nil { - r.logger.Trace("closing fakeip store") + monitor.Start("close fakeip store") err = E.Append(err, r.fakeIPStore.Close(), func(err error) error { return E.Cause(err, "close fakeip store") }) + monitor.Finish() } return err } From 29ab114767a2a7181a4f878979d34c3cff68cba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 4 Dec 2023 15:24:25 +0800 Subject: [PATCH 12/15] Remove comparable limit for Listable --- option/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/option/types.go b/option/types.go index 6f0129af6c..aba445eead 100644 --- a/option/types.go +++ b/option/types.go @@ -83,7 +83,7 @@ func (v NetworkList) Build() []string { return strings.Split(string(v), "\n") } -type Listable[T comparable] []T +type Listable[T any] []T func (l Listable[T]) MarshalJSON() ([]byte, error) { arrayList := []T(l) From 41c324d78c9abae07975ffb61784fd5e48a28366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 5 Dec 2023 19:09:13 +0800 Subject: [PATCH 13/15] Make type check strict --- option/inbound.go | 2 ++ option/outbound.go | 2 ++ option/tls_acme.go | 2 ++ option/v2ray_transport.go | 6 +++--- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/option/inbound.go b/option/inbound.go index 76815b5f66..1e1cfa3234 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -109,6 +109,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error { v = &h.TUICOptions case C.TypeHysteria2: v = &h.Hysteria2Options + case "": + return E.New("missing inbound type") default: return E.New("unknown inbound type: ", h.Type) } diff --git a/option/outbound.go b/option/outbound.go index e01ffed665..29c636dcd4 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -119,6 +119,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error { v = &h.SelectorOptions case C.TypeURLTest: v = &h.URLTestOptions + case "": + return E.New("missing outbound type") default: return E.New("unknown outbound type: ", h.Type) } diff --git a/option/tls_acme.go b/option/tls_acme.go index 5febd09ec1..8bff9e9257 100644 --- a/option/tls_acme.go +++ b/option/tls_acme.go @@ -40,6 +40,8 @@ func (o ACMEDNS01ChallengeOptions) MarshalJSON() ([]byte, error) { v = o.AliDNSOptions case C.DNSProviderCloudflare: v = o.CloudflareOptions + case "": + return nil, E.New("missing provider type") default: return nil, E.New("unknown provider type: " + o.Provider) } diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go index ba0332114e..aa0040239e 100644 --- a/option/v2ray_transport.go +++ b/option/v2ray_transport.go @@ -7,7 +7,7 @@ import ( ) type _V2RayTransportOptions struct { - Type string `json:"type,omitempty"` + Type string `json:"type"` HTTPOptions V2RayHTTPOptions `json:"-"` WebsocketOptions V2RayWebsocketOptions `json:"-"` QUICOptions V2RayQUICOptions `json:"-"` @@ -20,8 +20,6 @@ type V2RayTransportOptions _V2RayTransportOptions func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { var v any switch o.Type { - case "": - return nil, nil case C.V2RayTransportTypeHTTP: v = o.HTTPOptions case C.V2RayTransportTypeWebsocket: @@ -32,6 +30,8 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { v = o.GRPCOptions case C.V2RayTransportTypeHTTPUpgrade: v = o.HTTPUpgradeOptions + case "": + return nil, E.New("missing transport type") default: return nil, E.New("unknown transport type: " + o.Type) } From e839711d43996a819c7f3add3286ca1fc53f19fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Dec 2023 14:10:08 +0800 Subject: [PATCH 14/15] documentation: Bump version --- docs/changelog.md | 104 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 5 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 44cbe4d6e0..3ba8653be8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,15 +2,106 @@ icon: material/alert-decagram --- +#### 1.8.0-alpha.13 + +* Fixes and improvements + +#### 1.8.0-alpha.10 + +* Add `idle_timeout` for URLTest outbound **1** +* Fixes and improvements + +**1**: + +When URLTest is idle for a certain period of time, the scheduled delay test will be paused. #### 1.7.2 * Fixes and improvements +#### 1.8.0-alpha.8 + +* Add context to JSON decode error message **1** +* Reject internal fake-ip queries **2** +* Fixes and improvements + +**1**: + +JSON parse errors will now include the current key path. +Only takes effect when compiled with Go 1.21+. + +**2**: + +All internal DNS queries now skip DNS rules with `server` type `fakeip`, +and the default DNS server can no longer be `fakeip`. + +This change is intended to break incorrect usage and essentially requires no action. + +#### 1.8.0-alpha.7 + +* Fixes and improvements + #### 1.7.1 * Fixes and improvements +#### 1.8.0-alpha.6 + +* Fix rule-set matching logic **1** +* Fixes and improvements + +**1**: + +Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets, +rather than completely following the AND logic. + +#### 1.8.0-alpha.5 + +* Parallel rule-set initialization +* Independent `source_ip_is_private` and `ip_is_private` rules **1** + +**1**: + +The `private` GeoIP country never existed and was actually implemented inside V2Ray. +Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets). + +#### 1.8.0-alpha.1 + +* Migrate cache file from Clash API to independent options **1** +* Introducing [Rule Set](/configuration/rule-set) **2** +* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** +* Allow nested logical rules **4** + +**1**: + +See [Cache File](/configuration/experimental/cache-file) and +[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). + +**2**: + +Rule set is independent collections of rules that can be compiled into binaries to improve performance. +Compared to legacy GeoIP and Geosite resources, +it can include more types of rules, load faster, +use less memory, and update automatically. + +See [Route#rule_set](/configuration/route/#rule_set), +[Route Rule](/configuration/route/rule), +[DNS Rule](/configuration/dns/rule), +[Rule Set](/configuration/rule-set), +[Source Format](/configuration/rule-set/source-format) and +[Headless Rule](/configuration/rule-set/headless-rule). + +For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and +[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). + +**3**: + +New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. + +**4**: + +Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. + #### 1.7.0 * Fixes and improvements @@ -149,11 +240,13 @@ Only supported in graphical clients on Android and iOS. **1**: -Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound options. +Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound +options. **2** -Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, see [TCP Brutal](/configuration/shared/tcp-brutal) for details. +Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, +see [TCP Brutal](/configuration/shared/tcp-brutal) for details. #### 1.7.0-alpha.3 @@ -220,8 +313,8 @@ When `auto_route` is enabled and `strict_route` is disabled, the device can now **2**: -Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave. - +Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High +Sierra, 10.14 Mojave. #### 1.6.0-rc.4 @@ -234,7 +327,8 @@ Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008 **1**: -Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave. +Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High +Sierra, 10.14 Mojave. #### 1.6.0-beta.4 From 8041d7ba259e6c5993e774f017071ddcc3edead8 Mon Sep 17 00:00:00 2001 From: Pedro de la Cruz Jr <949341+delaman@users.noreply.github.com> Date: Wed, 6 Dec 2023 21:22:57 -0600 Subject: [PATCH 15/15] Update tor outbound doc --- docs/configuration/outbound/tor.md | 2 +- docs/configuration/outbound/tor.zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/outbound/tor.md b/docs/configuration/outbound/tor.md index e3188ea330..25228218be 100644 --- a/docs/configuration/outbound/tor.md +++ b/docs/configuration/outbound/tor.md @@ -9,7 +9,7 @@ "extra_args": [], "data_directory": "$HOME/.cache/tor", "torrc": { - "ClientOnly": 1 + "ClientOnly": "1" }, ... // Dial Fields diff --git a/docs/configuration/outbound/tor.zh.md b/docs/configuration/outbound/tor.zh.md index be5059642f..2b4b23ad89 100644 --- a/docs/configuration/outbound/tor.zh.md +++ b/docs/configuration/outbound/tor.zh.md @@ -9,7 +9,7 @@ "extra_args": [], "data_directory": "$HOME/.cache/tor", "torrc": { - "ClientOnly": 1 + "ClientOnly": "1" }, ... // 拨号字段