Compare commits

...

9 Commits

Author SHA1 Message Date
世界
039c7e5bcb
documentation: Bump version 2023-11-30 16:29:52 +08:00
世界
218567be46
Independent source_ip_is_private and ip_is_private rules 2023-11-30 16:29:52 +08:00
世界
889f426939
Make rule-set initialization parallel 2023-11-30 16:29:52 +08:00
世界
cb28aba697
documentation: Update rule-set example 2023-11-30 11:05:46 +08:00
世界
bbc1d12015
documentation: Bump version 2023-11-30 10:31:18 +08:00
世界
249e501754
documentation: Add rule set 2023-11-30 10:30:58 +08:00
世界
4e4c0820d5
Add rule set 2023-11-30 10:30:58 +08:00
世界
33881ebd8c
Allow nested logical rules 2023-11-30 10:30:57 +08:00
世界
4d6b7ff054
Migrate to independent cache file 2023-11-30 10:30:57 +08:00
84 changed files with 4113 additions and 718 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
/.idea/ /.idea/
/vendor/ /vendor/
/*.json /*.json
/*.srs
/*.db /*.db
/site/ /site/
/bin/ /bin/

View File

@ -1,11 +1,16 @@
package adapter package adapter
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"io"
"net" "net"
"time"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
) )
type ClashServer interface { type ClashServer interface {
@ -13,22 +18,83 @@ type ClashServer interface {
PreStarter PreStarter
Mode() string Mode() string
ModeList() []string ModeList() []string
StoreSelected() bool
StoreFakeIP() bool
CacheFile() ClashCacheFile
HistoryStorage() *urltest.HistoryStorage HistoryStorage() *urltest.HistoryStorage
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
} }
type ClashCacheFile interface { type CacheFile interface {
Service
PreStarter
StoreFakeIP() bool
FakeIPStorage
LoadMode() string LoadMode() string
StoreMode(mode string) error StoreMode(mode string) error
LoadSelected(group string) string LoadSelected(group string) string
StoreSelected(group string, selected string) error StoreSelected(group string, selected string) error
LoadGroupExpand(group string) (isExpand bool, loaded bool) LoadGroupExpand(group string) (isExpand bool, loaded bool)
StoreGroupExpand(group string, expand bool) error 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 { type Tracker interface {

View File

@ -47,6 +47,7 @@ type InboundContext struct {
GeoIPCode string GeoIPCode string
ProcessInfo *process.Info ProcessInfo *process.Info
FakeIP bool FakeIP bool
IPCIDRMatchSource bool
// dns cache // dns cache

View File

@ -2,12 +2,14 @@ package adapter
import ( import (
"context" "context"
"net/http"
"net/netip" "net/netip"
"github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geoip"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
mdns "github.com/miekg/dns" mdns "github.com/miekg/dns"
@ -18,7 +20,7 @@ type Router interface {
Outbounds() []Outbound Outbounds() []Outbound
Outbound(tag string) (Outbound, bool) Outbound(tag string) (Outbound, bool)
DefaultOutbound(network string) Outbound DefaultOutbound(network string) (Outbound, error)
FakeIPStore() FakeIPStore FakeIPStore() FakeIPStore
@ -27,6 +29,8 @@ type Router interface {
GeoIPReader() *geoip.Reader GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error) LoadGeosite(code string) (Rule, error)
RuleSet(tag string) (RuleSet, bool)
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
@ -61,11 +65,15 @@ func RouterFromContext(ctx context.Context) Router {
return service.FromContext[Router](ctx) return service.FromContext[Router](ctx)
} }
type HeadlessRule interface {
Match(metadata *InboundContext) bool
}
type Rule interface { type Rule interface {
HeadlessRule
Service Service
Type() string Type() string
UpdateGeosite() error UpdateGeosite() error
Match(metadata *InboundContext) bool
Outbound() string Outbound() string
String() string String() string
} }
@ -76,6 +84,17 @@ type DNSRule interface {
RewriteTTL() *uint32 RewriteTTL() *uint32
} }
type RuleSet interface {
StartContext(ctx context.Context, startContext RuleSetStartContext) error
Close() error
HeadlessRule
}
type RuleSetStartContext interface {
HTTPClient(detour string, dialer N.Dialer) *http.Client
Close()
}
type InterfaceUpdateListener interface { type InterfaceUpdateListener interface {
InterfaceUpdated() InterfaceUpdated()
} }

53
box.go
View File

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

View File

@ -7,7 +7,6 @@ import (
"github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -69,41 +68,3 @@ func format() error {
} }
return nil 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
}

43
cmd/sing-box/cmd_geoip.go Normal file
View File

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

View File

@ -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-<country>.srs"
var commandGeoipExport = &cobra.Command{
Use: "export <country>",
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)
}

View File

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

View File

@ -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 <address>",
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
}

View File

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

View File

@ -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-<category>.json"
var commandGeositeExport = &cobra.Command{
Use: "export <category>",
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)
}

View File

@ -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 <category>",
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
}

View File

@ -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] <domain>",
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
}

View File

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

View File

@ -18,7 +18,7 @@ import (
) )
var commandMerge = &cobra.Command{ var commandMerge = &cobra.Command{
Use: "merge [output]", Use: "merge <output>",
Short: "Merge configurations", Short: "Merge configurations",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
err := merge(args[0]) err := merge(args[0])

View File

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

View File

@ -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 = "<file_name>.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
}

View File

@ -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 <source-path>",
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
}

View File

@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) {
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
if outboundTag == "" { if outboundTag == "" {
outbound := instance.Router().DefaultOutbound(N.NetworkName(network)) return instance.Router().DefaultOutbound(N.NetworkName(network))
if outbound == nil {
return nil, E.New("missing default outbound")
}
return outbound, nil
} else { } else {
outbound, loaded := instance.Router().Outbound(outboundTag) outbound, loaded := instance.Router().Outbound(outboundTag)
if !loaded { if !loaded {

View File

@ -18,7 +18,7 @@ import (
var commandConnectFlagNetwork string var commandConnectFlagNetwork string
var commandConnect = &cobra.Command{ var commandConnect = &cobra.Command{
Use: "connect [address]", Use: "connect <address>",
Short: "Connect to an address", Short: "Connect to an address",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {

View File

@ -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) { 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) { 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 { func (d *RouterDialer) Upstream() any {

485
common/srs/binary.go Normal file
View File

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

116
common/srs/ip_set.go Normal file
View File

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

View File

@ -9,3 +9,11 @@ const (
LogicalTypeAnd = "and" LogicalTypeAnd = "and"
LogicalTypeOr = "or" LogicalTypeOr = "or"
) )
const (
RuleSetTypeLocal = "local"
RuleSetTypeRemote = "remote"
RuleSetVersion1 = 1
RuleSetFormatSource = "source"
RuleSetFormatBinary = "binary"
)

View File

@ -4,6 +4,53 @@ icon: material/alert-decagram
# ChangeLog # ChangeLog
#### 1.8.0-alpha.2
* 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 #### 1.7.0
* Fixes and improvements * Fixes and improvements
@ -142,11 +189,13 @@ Only supported in graphical clients on Android and iOS.
**1**: **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** **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 #### 1.7.0-alpha.3
@ -213,8 +262,8 @@ When `auto_route` is enabled and `strict_route` is disabled, the device can now
**2**: **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 #### 1.6.0-rc.4
@ -227,7 +276,8 @@ Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008
**1**: **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 #### 1.6.0-beta.4

View File

@ -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 ### Structure
```json ```json
@ -46,6 +57,7 @@
"10.0.0.0/24", "10.0.0.0/24",
"192.168.0.1" "192.168.0.1"
], ],
"source_ip_is_private": false,
"source_port": [ "source_port": [
12345 12345
], ],
@ -85,6 +97,10 @@
"wifi_bssid": [ "wifi_bssid": [
"00:00:00:00:00:00" "00:00:00:00:00:00"
], ],
"rule_set": [
"geoip-cn",
"geosite-cn"
],
"invert": false, "invert": false,
"outbound": [ "outbound": [
"direct" "direct"
@ -166,15 +182,29 @@ Match domain using regular expression.
#### geosite #### 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-set).
Match geosite. Match geosite.
#### source_geoip #### 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-set).
Match source geoip. Match source geoip.
#### source_ip_cidr #### 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 #### source_port
@ -250,6 +280,12 @@ Match WiFi SSID.
Match WiFi BSSID. Match WiFi BSSID.
#### rule_set
!!! question "Since sing-box 1.8.0"
Match [Rule Set](/configuration/route/#rule_set).
#### invert #### invert
Invert match result. Invert match result.
@ -286,4 +322,4 @@ Rewrite TTL in DNS responses.
#### rules #### rules
Included default rules. Included rules.

View File

@ -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 ```json
@ -45,6 +56,7 @@
"source_ip_cidr": [ "source_ip_cidr": [
"10.0.0.0/24" "10.0.0.0/24"
], ],
"source_ip_is_private": false,
"source_port": [ "source_port": [
12345 12345
], ],
@ -84,6 +96,10 @@
"wifi_bssid": [ "wifi_bssid": [
"00:00:00:00:00:00" "00:00:00:00:00:00"
], ],
"rule_set": [
"geoip-cn",
"geosite-cn"
],
"invert": false, "invert": false,
"outbound": [ "outbound": [
"direct" "direct"
@ -163,16 +179,30 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
#### geosite #### geosite
匹配 GeoSite。 !!! failure "已在 sing-box 1.8.0 废弃"
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。
匹配 Geosite。
#### source_geoip #### source_geoip
!!! failure "已在 sing-box 1.8.0 废弃"
GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。
匹配源 GeoIP。 匹配源 GeoIP。
#### source_ip_cidr #### source_ip_cidr
匹配源 IP CIDR。 匹配源 IP CIDR。
#### source_ip_is_private
!!! question "自 sing-box 1.8.0 起"
匹配非公开源 IP。
#### source_port #### source_port
匹配源端口。 匹配源端口。
@ -245,6 +275,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
匹配 WiFi BSSID。 匹配 WiFi BSSID。
#### rule_set
!!! question "自 sing-box 1.8.0 起"
匹配[规则集](/zh/configuration/route/#rule_set)。
#### invert #### invert
反选匹配结果。 反选匹配结果。
@ -281,4 +317,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
#### rules #### rules
包括的默认规则。 包括的规则。

View File

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

View File

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

View File

@ -1,139 +1,30 @@
---
icon: material/alert-decagram
---
# Experimental # Experimental
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [cache_file](#cache_file)
:material-alert-decagram: [clash_api](#clash_api)
### Structure ### Structure
```json ```json
{ {
"experimental": { "experimental": {
"clash_api": { "cache_file": {},
"external_controller": "127.0.0.1:9090", "clash_api": {},
"external_ui": "", "v2ray_api": {}
"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 "" ### Fields
Traffic statistics and connection management can degrade performance. | Key | Format |
|--------------|----------------------------|
### Clash API Fields | `cache_file` | [Cache File](./cache-file) |
| `clash_api` | [Clash API](./clash-api) |
!!! quote "" | `v2ray_api` | [V2Ray API](./v2ray-api) |
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
User list to count traffic.

View File

@ -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
统计流量的用户列表。

View File

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

View File

@ -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-set).
### Structure ### Structure
```json ```json

View File

@ -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 资源的出站的标签。
如果为空,将使用默认出站。

View File

@ -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-set).
### Structure ### Structure
```json ```json

View File

@ -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 资源的出站的标签。
如果为空,将使用默认出站。

View File

@ -1,5 +1,15 @@
---
icon: material/alert-decagram
---
# Route # 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 ### Structure
```json ```json
@ -8,6 +18,7 @@
"geoip": {}, "geoip": {},
"geosite": {}, "geosite": {},
"rules": [], "rules": [],
"rule_set": [],
"final": "", "final": "",
"auto_detect_interface": false, "auto_detect_interface": false,
"override_android_vpn": false, "override_android_vpn": false,
@ -20,10 +31,19 @@
### Fields ### Fields
| Key | Format | | Key | Format |
|------------|------------------------------------| |-----------|----------------------|
| `geoip` | [GeoIP](./geoip) | | `geoip` | [GeoIP](./geoip) |
| `geosite` | [Geosite](./geosite) | | `geosite` | [Geosite](./geosite) |
| `rules` | List of [Route Rule](./rule) |
#### rules
List of [Route Rule](./rule)
#### rule_set
!!! question "Since sing-box 1.8.0"
List of [Rule Set](/configuration/rule-set)
#### final #### final

View File

@ -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 ```json
@ -7,8 +17,8 @@
"route": { "route": {
"geoip": {}, "geoip": {},
"geosite": {}, "geosite": {},
"ip_rules": [],
"rules": [], "rules": [],
"rule_set": [],
"final": "", "final": "",
"auto_detect_interface": false, "auto_detect_interface": false,
"override_android_vpn": false, "override_android_vpn": false,
@ -21,10 +31,20 @@
### 字段 ### 字段
| 键 | 格式 | | 键 | 格式 |
|------------|-------------------------| |------------|-----------------------------------|
| `geoip` | [GeoIP](./geoip) | | `geoip` | [GeoIP](./geoip) |
| `geosite` | [GeoSite](./geosite) | | `geosite` | [Geosite](./geosite) |
| `rules` | 一组 [路由规则](./rule) |
#### rule
一组 [路由规则](./rule)。
#### rule_set
!!! question "自 sing-box 1.8.0 起"
一组 [规则集](/configuration/rule-set)。
#### final #### final

View File

@ -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 ### Structure
```json ```json
@ -46,10 +60,12 @@
"10.0.0.0/24", "10.0.0.0/24",
"192.168.0.1" "192.168.0.1"
], ],
"source_ip_is_private": false,
"ip_cidr": [ "ip_cidr": [
"10.0.0.0/24", "10.0.0.0/24",
"192.168.0.1" "192.168.0.1"
], ],
"ip_is_private": false,
"source_port": [ "source_port": [
12345 12345
], ],
@ -89,6 +105,10 @@
"wifi_bssid": [ "wifi_bssid": [
"00:00:00:00:00:00" "00:00:00:00:00:00"
], ],
"rule_set": [
"geoip-cn",
"geosite-cn"
],
"invert": false, "invert": false,
"outbound": "direct" "outbound": "direct"
}, },
@ -160,23 +180,47 @@ Match domain using regular expression.
#### geosite #### 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-set).
Match geosite. Match geosite.
#### source_geoip #### 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-set).
Match source geoip. Match source geoip.
#### 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-set).
Match geoip. Match geoip.
#### source_ip_cidr #### 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 #### 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 #### source_port
@ -250,6 +294,18 @@ Match WiFi SSID.
Match WiFi BSSID. 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
Invert match result. Invert match result.
@ -276,4 +332,4 @@ Tag of the target outbound.
==Required== ==Required==
Included default rules. Included rules.

View File

@ -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 ```json
@ -45,9 +59,11 @@
"source_ip_cidr": [ "source_ip_cidr": [
"10.0.0.0/24" "10.0.0.0/24"
], ],
"source_ip_is_private": false,
"ip_cidr": [ "ip_cidr": [
"10.0.0.0/24" "10.0.0.0/24"
], ],
"ip_is_private": false,
"source_port": [ "source_port": [
12345 12345
], ],
@ -87,6 +103,10 @@
"wifi_bssid": [ "wifi_bssid": [
"00:00:00:00:00:00" "00:00:00:00:00:00"
], ],
"rule_set": [
"geoip-cn",
"geosite-cn"
],
"invert": false, "invert": false,
"outbound": "direct" "outbound": "direct"
}, },
@ -158,24 +178,48 @@
#### geosite #### geosite
匹配 GeoSite。 !!! failure "已在 sing-box 1.8.0 废弃"
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。
匹配 Geosite。
#### source_geoip #### source_geoip
!!! failure "已在 sing-box 1.8.0 废弃"
GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。
匹配源 GeoIP。 匹配源 GeoIP。
#### geoip #### geoip
!!! failure "已在 sing-box 1.8.0 废弃"
GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。
匹配 GeoIP。 匹配 GeoIP。
#### source_ip_cidr #### source_ip_cidr
匹配源 IP CIDR。 匹配源 IP CIDR。
#### source_ip_is_private
!!! question "自 sing-box 1.8.0 起"
匹配非公开源 IP。
#### ip_cidr #### ip_cidr
匹配 IP CIDR。 匹配 IP CIDR。
#### ip_is_private
!!! question "自 sing-box 1.8.0 起"
匹配非公开 IP。
#### source_port #### source_port
匹配源端口。 匹配源端口。
@ -248,6 +292,18 @@
匹配 WiFi BSSID。 匹配 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 #### invert
反选匹配结果。 反选匹配结果。
@ -274,4 +330,4 @@
==必填== ==必填==
包括的默认规则。 包括的规则。

View File

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

View File

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

View File

@ -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 <file-name>.srs] <file-name>.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).

View File

@ -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" === ":material-router-network: Route rules"
```json ```json
@ -423,3 +500,110 @@ 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"
},
{
"geoip": "private",
"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"
}
]
}
}
```

195
docs/migration.md Normal file
View File

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

View File

@ -12,6 +12,7 @@ import (
"github.com/sagernet/bbolt" "github.com/sagernet/bbolt"
bboltErrors "github.com/sagernet/bbolt/errors" bboltErrors "github.com/sagernet/bbolt/errors"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service/filemanager" "github.com/sagernet/sing/service/filemanager"
@ -21,21 +22,27 @@ var (
bucketSelected = []byte("selected") bucketSelected = []byte("selected")
bucketExpand = []byte("group_expand") bucketExpand = []byte("group_expand")
bucketMode = []byte("clash_mode") bucketMode = []byte("clash_mode")
bucketRuleSet = []byte("rule_set")
bucketNameList = []string{ bucketNameList = []string{
string(bucketSelected), string(bucketSelected),
string(bucketExpand), string(bucketExpand),
string(bucketMode), string(bucketMode),
string(bucketRuleSet),
} }
cacheIDDefault = []byte("default") cacheIDDefault = []byte("default")
) )
var _ adapter.ClashCacheFile = (*CacheFile)(nil) var _ adapter.CacheFile = (*CacheFile)(nil)
type CacheFile struct { type CacheFile struct {
DB *bbolt.DB ctx context.Context
path string
cacheID []byte cacheID []byte
storeFakeIP bool
DB *bbolt.DB
saveAccess sync.RWMutex saveAccess sync.RWMutex
saveDomain map[netip.Addr]string saveDomain map[netip.Addr]string
saveAddress4 map[string]netip.Addr saveAddress4 map[string]netip.Addr
@ -43,7 +50,29 @@ type CacheFile struct {
saveMetadataTimer *time.Timer saveMetadataTimer *time.Timer
} }
func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) { func NewCacheFile(ctx context.Context, options option.CacheFileOptions) *CacheFile {
var path string
if options.Path != "" {
path = options.Path
} else {
path = "cache.db"
}
var cacheIDBytes []byte
if options.CacheID != "" {
cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...)
}
return &CacheFile{
ctx: ctx,
path: filemanager.BasePath(ctx, path),
cacheID: cacheIDBytes,
storeFakeIP: options.StoreFakeIP,
saveDomain: make(map[netip.Addr]string),
saveAddress4: make(map[string]netip.Addr),
saveAddress6: make(map[string]netip.Addr),
}
}
func (c *CacheFile) start() error {
const fileMode = 0o666 const fileMode = 0o666
options := bbolt.Options{Timeout: time.Second} options := bbolt.Options{Timeout: time.Second}
var ( var (
@ -51,7 +80,7 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error)
err error err error
) )
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
db, err = bbolt.Open(path, fileMode, &options) db, err = bbolt.Open(c.path, fileMode, &options)
if err == nil { if err == nil {
break break
} }
@ -59,23 +88,20 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error)
continue continue
} }
if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) {
rmErr := os.Remove(path) rmErr := os.Remove(c.path)
if rmErr != nil { if rmErr != nil {
return nil, err return err
} }
} }
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
if err != nil { if err != nil {
return nil, err return err
} }
err = filemanager.Chown(ctx, path) err = filemanager.Chown(c.ctx, c.path)
if err != nil { if err != nil {
return nil, E.Cause(err, "platform chown") db.Close()
} return E.Cause(err, "platform chown")
var cacheIDBytes []byte
if cacheID != "" {
cacheIDBytes = append([]byte{0}, []byte(cacheID)...)
} }
err = db.Batch(func(tx *bbolt.Tx) error { err = db.Batch(func(tx *bbolt.Tx) error {
return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { return tx.ForEach(func(name []byte, b *bbolt.Bucket) error {
@ -97,15 +123,30 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error)
}) })
}) })
if err != nil { if err != nil {
return nil, err db.Close()
return err
} }
return &CacheFile{ c.DB = db
DB: db, return nil
cacheID: cacheIDBytes, }
saveDomain: make(map[netip.Addr]string),
saveAddress4: make(map[string]netip.Addr), func (c *CacheFile) PreStart() error {
saveAddress6: make(map[string]netip.Addr), return c.start()
}, nil }
func (c *CacheFile) Start() error {
return nil
}
func (c *CacheFile) Close() error {
if c.DB == nil {
return nil
}
return c.DB.Close()
}
func (c *CacheFile) StoreFakeIP() bool {
return c.storeFakeIP
} }
func (c *CacheFile) LoadMode() string { func (c *CacheFile) LoadMode() string {
@ -219,6 +260,35 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
}) })
} }
func (c *CacheFile) Close() error { func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet {
return c.DB.Close() 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)
})
} }

View File

@ -25,7 +25,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
err := c.DB.Batch(func(tx *bbolt.Tx) error { err := c.DB.Batch(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP) bucket := tx.Bucket(bucketFakeIP)
if bucket == nil { if bucket == nil {
return nil return os.ErrNotExist
} }
metadataBinary := bucket.Get(keyMetadata) metadataBinary := bucket.Get(keyMetadata)
if len(metadataBinary) == 0 { if len(metadataBinary) == 0 {

View File

@ -5,6 +5,7 @@ import (
"os" "os"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "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 { func CalculateClashModeList(options option.Options) []string {
var clashMode []string var clashMode []string
for _, dnsRule := range common.PtrValueOrDefault(options.DNS).Rules { clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...)
if dnsRule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, dnsRule.DefaultOptions.ClashMode) { clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...)
clashMode = append(clashMode, dnsRule.DefaultOptions.ClashMode) clashMode = common.FilterNotDefault(common.Uniq(clashMode))
} return clashMode
for _, defaultRule := range dnsRule.LogicalOptions.Rules { }
if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) {
clashMode = append(clashMode, defaultRule.ClashMode) func extraClashModeFromRule(rules []option.Rule) []string {
} var clashMode []string
} for _, rule := range rules {
} switch rule.Type {
for _, rule := range common.PtrValueOrDefault(options.Route).Rules { case C.RuleTypeDefault:
if rule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, rule.DefaultOptions.ClashMode) { if rule.DefaultOptions.ClashMode != "" {
clashMode = append(clashMode, rule.DefaultOptions.ClashMode) clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
} }
for _, defaultRule := range rule.LogicalOptions.Rules { case C.RuleTypeLogical:
if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...)
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 return clashMode

View File

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

View File

@ -100,8 +100,10 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite
allProxies = append(allProxies, detour.Tag()) allProxies = append(allProxies, detour.Tag())
} }
defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag() var defaultTag string
if defaultTag == "" { if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
defaultTag = defaultOutbound.Tag()
} else {
defaultTag = allProxies[0] defaultTag = allProxies[0]
} }

View File

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

View File

@ -51,7 +51,11 @@ func (s *Server) downloadExternalUI() error {
} }
detour = outbound detour = outbound
} else { } else {
detour = s.router.DefaultOutbound(N.NetworkTCP) outbound, err := s.router.DefaultOutbound(N.NetworkTCP)
if err != nil {
return err
}
detour = outbound
} }
httpClient := &http.Client{ httpClient := &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{

View File

@ -94,7 +94,9 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
var chain []string var chain []string
var next string var next string
if rule == nil { if rule == nil {
next = router.DefaultOutbound(N.NetworkTCP).Tag() if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
next = defaultOutbound.Tag()
}
} else { } else {
next = rule.Outbound() next = rule.Outbound()
} }
@ -181,7 +183,9 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
var chain []string var chain []string
var next string var next string
if rule == nil { if rule == nil {
next = router.DefaultOutbound(N.NetworkUDP).Tag() if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
next = defaultOutbound.Tag()
}
} else { } else {
next = rule.Outbound() next = rule.Outbound()
} }

View File

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

View File

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

2
go.mod
View File

@ -26,7 +26,7 @@ require (
github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930
github.com/sagernet/quic-go v0.40.0 github.com/sagernet/quic-go v0.40.0
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc github.com/sagernet/sing v0.2.18-0.20231130082037-cac27afa2a18
github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-dns v0.1.11
github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07
github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203 github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203

4
go.sum
View File

@ -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/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc h1:vESVuxHgbd2EzHxd+TYTpNACIEGBOhp5n3KG7bgbcws= github.com/sagernet/sing v0.2.18-0.20231130082037-cac27afa2a18 h1:R9A7AV+YKh/uVQkfjFZ9xJ7vH2hxYHoOc4FnIReONY8=
github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing v0.2.18-0.20231130082037-cac27afa2a18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= 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-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE=
github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM=

View File

@ -32,12 +32,16 @@ theme:
- content.code.copy - content.code.copy
- content.code.select - content.code.select
- content.code.annotate - content.code.annotate
icon:
admonition:
question: material/new-box
nav: nav:
- Home: - Home:
- index.md - index.md
- Change Log: changelog.md
- Migration: migration.md
- Deprecated: deprecated.md - Deprecated: deprecated.md
- Support: support.md - Support: support.md
- Change Log: changelog.md
- Installation: - Installation:
- Package Manager: installation/package-manager.md - Package Manager: installation/package-manager.md
- Docker: installation/docker.md - Docker: installation/docker.md
@ -56,7 +60,7 @@ nav:
- Proxy: - Proxy:
- Server: manual/proxy/server.md - Server: manual/proxy/server.md
- Client: manual/proxy/client.md - Client: manual/proxy/client.md
# - TUN: manual/proxy/tun.md # - TUN: manual/proxy/tun.md
- Proxy Protocol: - Proxy Protocol:
- Shadowsocks: manual/proxy-protocol/shadowsocks.md - Shadowsocks: manual/proxy-protocol/shadowsocks.md
- Trojan: manual/proxy-protocol/trojan.md - Trojan: manual/proxy-protocol/trojan.md
@ -79,8 +83,15 @@ nav:
- Geosite: configuration/route/geosite.md - Geosite: configuration/route/geosite.md
- Route Rule: configuration/route/rule.md - Route Rule: configuration/route/rule.md
- Protocol Sniff: configuration/route/sniff.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: - Experimental:
- configuration/experimental/index.md - 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: - Shared:
- Listen Fields: configuration/shared/listen.md - Listen Fields: configuration/shared/listen.md
- Dial Fields: configuration/shared/dial.md - Dial Fields: configuration/shared/dial.md
@ -180,9 +191,10 @@ plugins:
name: 简体中文 name: 简体中文
nav_translations: nav_translations:
Home: 开始 Home: 开始
Change Log: 更新日志
Migration: 迁移指南
Deprecated: 废弃功能列表 Deprecated: 废弃功能列表
Support: 支持 Support: 支持
Change Log: 更新日志
Installation: 安装 Installation: 安装
Package Manager: 包管理器 Package Manager: 包管理器
@ -203,6 +215,10 @@ plugins:
Route Rule: 路由规则 Route Rule: 路由规则
Protocol Sniff: 协议探测 Protocol Sniff: 协议探测
Rule Set: 规则集
Source Format: 源文件格式
Headless Rule: 无头规则
Experimental: 实验性 Experimental: 实验性
Shared: 通用 Shared: 通用
@ -215,10 +231,6 @@ plugins:
Inbound: 入站 Inbound: 入站
Outbound: 出站 Outbound: 出站
FAQ: 常见问题 Manual: 手册
Known Issues: 已知问题
Examples: 示例
Linux Server Installation: Linux 服务器安装
DNS Hijack: DNS 劫持
reconfigure_material: true reconfigure_material: true
reconfigure_search: true reconfigure_search: true

View File

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

View File

@ -1,7 +1,48 @@
package option package option
type ExperimentalOptions struct { type ExperimentalOptions struct {
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
Debug *DebugOptions `json:"debug,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
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"`
}
type V2RayAPIOptions struct {
Listen string `json:"listen,omitempty"`
Stats *V2RayStatsServiceOptions `json:"stats,omitempty"`
}
type V2RayStatsServiceOptions struct {
Enabled bool `json:"enabled,omitempty"`
Inbounds []string `json:"inbounds,omitempty"`
Outbounds []string `json:"outbounds,omitempty"`
Users []string `json:"users,omitempty"`
}

15
option/group.go Normal file
View File

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

View File

@ -4,6 +4,7 @@ type RouteOptions struct {
GeoIP *GeoIPOptions `json:"geoip,omitempty"` GeoIP *GeoIPOptions `json:"geoip,omitempty"`
Geosite *GeositeOptions `json:"geosite,omitempty"` Geosite *GeositeOptions `json:"geosite,omitempty"`
Rules []Rule `json:"rules,omitempty"` Rules []Rule `json:"rules,omitempty"`
RuleSet []RuleSet `json:"rule_set,omitempty"`
Final string `json:"final,omitempty"` Final string `json:"final,omitempty"`
FindProcess bool `json:"find_process,omitempty"` FindProcess bool `json:"find_process,omitempty"`
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`

View File

@ -53,6 +53,17 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error {
return nil 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 { type DefaultRule struct {
Inbound Listable[string] `json:"inbound,omitempty"` Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"` IPVersion int `json:"ip_version,omitempty"`
@ -67,7 +78,9 @@ type DefaultRule struct {
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
GeoIP Listable[string] `json:"geoip,omitempty"` GeoIP Listable[string] `json:"geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"` IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"` SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"` SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"` Port Listable[uint16] `json:"port,omitempty"`
@ -80,6 +93,8 @@ type DefaultRule struct {
ClashMode string `json:"clash_mode,omitempty"` ClashMode string `json:"clash_mode,omitempty"`
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID Listable[string] `json:"wifi_bssid,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"` Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"` Outbound string `json:"outbound,omitempty"`
} }
@ -93,11 +108,11 @@ func (r DefaultRule) IsValid() bool {
type LogicalRule struct { type LogicalRule struct {
Mode string `json:"mode"` Mode string `json:"mode"`
Rules []DefaultRule `json:"rules,omitempty"` Rules []Rule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"` Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"` Outbound string `json:"outbound,omitempty"`
} }
func (r LogicalRule) IsValid() bool { 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)
} }

View File

@ -53,6 +53,17 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
return nil 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 { type DefaultDNSRule struct {
Inbound Listable[string] `json:"inbound,omitempty"` Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"` IPVersion int `json:"ip_version,omitempty"`
@ -67,6 +78,7 @@ type DefaultDNSRule struct {
Geosite Listable[string] `json:"geosite,omitempty"` Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"` SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"` SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"` Port Listable[uint16] `json:"port,omitempty"`
@ -80,6 +92,7 @@ type DefaultDNSRule struct {
ClashMode string `json:"clash_mode,omitempty"` ClashMode string `json:"clash_mode,omitempty"`
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
RuleSet Listable[string] `json:"rule_set,omitempty"`
Invert bool `json:"invert,omitempty"` Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"` Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"` DisableCache bool `json:"disable_cache,omitempty"`
@ -97,7 +110,7 @@ func (r DefaultDNSRule) IsValid() bool {
type LogicalDNSRule struct { type LogicalDNSRule struct {
Mode string `json:"mode"` Mode string `json:"mode"`
Rules []DefaultDNSRule `json:"rules,omitempty"` Rules []DNSRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"` Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"` Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"` DisableCache bool `json:"disable_cache,omitempty"`
@ -105,5 +118,5 @@ type LogicalDNSRule struct {
} }
func (r LogicalDNSRule) IsValid() bool { 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)
} }

230
option/rule_set.go Normal file
View File

@ -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 rule_set.[].tag")
}
switch r.Format {
case "":
return E.New("missing rule_set.[].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 rule_set.[].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"`
}

View File

@ -174,6 +174,14 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error {
type DNSQueryType uint16 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) { func (t DNSQueryType) MarshalJSON() ([]byte, error) {
typeName, loaded := mDNS.TypeToString[uint16(t)] typeName, loaded := mDNS.TypeToString[uint16(t)]
if loaded { if loaded {

View File

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

View File

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

View File

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

View File

@ -39,6 +39,7 @@ import (
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
serviceNTP "github.com/sagernet/sing/common/ntp" serviceNTP "github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/common/task"
"github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/common/uot"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause" "github.com/sagernet/sing/service/pause"
@ -67,6 +68,8 @@ type Router struct {
dnsClient *dns.Client dnsClient *dns.Client
defaultDomainStrategy dns.DomainStrategy defaultDomainStrategy dns.DomainStrategy
dnsRules []adapter.DNSRule dnsRules []adapter.DNSRule
ruleSets []adapter.RuleSet
ruleSetMap map[string]adapter.RuleSet
defaultTransport dns.Transport defaultTransport dns.Transport
transports []dns.Transport transports []dns.Transport
transportMap map[string]dns.Transport transportMap map[string]dns.Transport
@ -106,6 +109,7 @@ func NewRouter(
outboundByTag: make(map[string]adapter.Outbound), outboundByTag: make(map[string]adapter.Outbound),
rules: make([]adapter.Rule, 0, len(options.Rules)), rules: make([]adapter.Rule, 0, len(options.Rules)),
dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)), dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)),
ruleSetMap: make(map[string]adapter.RuleSet),
needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule), needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule),
needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule), needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule),
geoIPOptions: common.PtrValueOrDefault(options.GeoIP), geoIPOptions: common.PtrValueOrDefault(options.GeoIP),
@ -127,19 +131,27 @@ func NewRouter(
Logger: router.dnsLogger, Logger: router.dnsLogger,
}) })
for i, ruleOptions := range options.Rules { for i, ruleOptions := range options.Rules {
routeRule, err := NewRule(router, router.logger, ruleOptions) routeRule, err := NewRule(router, router.logger, ruleOptions, true)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse rule[", i, "]") return nil, E.Cause(err, "parse rule[", i, "]")
} }
router.rules = append(router.rules, routeRule) router.rules = append(router.rules, routeRule)
} }
for i, dnsRuleOptions := range dnsOptions.Rules { for i, dnsRuleOptions := range dnsOptions.Rules {
dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions) dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions, true)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse dns rule[", i, "]") return nil, E.Cause(err, "parse dns rule[", i, "]")
} }
router.dnsRules = append(router.dnsRules, dnsRule) router.dnsRules = append(router.dnsRules, dnsRule)
} }
for i, ruleSetOptions := range options.RuleSet {
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)) transports := make([]dns.Transport, len(dnsOptions.Servers))
dummyTransportMap := make(map[string]dns.Transport) dummyTransportMap := make(map[string]dns.Transport)
@ -261,7 +273,7 @@ func NewRouter(
if fakeIPOptions.Inet6Range != nil { if fakeIPOptions.Inet6Range != nil {
inet6Range = *fakeIPOptions.Inet6Range inet6Range = *fakeIPOptions.Inet6Range
} }
router.fakeIPStore = fakeip.NewStore(router, router.logger, inet4Range, inet6Range) router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range)
} }
usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor() usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor()
@ -479,6 +491,24 @@ func (r *Router) Start() error {
if r.needWIFIState { if r.needWIFIState {
r.updateWIFIState() r.updateWIFIState()
} }
ruleSetStartContext := NewRuleSetStartContext()
var ruleSetStartGroup task.Group
for i, ruleSet := range r.ruleSets {
ruleSetStartGroup.Append0(func(ctx context.Context) error {
err := ruleSet.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()
for i, rule := range r.rules { for i, rule := range r.rules {
err := rule.Start() err := rule.Start()
if err != nil { if err != nil {
@ -576,11 +606,17 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
return outbound, loaded return outbound, loaded
} }
func (r *Router) DefaultOutbound(network string) adapter.Outbound { func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) {
if network == N.NetworkTCP { 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 { } else {
return r.defaultOutboundForPacketConnection if r.defaultOutboundForPacketConnection == nil {
return nil, E.New("missing default outbound for UDP connections")
}
return r.defaultOutboundForPacketConnection, nil
} }
} }
@ -588,6 +624,11 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore {
return r.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 { func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
if metadata.InboundDetour != "" { if metadata.InboundDetour != "" {
if metadata.LastInbound == metadata.InboundDetour { if metadata.LastInbound == metadata.InboundDetour {

View File

@ -252,13 +252,11 @@ func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool
return true return true
} }
case C.RuleTypeLogical: case C.RuleTypeLogical:
for _, subRule := range rule.LogicalOptions.Rules { if hasRule(rule.LogicalOptions.Rules, cond) {
if cond(subRule) {
return true return true
} }
} }
} }
}
return false return false
} }
@ -270,13 +268,11 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo
return true return true
} }
case C.RuleTypeLogical: case C.RuleTypeLogical:
for _, subRule := range rule.LogicalOptions.Rules { if hasDNSRule(rule.LogicalOptions.Rules, cond) {
if cond(subRule) {
return true return true
} }
} }
} }
}
return false return false
} }

View File

@ -1,6 +1,7 @@
package route package route
import ( import (
"io"
"strings" "strings"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@ -135,7 +136,7 @@ func (r *abstractDefaultRule) String() string {
} }
type abstractLogicalRule struct { type abstractLogicalRule struct {
rules []adapter.Rule rules []adapter.HeadlessRule
mode string mode string
invert bool invert bool
outbound string outbound string
@ -146,7 +147,10 @@ func (r *abstractLogicalRule) Type() string {
} }
func (r *abstractLogicalRule) UpdateGeosite() error { 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() err := rule.UpdateGeosite()
if err != nil { if err != nil {
return err return err
@ -156,7 +160,10 @@ func (r *abstractLogicalRule) UpdateGeosite() error {
} }
func (r *abstractLogicalRule) Start() 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() err := rule.Start()
if err != nil { if err != nil {
return err return err
@ -166,7 +173,10 @@ func (r *abstractLogicalRule) Start() error {
} }
func (r *abstractLogicalRule) Close() 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() err := rule.Close()
if err != nil { if err != nil {
return err return err
@ -177,11 +187,11 @@ func (r *abstractLogicalRule) Close() error {
func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
if r.mode == C.LogicalTypeAnd { 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 {
return it.Match(metadata) return it.Match(metadata)
}) != r.invert }) != r.invert
} else { } else {
return common.Any(r.rules, func(it adapter.Rule) bool { return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
return it.Match(metadata) return it.Match(metadata)
}) != r.invert }) != r.invert
} }

View File

@ -8,13 +8,13 @@ import (
E "github.com/sagernet/sing/common/exceptions" 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 { switch options.Type {
case "", C.RuleTypeDefault: case "", C.RuleTypeDefault:
if !options.DefaultOptions.IsValid() { if !options.DefaultOptions.IsValid() {
return nil, E.New("missing conditions") return nil, E.New("missing conditions")
} }
if options.DefaultOptions.Outbound == "" { if options.DefaultOptions.Outbound == "" && checkOutbound {
return nil, E.New("missing outbound field") return nil, E.New("missing outbound field")
} }
return NewDefaultRule(router, logger, options.DefaultOptions) 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() { if !options.LogicalOptions.IsValid() {
return nil, E.New("missing conditions") return nil, E.New("missing conditions")
} }
if options.LogicalOptions.Outbound == "" { if options.LogicalOptions.Outbound == "" && checkOutbound {
return nil, E.New("missing outbound field") return nil, E.New("missing outbound field")
} }
return NewLogicalRule(router, logger, options.LogicalOptions) return NewLogicalRule(router, logger, options.LogicalOptions)
@ -120,6 +120,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, 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 { if len(options.IPCIDR) > 0 {
item, err := NewIPCIDRItem(false, options.IPCIDR) item, err := NewIPCIDRItem(false, options.IPCIDR)
if err != nil { if err != nil {
@ -128,6 +133,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, 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 { if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort) item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item) rule.sourcePortItems = append(rule.sourcePortItems, item)
@ -194,6 +204,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
rule.items = append(rule.items, item) rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, 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 return rule, nil
} }
@ -206,7 +221,7 @@ type LogicalRule struct {
func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) {
r := &LogicalRule{ r := &LogicalRule{
abstractLogicalRule{ abstractLogicalRule{
rules: make([]adapter.Rule, len(options.Rules)), rules: make([]adapter.HeadlessRule, len(options.Rules)),
invert: options.Invert, invert: options.Invert,
outbound: options.Outbound, outbound: options.Outbound,
}, },
@ -220,7 +235,7 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt
return nil, E.New("unknown logical mode: ", options.Mode) return nil, E.New("unknown logical mode: ", options.Mode)
} }
for i, subRule := range options.Rules { for i, subRule := range options.Rules {
rule, err := NewDefaultRule(router, logger, subRule) rule, err := NewRule(router, logger, subRule, false)
if err != nil { if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]") return nil, E.Cause(err, "sub rule[", i, "]")
} }

View File

@ -8,13 +8,13 @@ import (
E "github.com/sagernet/sing/common/exceptions" 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 { switch options.Type {
case "", C.RuleTypeDefault: case "", C.RuleTypeDefault:
if !options.DefaultOptions.IsValid() { if !options.DefaultOptions.IsValid() {
return nil, E.New("missing conditions") return nil, E.New("missing conditions")
} }
if options.DefaultOptions.Server == "" { if options.DefaultOptions.Server == "" && checkServer {
return nil, E.New("missing server field") return nil, E.New("missing server field")
} }
return NewDefaultDNSRule(router, logger, options.DefaultOptions) return NewDefaultDNSRule(router, logger, options.DefaultOptions)
@ -22,7 +22,7 @@ func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.
if !options.LogicalOptions.IsValid() { if !options.LogicalOptions.IsValid() {
return nil, E.New("missing conditions") return nil, E.New("missing conditions")
} }
if options.LogicalOptions.Server == "" { if options.LogicalOptions.Server == "" && checkServer {
return nil, E.New("missing server field") return nil, E.New("missing server field")
} }
return NewLogicalDNSRule(router, logger, options.LogicalOptions) return NewLogicalDNSRule(router, logger, options.LogicalOptions)
@ -119,6 +119,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, 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 { if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort) item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item) rule.sourcePortItems = append(rule.sourcePortItems, item)
@ -190,6 +195,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
rule.items = append(rule.items, item) rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, 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 return rule, nil
} }
@ -212,7 +222,7 @@ type LogicalDNSRule struct {
func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
r := &LogicalDNSRule{ r := &LogicalDNSRule{
abstractLogicalRule: abstractLogicalRule{ abstractLogicalRule: abstractLogicalRule{
rules: make([]adapter.Rule, len(options.Rules)), rules: make([]adapter.HeadlessRule, len(options.Rules)),
invert: options.Invert, invert: options.Invert,
outbound: options.Server, outbound: options.Server,
}, },
@ -228,7 +238,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options
return nil, E.New("unknown logical mode: ", options.Mode) return nil, E.New("unknown logical mode: ", options.Mode)
} }
for i, subRule := range options.Rules { for i, subRule := range options.Rules {
rule, err := NewDefaultDNSRule(router, logger, subRule) rule, err := NewDNSRule(router, logger, subRule, false)
if err != nil { if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]") return nil, E.Cause(err, "sub rule[", i, "]")
} }

173
route/rule_headless.go Normal file
View File

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

View File

@ -31,7 +31,7 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) {
builder.Add(addr) builder.Add(addr)
continue continue
} }
return nil, E.Cause(err, "parse ip_cidr [", i, "]") return nil, E.Cause(err, "parse [", i, "]")
} }
var description string var description string
if isSource { if isSource {
@ -57,8 +57,23 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) {
}, nil }, nil
} }
func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem {
var description string
if isSource {
description = "source_ipcidr="
} else {
description = "ipcidr="
}
description += "<binary>"
return &IPCIDRItem{
ipSet: ipSet,
isSource: isSource,
description: description,
}
}
func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { 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) return r.ipSet.Contains(metadata.Source.Addr)
} else { } else {
if metadata.Destination.IsIP() { if metadata.Destination.IsIP() {

View File

@ -43,6 +43,13 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem {
} }
} }
func NewRawDomainItem(matcher *domain.Matcher) *DomainItem {
return &DomainItem{
matcher,
"domain/domain_suffix=<binary>",
}
}
func (r *DomainItem) Match(metadata *adapter.InboundContext) bool { func (r *DomainItem) Match(metadata *adapter.InboundContext) bool {
var domainHost string var domainHost string
if metadata.Domain != "" { if metadata.Domain != "" {

View File

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

View File

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

67
route/rule_set.go Normal file
View File

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

70
route/rule_set_local.go Normal file
View File

@ -0,0 +1,70 @@
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
}
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, "]")
}
}
return &LocalRuleSet{rules}, 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) Close() error {
return nil
}

222
route/rule_set_remote.go Normal file
View File

@ -0,0 +1,222 @@
package route
import (
"bytes"
"context"
"io"
"net"
"net/http"
"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"
)
var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
type RemoteRuleSet struct {
ctx context.Context
cancel context.CancelFunc
router adapter.Router
logger logger.ContextLogger
options option.RuleSet
updateInterval time.Duration
dialer N.Dialer
rules []adapter.HeadlessRule
lastUpdated time.Time
lastEtag string
updateTicker *time.Ticker
}
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 = 12 * time.Hour
}
return &RemoteRuleSet{
ctx: ctx,
cancel: cancel,
router: router,
logger: logger,
options: options,
updateInterval: updateInterval,
}
}
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() || time.Since(s.lastUpdated) > s.updateInterval {
err := s.fetchOnce(ctx, startContext)
if err != nil {
return E.Cause(err, "fetch rule-set ", s.options.Tag)
}
}
s.updateTicker = time.NewTicker(s.updateInterval)
go s.loopUpdate()
return nil
}
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.rules = rules
return nil
}
func (s *RemoteRuleSet) loopUpdate() {
for {
select {
case <-s.ctx.Done():
return
case <-s.updateTicker.C:
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.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
}

View File

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