Add rule-set

This commit is contained in:
世界 2023-12-01 13:24:12 +08:00
parent 7df151e820
commit 4b43acfec0
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
51 changed files with 2975 additions and 265 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 {
@ -23,6 +28,7 @@ type CacheFile interface {
PreStarter PreStarter
StoreFakeIP() bool StoreFakeIP() bool
FakeIPStorage
LoadMode() string LoadMode() string
StoreMode(mode string) error StoreMode(mode string) error
@ -30,7 +36,65 @@ type CacheFile interface {
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

@ -46,11 +46,24 @@ type InboundContext struct {
SourceGeoIPCode string SourceGeoIPCode string
GeoIPCode string GeoIPCode string
ProcessInfo *process.Info ProcessInfo *process.Info
QueryType uint16
FakeIP bool FakeIP bool
// dns cache // rule cache
QueryType uint16 IPCIDRMatchSource bool
SourceAddressMatch bool
SourcePortMatch bool
DestinationAddressMatch bool
DestinationPortMatch bool
}
func (c *InboundContext) ResetRuleCache() {
c.IPCIDRMatchSource = false
c.SourceAddressMatch = false
c.SourcePortMatch = false
c.DestinationAddressMatch = false
c.DestinationPortMatch = false
} }
type inboundContextKey struct{} type inboundContextKey struct{}

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"
@ -19,7 +21,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
@ -28,6 +30,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)
@ -62,11 +66,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
} }
@ -77,6 +85,24 @@ type DNSRule interface {
RewriteTTL() *uint32 RewriteTTL() *uint32
} }
type RuleSet interface {
StartContext(ctx context.Context, startContext RuleSetStartContext) error
PostStart() error
Metadata() RuleSetMetadata
Close() error
HeadlessRule
}
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
}
type RuleSetStartContext interface {
HTTPClient(detour string, dialer N.Dialer) *http.Client
Close()
}
type InterfaceUpdateListener interface { type InterfaceUpdateListener interface {
InterfaceUpdated() InterfaceUpdated()
} }

4
box.go
View file

@ -308,6 +308,10 @@ func (s *Box) postStart() error {
} }
} }
s.logger.Trace("post-starting router") s.logger.Trace("post-starting router")
err := s.router.PostStart()
if err != nil {
return E.Cause(err, "post-start router")
}
return s.router.PostStart() return s.router.PostStart()
} }

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,84 @@
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
}
}
content, err := io.ReadAll(reader)
if err != nil {
return err
}
plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
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,83 @@
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
}
plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(plainRuleSet)
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 {

487
common/srs/binary.go Normal file
View file

@ -0,0 +1,487 @@
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.Type = C.RuleTypeDefault
rule.DefaultOptions, err = readDefaultRule(reader, recovery)
case 1:
rule.Type = C.RuleTypeLogical
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

@ -22,11 +22,13 @@ 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")
@ -257,3 +259,36 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
} }
}) })
} }
func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet {
var savedSet adapter.SavedRuleSet
err := c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketRuleSet)
if bucket == nil {
return os.ErrNotExist
}
setBinary := bucket.Get([]byte(tag))
if len(setBinary) == 0 {
return os.ErrInvalid
}
return savedSet.UnmarshalBinary(setBinary)
})
if err != nil {
return nil
}
return &savedSet
}
func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedRuleSet) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketRuleSet)
if err != nil {
return err
}
setBinary, err := set.MarshalBinary()
if err != nil {
return err
}
return bucket.Put([]byte(tag), setBinary)
})
}

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

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

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

@ -23,16 +23,16 @@ type ClashAPIOptions struct {
DefaultMode string `json:"default_mode,omitempty"` DefaultMode string `json:"default_mode,omitempty"`
ModeList []string `json:"-"` 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 // Deprecated: migrated to global cache file
StoreMode bool `json:"store_mode,omitempty"` StoreMode bool `json:"store_mode,omitempty"`
// Deprecated: migrated to global cache file // Deprecated: migrated to global cache file
StoreSelected bool `json:"store_selected,omitempty"` StoreSelected bool `json:"store_selected,omitempty"`
// Deprecated: migrated to global cache file // Deprecated: migrated to global cache file
StoreFakeIP bool `json:"store_fakeip,omitempty"` StoreFakeIP bool `json:"store_fakeip,omitempty"`
// Deprecated: migrated to global cache file
CacheFile string `json:"cache_file,omitempty"`
// Deprecated: migrated to global cache file
CacheID string `json:"cache_id,omitempty"`
} }
type V2RayAPIOptions struct { type V2RayAPIOptions struct {

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

@ -65,34 +65,36 @@ func (r Rule) IsValid() bool {
} }
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"`
Network Listable[string] `json:"network,omitempty"` Network Listable[string] `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"` AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"` Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"` Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"` DomainRegex Listable[string] `json:"domain_regex,omitempty"`
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"`
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"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"` IPCIDR Listable[string] `json:"ip_cidr,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"`
PortRange Listable[string] `json:"port_range,omitempty"` PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"` ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"` ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"` PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"` User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"` UserID Listable[int32] `json:"user_id,omitempty"`
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"`
Invert bool `json:"invert,omitempty"` RuleSet Listable[string] `json:"rule_set,omitempty"`
Outbound string `json:"outbound,omitempty"` RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
} }
func (r DefaultRule) IsValid() bool { func (r DefaultRule) IsValid() bool {

View file

@ -91,6 +91,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"`

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 tag")
}
switch r.Format {
case "":
return E.New("missing format")
case C.RuleSetFormatSource, C.RuleSetFormatBinary:
default:
return E.New("unknown rule set format: " + r.Format)
}
var v any
switch r.Type {
case C.RuleSetTypeLocal:
v = &r.LocalOptions
case C.RuleSetTypeRemote:
v = &r.RemoteOptions
case "":
return E.New("missing type")
default:
return E.New("unknown rule set type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v)
if err != nil {
return E.Cause(err, "rule set")
}
return nil
}
type LocalRuleSet struct {
Path string `json:"path,omitempty"`
}
type RemoteRuleSet struct {
URL string `json:"url"`
DownloadDetour string `json:"download_detour,omitempty"`
UpdateInterval Duration `json:"update_interval,omitempty"`
}
type _HeadlessRule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultHeadlessRule `json:"-"`
LogicalOptions LogicalHeadlessRule `json:"-"`
}
type HeadlessRule _HeadlessRule
func (r HeadlessRule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_HeadlessRule)(r), v)
}
func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_HeadlessRule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v)
if err != nil {
return E.Cause(err, "route rule-set rule")
}
return nil
}
func (r HeadlessRule) IsValid() bool {
switch r.Type {
case C.RuleTypeDefault, "":
return r.DefaultOptions.IsValid()
case C.RuleTypeLogical:
return r.LogicalOptions.IsValid()
default:
panic("unknown rule type: " + r.Type)
}
}
type DefaultHeadlessRule struct {
QueryType Listable[DNSQueryType] `json:"query_type,omitempty"`
Network Listable[string] `json:"network,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
Invert bool `json:"invert,omitempty"`
DomainMatcher *domain.Matcher `json:"-"`
SourceIPSet *netipx.IPSet `json:"-"`
IPSet *netipx.IPSet `json:"-"`
}
func (r DefaultHeadlessRule) IsValid() bool {
var defaultValue DefaultHeadlessRule
defaultValue.Invert = r.Invert
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalHeadlessRule struct {
Mode string `json:"mode"`
Rules []HeadlessRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
}
func (r LogicalHeadlessRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid)
}
type _PlainRuleSetCompat struct {
Version int `json:"version"`
Options PlainRuleSet `json:"-"`
}
type PlainRuleSetCompat _PlainRuleSetCompat
func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) {
var v any
switch r.Version {
case C.RuleSetVersion1:
v = r.Options
default:
return nil, E.New("unknown rule set version: ", r.Version)
}
return MarshallObjects((_PlainRuleSetCompat)(r), v)
}
func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r))
if err != nil {
return err
}
var v any
switch r.Version {
case C.RuleSetVersion1:
v = &r.Options
case 0:
return E.New("missing rule set version")
default:
return E.New("unknown rule set version: ", r.Version)
}
err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v)
if err != nil {
return E.Cause(err, "rule set")
}
return nil
}
func (r PlainRuleSetCompat) Upgrade() PlainRuleSet {
var result PlainRuleSet
switch r.Version {
case C.RuleSetVersion1:
result = r.Options
default:
panic("unknown rule set version: " + F.ToString(r.Version))
}
return result
}
type PlainRuleSet struct {
Rules []HeadlessRule `json:"rules,omitempty"`
}

226
option/time_unit.go Normal file
View file

@ -0,0 +1,226 @@
package option
import (
"errors"
"time"
)
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
const durationDay = 24 * time.Hour
var unitMap = map[string]uint64{
"ns": uint64(time.Nanosecond),
"us": uint64(time.Microsecond),
"µs": uint64(time.Microsecond), // U+00B5 = micro symbol
"μs": uint64(time.Microsecond), // U+03BC = Greek letter mu
"ms": uint64(time.Millisecond),
"s": uint64(time.Second),
"m": uint64(time.Minute),
"h": uint64(time.Hour),
"d": uint64(durationDay),
}
// ParseDuration parses a duration string.
// A duration string is a possibly signed sequence of
// decimal numbers, each with optional fraction and a unit suffix,
// such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
func ParseDuration(s string) (Duration, error) {
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
orig := s
var d uint64
neg := false
// Consume [-+]?
if s != "" {
c := s[0]
if c == '-' || c == '+' {
neg = c == '-'
s = s[1:]
}
}
// Special case: if all that is left is "0", this is zero.
if s == "0" {
return 0, nil
}
if s == "" {
return 0, errors.New("time: invalid duration " + quote(orig))
}
for s != "" {
var (
v, f uint64 // integers before, after decimal point
scale float64 = 1 // value = v + f/scale
)
var err error
// The next character must be [0-9.]
if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') {
return 0, errors.New("time: invalid duration " + quote(orig))
}
// Consume [0-9]*
pl := len(s)
v, s, err = leadingInt(s)
if err != nil {
return 0, errors.New("time: invalid duration " + quote(orig))
}
pre := pl != len(s) // whether we consumed anything before a period
// Consume (\.[0-9]*)?
post := false
if s != "" && s[0] == '.' {
s = s[1:]
pl := len(s)
f, scale, s = leadingFraction(s)
post = pl != len(s)
}
if !pre && !post {
// no digits (e.g. ".s" or "-.s")
return 0, errors.New("time: invalid duration " + quote(orig))
}
// Consume unit.
i := 0
for ; i < len(s); i++ {
c := s[i]
if c == '.' || '0' <= c && c <= '9' {
break
}
}
if i == 0 {
return 0, errors.New("time: missing unit in duration " + quote(orig))
}
u := s[:i]
s = s[i:]
unit, ok := unitMap[u]
if !ok {
return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig))
}
if v > 1<<63/unit {
// overflow
return 0, errors.New("time: invalid duration " + quote(orig))
}
v *= unit
if f > 0 {
// float64 is needed to be nanosecond accurate for fractions of hours.
// v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
v += uint64(float64(f) * (float64(unit) / scale))
if v > 1<<63 {
// overflow
return 0, errors.New("time: invalid duration " + quote(orig))
}
}
d += v
if d > 1<<63 {
return 0, errors.New("time: invalid duration " + quote(orig))
}
}
if neg {
return -Duration(d), nil
}
if d > 1<<63-1 {
return 0, errors.New("time: invalid duration " + quote(orig))
}
return Duration(d), nil
}
var errLeadingInt = errors.New("time: bad [0-9]*") // never printed
// leadingInt consumes the leading [0-9]* from s.
func leadingInt[bytes []byte | string](s bytes) (x uint64, rem bytes, err error) {
i := 0
for ; i < len(s); i++ {
c := s[i]
if c < '0' || c > '9' {
break
}
if x > 1<<63/10 {
// overflow
return 0, rem, errLeadingInt
}
x = x*10 + uint64(c) - '0'
if x > 1<<63 {
// overflow
return 0, rem, errLeadingInt
}
}
return x, s[i:], nil
}
// leadingFraction consumes the leading [0-9]* from s.
// It is used only for fractions, so does not return an error on overflow,
// it just stops accumulating precision.
func leadingFraction(s string) (x uint64, scale float64, rem string) {
i := 0
scale = 1
overflow := false
for ; i < len(s); i++ {
c := s[i]
if c < '0' || c > '9' {
break
}
if overflow {
continue
}
if x > (1<<63-1)/10 {
// It's possible for overflow to give a positive number, so take care.
overflow = true
continue
}
y := x*10 + uint64(c) - '0'
if y > 1<<63 {
overflow = true
continue
}
x = y
scale *= 10
}
return x, scale, s[i:]
}
// These are borrowed from unicode/utf8 and strconv and replicate behavior in
// that package, since we can't take a dependency on either.
const (
lowerhex = "0123456789abcdef"
runeSelf = 0x80
runeError = '\uFFFD'
)
func quote(s string) string {
buf := make([]byte, 1, len(s)+2) // slice will be at least len(s) + quotes
buf[0] = '"'
for i, c := range s {
if c >= runeSelf || c < ' ' {
// This means you are asking us to parse a time.Duration or
// time.Location with unprintable or non-ASCII characters in it.
// We don't expect to hit this case very often. We could try to
// reproduce strconv.Quote's behavior with full fidelity but
// given how rarely we expect to hit these edge cases, speed and
// conciseness are better.
var width int
if c == runeError {
width = 1
if i+2 < len(s) && s[i:i+3] == string(runeError) {
width = 3
}
} else {
width = len(string(c))
}
for j := 0; j < width; j++ {
buf = append(buf, `\x`...)
buf = append(buf, lowerhex[s[i+j]>>4])
buf = append(buf, lowerhex[s[i+j]&0xF])
}
} else {
if c == '"' || c == '\\' {
buf = append(buf, '\\')
}
buf = append(buf, string(c)...)
}
}
buf = append(buf, '"')
return string(buf)
}

View file

@ -164,7 +164,7 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error {
if err != nil { if err != nil {
return err return err
} }
duration, err := time.ParseDuration(value) duration, err := ParseDuration(value)
if err != nil { if err != nil {
return err return err
} }
@ -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

@ -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"
@ -64,9 +65,12 @@ type Router struct {
geoIPReader *geoip.Reader geoIPReader *geoip.Reader
geositeReader *geosite.Reader geositeReader *geosite.Reader
geositeCache map[string]adapter.Rule geositeCache map[string]adapter.Rule
needFindProcess bool
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
@ -107,11 +111,13 @@ 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),
geositeOptions: common.PtrValueOrDefault(options.Geosite), geositeOptions: common.PtrValueOrDefault(options.Geosite),
geositeCache: make(map[string]adapter.Rule), geositeCache: make(map[string]adapter.Rule),
needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess,
defaultDetour: options.Final, defaultDetour: options.Final,
defaultDomainStrategy: dns.DomainStrategy(dnsOptions.Strategy), defaultDomainStrategy: dns.DomainStrategy(dnsOptions.Strategy),
autoDetectInterface: options.AutoDetectInterface, autoDetectInterface: options.AutoDetectInterface,
@ -141,6 +147,17 @@ func NewRouter(
} }
router.dnsRules = append(router.dnsRules, dnsRule) router.dnsRules = append(router.dnsRules, dnsRule)
} }
for i, ruleSetOptions := range options.RuleSet {
if _, exists := router.ruleSetMap[ruleSetOptions.Tag]; exists {
return nil, E.New("duplicate rule-set tag: ", ruleSetOptions.Tag)
}
ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions)
if err != nil {
return nil, E.Cause(err, "parse rule-set[", i, "]")
}
router.ruleSets = append(router.ruleSets, ruleSet)
router.ruleSetMap[ruleSetOptions.Tag] = ruleSet
}
transports := make([]dns.Transport, len(dnsOptions.Servers)) transports := make([]dns.Transport, len(dnsOptions.Servers))
dummyTransportMap := make(map[string]dns.Transport) dummyTransportMap := make(map[string]dns.Transport)
@ -296,34 +313,6 @@ func NewRouter(
router.interfaceMonitor = interfaceMonitor router.interfaceMonitor = interfaceMonitor
} }
needFindProcess := hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess
needPackageManager := C.IsAndroid && platformInterface == nil && (needFindProcess || common.Any(inbounds, func(inbound option.Inbound) bool {
return len(inbound.TunOptions.IncludePackage) > 0 || len(inbound.TunOptions.ExcludePackage) > 0
}))
if needPackageManager {
packageManager, err := tun.NewPackageManager(router)
if err != nil {
return nil, E.Cause(err, "create package manager")
}
router.packageManager = packageManager
}
if needFindProcess {
if platformInterface != nil {
router.processSearcher = platformInterface
} else {
searcher, err := process.NewSearcher(process.Config{
Logger: logFactory.NewLogger("router/process"),
PackageManager: router.packageManager,
})
if err != nil {
if err != os.ErrInvalid {
router.logger.Warn(E.Cause(err, "create process searcher"))
}
} else {
router.processSearcher = searcher
}
}
}
if ntpOptions.Enabled { if ntpOptions.Enabled {
timeService, err := ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions) timeService, err := ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions)
if err != nil { if err != nil {
@ -332,11 +321,6 @@ func NewRouter(
service.ContextWith[serviceNTP.TimeService](ctx, timeService) service.ContextWith[serviceNTP.TimeService](ctx, timeService)
router.timeService = timeService router.timeService = timeService
} }
if platformInterface != nil && router.interfaceMonitor != nil && router.needWIFIState {
router.interfaceMonitor.RegisterCallback(func(_ int) {
router.updateWIFIState()
})
}
return router, nil return router, nil
} }
@ -451,12 +435,6 @@ func (r *Router) Start() error {
return err return err
} }
} }
if r.packageManager != nil {
err := r.packageManager.Start()
if err != nil {
return err
}
}
if r.needGeositeDatabase { if r.needGeositeDatabase {
for _, rule := range r.rules { for _, rule := range r.rules {
err := rule.UpdateGeosite() err := rule.UpdateGeosite()
@ -477,9 +455,89 @@ func (r *Router) Start() error {
r.geositeCache = nil r.geositeCache = nil
r.geositeReader = nil r.geositeReader = nil
} }
if r.needWIFIState { if r.fakeIPStore != nil {
err := r.fakeIPStore.Start()
if err != nil {
return err
}
}
if len(r.ruleSets) > 0 {
ruleSetStartContext := NewRuleSetStartContext()
var ruleSetStartGroup task.Group
for i, ruleSet := range r.ruleSets {
ruleSetInPlace := ruleSet
ruleSetStartGroup.Append0(func(ctx context.Context) error {
err := ruleSetInPlace.StartContext(ctx, ruleSetStartContext)
if err != nil {
return E.Cause(err, "initialize rule-set[", i, "]")
}
return nil
})
}
ruleSetStartGroup.Concurrency(5)
ruleSetStartGroup.FastFail()
err := ruleSetStartGroup.Run(r.ctx)
if err != nil {
return err
}
ruleSetStartContext.Close()
}
var (
needProcessFromRuleSet bool
needWIFIStateFromRuleSet bool
)
for _, ruleSet := range r.ruleSets {
metadata := ruleSet.Metadata()
if metadata.ContainsProcessRule {
needProcessFromRuleSet = true
}
if metadata.ContainsWIFIRule {
needWIFIStateFromRuleSet = true
}
}
if needProcessFromRuleSet || r.needFindProcess {
needPackageManager := C.IsAndroid && r.platformInterface == nil
if needPackageManager {
packageManager, err := tun.NewPackageManager(r)
if err != nil {
return E.Cause(err, "create package manager")
}
if packageManager != nil {
err = packageManager.Start()
if err != nil {
return err
}
}
r.packageManager = packageManager
}
if r.platformInterface != nil {
r.processSearcher = r.platformInterface
} else {
searcher, err := process.NewSearcher(process.Config{
Logger: r.logger,
PackageManager: r.packageManager,
})
if err != nil {
if err != os.ErrInvalid {
r.logger.Warn(E.Cause(err, "create process searcher"))
}
} else {
r.processSearcher = searcher
}
}
}
if needWIFIStateFromRuleSet || r.needWIFIState {
if r.platformInterface != nil && r.interfaceMonitor != nil {
r.interfaceMonitor.RegisterCallback(func(_ int) {
r.updateWIFIState()
})
}
r.updateWIFIState() r.updateWIFIState()
} }
for i, rule := range r.rules { for i, rule := range r.rules {
err := rule.Start() err := rule.Start()
if err != nil { if err != nil {
@ -492,12 +550,6 @@ func (r *Router) Start() error {
return E.Cause(err, "initialize DNS rule[", i, "]") return E.Cause(err, "initialize DNS rule[", i, "]")
} }
} }
if r.fakeIPStore != nil {
err := r.fakeIPStore.Start()
if err != nil {
return err
}
}
for i, transport := range r.transports { for i, transport := range r.transports {
err := transport.Start() err := transport.Start()
if err != nil { if err != nil {
@ -573,6 +625,14 @@ func (r *Router) Close() error {
} }
func (r *Router) PostStart() error { func (r *Router) PostStart() error {
if len(r.ruleSets) > 0 {
for i, ruleSet := range r.ruleSets {
err := ruleSet.PostStart()
if err != nil {
return E.Cause(err, "post start rule-set[", i, "]")
}
}
}
r.started = true r.started = true
return nil return nil
} }
@ -582,11 +642,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
} }
} }
@ -594,6 +660,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 {
@ -882,6 +953,7 @@ func (r *Router) match0(ctx context.Context, metadata *adapter.InboundContext, d
} }
} }
for i, rule := range r.rules { for i, rule := range r.rules {
metadata.ResetRuleCache()
if rule.Match(metadata) { if rule.Match(metadata) {
detour := rule.Outbound() detour := rule.Outbound()
r.logger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour) r.logger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour)

View file

@ -43,6 +43,7 @@ func (r *Router) matchDNS(ctx context.Context) (context.Context, dns.Transport,
panic("no context") panic("no context")
} }
for i, rule := range r.dnsRules { for i, rule := range r.dnsRules {
metadata.ResetRuleCache()
if rule.Match(metadata) { if rule.Match(metadata) {
detour := rule.Outbound() detour := rule.Outbound()
transport, loaded := r.transportMap[detour] transport, loaded := r.transportMap[detour]

View file

@ -13,8 +13,6 @@ import (
"github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geoip"
"github.com/sagernet/sing-box/common/geosite" "github.com/sagernet/sing-box/common/geosite"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/rw"
@ -243,71 +241,3 @@ func (r *Router) downloadGeositeDatabase(savePath string) error {
} }
return err return err
} }
func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
if hasRule(rule.LogicalOptions.Rules, cond) {
return true
}
}
}
return false
}
func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
if hasDNSRule(rule.LogicalOptions.Rules, cond) {
return true
}
}
}
return false
}
func isGeoIPRule(rule option.DefaultRule) bool {
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode)
}
func isGeoIPDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode)
}
func isGeositeRule(rule option.DefaultRule) bool {
return len(rule.Geosite) > 0
}
func isGeositeDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.Geosite) > 0
}
func isProcessRule(rule option.DefaultRule) bool {
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
}
func isProcessDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
}
func notPrivateNode(code string) bool {
return code != "private"
}
func isWIFIRule(rule option.DefaultRule) bool {
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
}
func isWIFIDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
}

99
route/router_rule.go Normal file
View file

@ -0,0 +1,99 @@
package route
import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
)
func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
if hasRule(rule.LogicalOptions.Rules, cond) {
return true
}
}
}
return false
}
func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
if hasDNSRule(rule.LogicalOptions.Rules, cond) {
return true
}
}
}
return false
}
func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
if hasHeadlessRule(rule.LogicalOptions.Rules, cond) {
return true
}
}
}
return false
}
func isGeoIPRule(rule option.DefaultRule) bool {
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode)
}
func isGeoIPDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode)
}
func isGeositeRule(rule option.DefaultRule) bool {
return len(rule.Geosite) > 0
}
func isGeositeDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.Geosite) > 0
}
func isProcessRule(rule option.DefaultRule) bool {
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
}
func isProcessDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
}
func isProcessHeadlessRule(rule option.DefaultHeadlessRule) bool {
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0
}
func notPrivateNode(code string) bool {
return code != "private"
}
func isWIFIRule(rule option.DefaultRule) bool {
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
}
func isWIFIDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
}
func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool {
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
}

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"
@ -16,6 +17,7 @@ type abstractDefaultRule struct {
destinationAddressItems []RuleItem destinationAddressItems []RuleItem
destinationPortItems []RuleItem destinationPortItems []RuleItem
allItems []RuleItem allItems []RuleItem
ruleSetItem RuleItem
invert bool invert bool
outbound string outbound string
} }
@ -61,62 +63,62 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
return true return true
} }
if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch {
for _, item := range r.sourceAddressItems {
if item.Match(metadata) {
metadata.SourceAddressMatch = true
break
}
}
}
if len(r.sourcePortItems) > 0 && !metadata.SourceAddressMatch {
for _, item := range r.sourcePortItems {
if item.Match(metadata) {
metadata.SourcePortMatch = true
break
}
}
}
if len(r.destinationAddressItems) > 0 && !metadata.SourceAddressMatch {
for _, item := range r.destinationAddressItems {
if item.Match(metadata) {
metadata.DestinationAddressMatch = true
break
}
}
}
if len(r.destinationPortItems) > 0 && !metadata.SourceAddressMatch {
for _, item := range r.destinationPortItems {
if item.Match(metadata) {
metadata.DestinationPortMatch = true
break
}
}
}
for _, item := range r.items { for _, item := range r.items {
if !item.Match(metadata) { if !item.Match(metadata) {
return r.invert return r.invert
} }
} }
if len(r.sourceAddressItems) > 0 { if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch {
var sourceAddressMatch bool return r.invert
for _, item := range r.sourceAddressItems {
if item.Match(metadata) {
sourceAddressMatch = true
break
}
}
if !sourceAddressMatch {
return r.invert
}
} }
if len(r.sourcePortItems) > 0 { if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch {
var sourcePortMatch bool return r.invert
for _, item := range r.sourcePortItems {
if item.Match(metadata) {
sourcePortMatch = true
break
}
}
if !sourcePortMatch {
return r.invert
}
} }
if len(r.destinationAddressItems) > 0 { if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch {
var destinationAddressMatch bool return r.invert
for _, item := range r.destinationAddressItems {
if item.Match(metadata) {
destinationAddressMatch = true
break
}
}
if !destinationAddressMatch {
return r.invert
}
} }
if len(r.destinationPortItems) > 0 { if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch {
var destinationPortMatch bool return r.invert
for _, item := range r.destinationPortItems {
if item.Match(metadata) {
destinationPortMatch = true
break
}
}
if !destinationPortMatch {
return r.invert
}
} }
return !r.invert return !r.invert
@ -135,7 +137,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 +148,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 +161,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 +174,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 +188,13 @@ 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 {
metadata.ResetRuleCache()
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 {
metadata.ResetRuleCache()
return it.Match(metadata) return it.Match(metadata)
}) != r.invert }) != r.invert
} }

View file

@ -194,6 +194,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 +211,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,
}, },

View file

@ -190,6 +190,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 +217,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,
}, },

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

84
route/rule_set_local.go Normal file
View file

@ -0,0 +1,84 @@
package route
import (
"context"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
var _ adapter.RuleSet = (*LocalRuleSet)(nil)
type LocalRuleSet struct {
rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata
}
func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) {
var plainRuleSet option.PlainRuleSet
switch options.Format {
case C.RuleSetFormatSource, "":
content, err := os.ReadFile(options.LocalOptions.Path)
if err != nil {
return nil, err
}
compat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return nil, err
}
plainRuleSet = compat.Upgrade()
case C.RuleSetFormatBinary:
setFile, err := os.Open(options.LocalOptions.Path)
if err != nil {
return nil, err
}
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))
var err error
for i, ruleOptions := range plainRuleSet.Rules {
rules[i], err = NewHeadlessRule(router, ruleOptions)
if err != nil {
return nil, E.Cause(err, "parse rule_set.rules.[", i, "]")
}
}
var metadata adapter.RuleSetMetadata
metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
return &LocalRuleSet{rules, metadata}, nil
}
func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
for _, rule := range s.rules {
if rule.Match(metadata) {
return true
}
}
return false
}
func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
return nil
}
func (s *LocalRuleSet) PostStart() error {
return nil
}
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
return s.metadata
}
func (s *LocalRuleSet) Close() error {
return nil
}

262
route/rule_set_remote.go Normal file
View file

@ -0,0 +1,262 @@
package route
import (
"bytes"
"context"
"io"
"net"
"net/http"
"runtime"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
)
var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
type RemoteRuleSet struct {
ctx context.Context
cancel context.CancelFunc
router adapter.Router
logger logger.ContextLogger
options option.RuleSet
metadata adapter.RuleSetMetadata
updateInterval time.Duration
dialer N.Dialer
rules []adapter.HeadlessRule
lastUpdated time.Time
lastEtag string
updateTicker *time.Ticker
pauseManager pause.Manager
}
func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet {
ctx, cancel := context.WithCancel(ctx)
var updateInterval time.Duration
if options.RemoteOptions.UpdateInterval > 0 {
updateInterval = time.Duration(options.RemoteOptions.UpdateInterval)
} else {
updateInterval = 24 * time.Hour
}
return &RemoteRuleSet{
ctx: ctx,
cancel: cancel,
router: router,
logger: logger,
options: options,
updateInterval: updateInterval,
pauseManager: pause.ManagerFromContext(ctx),
}
}
func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
for _, rule := range s.rules {
if rule.Match(metadata) {
return true
}
}
return false
}
func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
var dialer N.Dialer
if s.options.RemoteOptions.DownloadDetour != "" {
outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour)
if !loaded {
return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour)
}
dialer = outbound
} else {
outbound, err := s.router.DefaultOutbound(N.NetworkTCP)
if err != nil {
return err
}
dialer = outbound
}
s.dialer = dialer
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
if cacheFile != nil {
if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil {
err := s.loadBytes(savedSet.Content)
if err != nil {
return E.Cause(err, "restore cached rule-set")
}
s.lastUpdated = savedSet.LastUpdated
s.lastEtag = savedSet.LastEtag
}
}
if s.lastUpdated.IsZero() {
err := s.fetchOnce(ctx, startContext)
if err != nil {
return E.Cause(err, "initial rule-set: ", s.options.Tag)
}
}
s.updateTicker = time.NewTicker(s.updateInterval)
go s.loopUpdate()
return nil
}
func (s *RemoteRuleSet) PostStart() error {
if s.lastUpdated.IsZero() {
err := s.fetchOnce(s.ctx, nil)
if err != nil {
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
}
}
return nil
}
func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
return s.metadata
}
func (s *RemoteRuleSet) loadBytes(content []byte) error {
var (
plainRuleSet option.PlainRuleSet
err error
)
switch s.options.Format {
case C.RuleSetFormatSource, "":
var compat option.PlainRuleSetCompat
compat, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
plainRuleSet = compat.Upgrade()
case C.RuleSetFormatBinary:
plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
if err != nil {
return err
}
default:
return E.New("unknown rule set format: ", s.options.Format)
}
rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules))
for i, ruleOptions := range plainRuleSet.Rules {
rules[i], err = NewHeadlessRule(s.router, ruleOptions)
if err != nil {
return E.Cause(err, "parse rule_set.rules.[", i, "]")
}
}
s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
s.rules = rules
return nil
}
func (s *RemoteRuleSet) loopUpdate() {
if time.Since(s.lastUpdated) > s.updateInterval {
err := s.fetchOnce(s.ctx, nil)
if err != nil {
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
}
}
for {
runtime.GC()
select {
case <-s.ctx.Done():
return
case <-s.updateTicker.C:
s.pauseManager.WaitActive()
err := s.fetchOnce(s.ctx, nil)
if err != nil {
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
}
}
}
}
func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.RuleSetStartContext) error {
s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL)
var httpClient *http.Client
if startContext != nil {
httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer)
} else {
httpClient = &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
}
request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil)
if err != nil {
return err
}
if s.lastEtag != "" {
request.Header.Set("If-None-Match", s.lastEtag)
}
response, err := httpClient.Do(request.WithContext(ctx))
if err != nil {
return err
}
switch response.StatusCode {
case http.StatusOK:
case http.StatusNotModified:
s.lastUpdated = time.Now()
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
if cacheFile != nil {
savedRuleSet := cacheFile.LoadRuleSet(s.options.Tag)
if savedRuleSet != nil {
savedRuleSet.LastUpdated = s.lastUpdated
err = cacheFile.SaveRuleSet(s.options.Tag, savedRuleSet)
if err != nil {
s.logger.Error("save rule-set updated time: ", err)
return nil
}
}
}
s.logger.Info("update rule-set ", s.options.Tag, ": not modified")
return nil
default:
return E.New("unexpected status: ", response.Status)
}
content, err := io.ReadAll(response.Body)
if err != nil {
response.Body.Close()
return err
}
err = s.loadBytes(content)
if err != nil {
response.Body.Close()
return err
}
response.Body.Close()
eTagHeader := response.Header.Get("Etag")
if eTagHeader != "" {
s.lastEtag = eTagHeader
}
s.lastUpdated = time.Now()
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
if cacheFile != nil {
err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{
LastUpdated: s.lastUpdated,
Content: content,
LastEtag: s.lastEtag,
})
if err != nil {
s.logger.Error("save rule-set cache: ", err)
}
}
s.logger.Info("updated rule-set ", s.options.Tag)
return nil
}
func (s *RemoteRuleSet) Close() error {
s.updateTicker.Stop()
s.cancel()
return nil
}