mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Merge branch 'dev-next' into origin/tls-client-auth
Signed-off-by: jose-C2OaWi <111356383+jose-C2OaWi@users.noreply.github.com>
This commit is contained in:
commit
49fc4c6a7b
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
/.idea/
|
||||
/vendor/
|
||||
/*.json
|
||||
/*.srs
|
||||
/*.db
|
||||
/site/
|
||||
/bin/
|
||||
|
@ -1,11 +1,16 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
type ClashServer interface {
|
||||
@ -23,6 +28,7 @@ type CacheFile interface {
|
||||
PreStarter
|
||||
|
||||
StoreFakeIP() bool
|
||||
FakeIPStorage
|
||||
|
||||
LoadMode() string
|
||||
StoreMode(mode string) error
|
||||
@ -30,7 +36,65 @@ type CacheFile interface {
|
||||
StoreSelected(group string, selected string) error
|
||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||
StoreGroupExpand(group string, expand bool) error
|
||||
FakeIPStorage
|
||||
LoadRuleSet(tag string) *SavedRuleSet
|
||||
SaveRuleSet(tag string, set *SavedRuleSet) error
|
||||
}
|
||||
|
||||
type SavedRuleSet struct {
|
||||
Content []byte
|
||||
LastUpdated time.Time
|
||||
LastEtag string
|
||||
}
|
||||
|
||||
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = rw.WriteUVariant(&buffer, uint64(len(s.Content)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer.Write(s.Content)
|
||||
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = rw.WriteVString(&buffer, s.LastEtag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
||||
reader := bytes.NewReader(data)
|
||||
var version uint8
|
||||
err := binary.Read(reader, binary.BigEndian, &version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contentLen, err := rw.ReadUVariant(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Content = make([]byte, contentLen)
|
||||
_, err = io.ReadFull(reader, s.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var lastUpdated int64
|
||||
err = binary.Read(reader, binary.BigEndian, &lastUpdated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.LastUpdated = time.Unix(lastUpdated, 0)
|
||||
s.LastEtag, err = rw.ReadVString(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Tracker interface {
|
||||
|
@ -47,6 +47,7 @@ type InboundContext struct {
|
||||
GeoIPCode string
|
||||
ProcessInfo *process.Info
|
||||
FakeIP bool
|
||||
IPCIDRMatchSource bool
|
||||
|
||||
// dns cache
|
||||
|
||||
|
@ -18,7 +18,7 @@ type Router interface {
|
||||
|
||||
Outbounds() []Outbound
|
||||
Outbound(tag string) (Outbound, bool)
|
||||
DefaultOutbound(network string) Outbound
|
||||
DefaultOutbound(network string) (Outbound, error)
|
||||
|
||||
FakeIPStore() FakeIPStore
|
||||
|
||||
@ -27,6 +27,8 @@ type Router interface {
|
||||
GeoIPReader() *geoip.Reader
|
||||
LoadGeosite(code string) (Rule, error)
|
||||
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
|
||||
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
||||
@ -61,11 +63,15 @@ func RouterFromContext(ctx context.Context) Router {
|
||||
return service.FromContext[Router](ctx)
|
||||
}
|
||||
|
||||
type HeadlessRule interface {
|
||||
Match(metadata *InboundContext) bool
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
HeadlessRule
|
||||
Service
|
||||
Type() string
|
||||
UpdateGeosite() error
|
||||
Match(metadata *InboundContext) bool
|
||||
Outbound() string
|
||||
String() string
|
||||
}
|
||||
@ -76,6 +82,11 @@ type DNSRule interface {
|
||||
RewriteTTL() *uint32
|
||||
}
|
||||
|
||||
type RuleSet interface {
|
||||
HeadlessRule
|
||||
Service
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
InterfaceUpdated()
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@ -69,41 +68,3 @@ func format() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatOne(configPath string) error {
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "read config")
|
||||
}
|
||||
var options option.Options
|
||||
err = options.UnmarshalJSON(configContent)
|
||||
if err != nil {
|
||||
return E.Cause(err, "decode config")
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(options)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
if !commandFormatFlagWrite {
|
||||
os.Stdout.WriteString(buffer.String() + "\n")
|
||||
return nil
|
||||
}
|
||||
if bytes.Equal(configContent, buffer.Bytes()) {
|
||||
return nil
|
||||
}
|
||||
output, err := os.Create(configPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "open output")
|
||||
}
|
||||
_, err = output.Write(buffer.Bytes())
|
||||
output.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "write output")
|
||||
}
|
||||
outputPath, _ := filepath.Abs(configPath)
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
return nil
|
||||
}
|
||||
|
43
cmd/sing-box/cmd_geoip.go
Normal file
43
cmd/sing-box/cmd_geoip.go
Normal 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
|
||||
}
|
98
cmd/sing-box/cmd_geoip_export.go
Normal file
98
cmd/sing-box/cmd_geoip_export.go
Normal 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)
|
||||
}
|
31
cmd/sing-box/cmd_geoip_list.go
Normal file
31
cmd/sing-box/cmd_geoip_list.go
Normal 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
|
||||
}
|
47
cmd/sing-box/cmd_geoip_lookup.go
Normal file
47
cmd/sing-box/cmd_geoip_lookup.go
Normal 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
|
||||
}
|
41
cmd/sing-box/cmd_geosite.go
Normal file
41
cmd/sing-box/cmd_geosite.go
Normal 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
|
||||
}
|
81
cmd/sing-box/cmd_geosite_export.go
Normal file
81
cmd/sing-box/cmd_geosite_export.go
Normal 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)
|
||||
}
|
50
cmd/sing-box/cmd_geosite_list.go
Normal file
50
cmd/sing-box/cmd_geosite_list.go
Normal 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
|
||||
}
|
97
cmd/sing-box/cmd_geosite_lookup.go
Normal file
97
cmd/sing-box/cmd_geosite_lookup.go
Normal 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
|
||||
}
|
56
cmd/sing-box/cmd_geosite_matcher.go
Normal file
56
cmd/sing-box/cmd_geosite_matcher.go
Normal 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 ""
|
||||
}
|
@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
var commandMerge = &cobra.Command{
|
||||
Use: "merge [output]",
|
||||
Use: "merge <output>",
|
||||
Short: "Merge configurations",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := merge(args[0])
|
||||
|
14
cmd/sing-box/cmd_rule_set.go
Normal file
14
cmd/sing-box/cmd_rule_set.go
Normal 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)
|
||||
}
|
80
cmd/sing-box/cmd_rule_set_compile.go
Normal file
80
cmd/sing-box/cmd_rule_set_compile.go
Normal file
@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var flagRuleSetCompileOutput string
|
||||
|
||||
const flagRuleSetCompileDefaultOutput = "<file_name>.srs"
|
||||
|
||||
var commandRuleSetCompile = &cobra.Command{
|
||||
Use: "compile [source-path]",
|
||||
Short: "Compile rule-set json to binary",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := compileRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSet.AddCommand(commandRuleSetCompile)
|
||||
commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
|
||||
}
|
||||
|
||||
func compileRuleSet(sourcePath string) error {
|
||||
var (
|
||||
reader io.Reader
|
||||
err error
|
||||
)
|
||||
if sourcePath == "stdin" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
reader, err = os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
decoder := json.NewDecoder(json.NewCommentFilter(reader))
|
||||
decoder.DisallowUnknownFields()
|
||||
var plainRuleSet option.PlainRuleSetCompat
|
||||
err = decoder.Decode(&plainRuleSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ruleSet := plainRuleSet.Upgrade()
|
||||
var outputPath string
|
||||
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
|
||||
if strings.HasSuffix(sourcePath, ".json") {
|
||||
outputPath = sourcePath[:len(sourcePath)-5] + ".srs"
|
||||
} else {
|
||||
outputPath = sourcePath + ".srs"
|
||||
}
|
||||
} else {
|
||||
outputPath = flagRuleSetCompileOutput
|
||||
}
|
||||
outputFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = srs.Write(outputFile, ruleSet)
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
os.Remove(outputPath)
|
||||
return err
|
||||
}
|
||||
outputFile.Close()
|
||||
return nil
|
||||
}
|
87
cmd/sing-box/cmd_rule_set_format.go
Normal file
87
cmd/sing-box/cmd_rule_set_format.go
Normal file
@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandRuleSetFormatFlagWrite bool
|
||||
|
||||
var commandRuleSetFormat = &cobra.Command{
|
||||
Use: "format <source-path>",
|
||||
Short: "Format rule-set json",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := formatRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||
commandRuleSet.AddCommand(commandRuleSetFormat)
|
||||
}
|
||||
|
||||
func formatRuleSet(sourcePath string) error {
|
||||
var (
|
||||
reader io.Reader
|
||||
err error
|
||||
)
|
||||
if sourcePath == "stdin" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
reader, err = os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content)))
|
||||
decoder.DisallowUnknownFields()
|
||||
var plainRuleSet option.PlainRuleSetCompat
|
||||
err = decoder.Decode(&plainRuleSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ruleSet := plainRuleSet.Upgrade()
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(ruleSet)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
outputPath, _ := filepath.Abs(sourcePath)
|
||||
if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" {
|
||||
os.Stdout.WriteString(buffer.String() + "\n")
|
||||
return nil
|
||||
}
|
||||
if bytes.Equal(content, buffer.Bytes()) {
|
||||
return nil
|
||||
}
|
||||
output, err := os.Create(sourcePath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "open output")
|
||||
}
|
||||
_, err = output.Write(buffer.Bytes())
|
||||
output.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "write output")
|
||||
}
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
return nil
|
||||
}
|
@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) {
|
||||
|
||||
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
|
||||
if outboundTag == "" {
|
||||
outbound := instance.Router().DefaultOutbound(N.NetworkName(network))
|
||||
if outbound == nil {
|
||||
return nil, E.New("missing default outbound")
|
||||
}
|
||||
return outbound, nil
|
||||
return instance.Router().DefaultOutbound(N.NetworkName(network))
|
||||
} else {
|
||||
outbound, loaded := instance.Router().Outbound(outboundTag)
|
||||
if !loaded {
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
var commandConnectFlagNetwork string
|
||||
|
||||
var commandConnect = &cobra.Command{
|
||||
Use: "connect [address]",
|
||||
Use: "connect <address>",
|
||||
Short: "Connect to an address",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
@ -18,11 +18,19 @@ func NewRouter(router adapter.Router) N.Dialer {
|
||||
}
|
||||
|
||||
func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
return d.router.DefaultOutbound(network).DialContext(ctx, network, destination)
|
||||
dialer, err := d.router.DefaultOutbound(network)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination)
|
||||
dialer, err := d.router.DefaultOutbound(N.NetworkUDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
|
||||
func (d *RouterDialer) Upstream() any {
|
||||
|
485
common/srs/binary.go
Normal file
485
common/srs/binary.go
Normal file
@ -0,0 +1,485 @@
|
||||
package srs
|
||||
|
||||
import (
|
||||
"compress/zlib"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/netip"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/domain"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS
|
||||
|
||||
const (
|
||||
ruleItemQueryType uint8 = iota
|
||||
ruleItemNetwork
|
||||
ruleItemDomain
|
||||
ruleItemDomainKeyword
|
||||
ruleItemDomainRegex
|
||||
ruleItemSourceIPCIDR
|
||||
ruleItemIPCIDR
|
||||
ruleItemSourcePort
|
||||
ruleItemSourcePortRange
|
||||
ruleItemPort
|
||||
ruleItemPortRange
|
||||
ruleItemProcessName
|
||||
ruleItemProcessPath
|
||||
ruleItemPackageName
|
||||
ruleItemWIFISSID
|
||||
ruleItemWIFIBSSID
|
||||
ruleItemFinal uint8 = 0xFF
|
||||
)
|
||||
|
||||
func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) {
|
||||
var magicBytes [3]byte
|
||||
_, err = io.ReadFull(reader, magicBytes[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if magicBytes != MagicBytes {
|
||||
err = E.New("invalid sing-box rule set file")
|
||||
return
|
||||
}
|
||||
var version uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &version)
|
||||
if err != nil {
|
||||
return ruleSet, err
|
||||
}
|
||||
if version != 1 {
|
||||
return ruleSet, E.New("unsupported version: ", version)
|
||||
}
|
||||
zReader, err := zlib.NewReader(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
length, err := rw.ReadUVariant(zReader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ruleSet.Rules = make([]option.HeadlessRule, length)
|
||||
for i := uint64(0); i < length; i++ {
|
||||
ruleSet.Rules[i], err = readRule(zReader, recovery)
|
||||
if err != nil {
|
||||
err = E.Cause(err, "read rule[", i, "]")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Write(writer io.Writer, ruleSet option.PlainRuleSet) error {
|
||||
_, err := writer.Write(MagicBytes[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rule := range ruleSet.Rules {
|
||||
err = writeRule(zWriter, rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return zWriter.Close()
|
||||
}
|
||||
|
||||
func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) {
|
||||
var ruleType uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &ruleType)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch ruleType {
|
||||
case 0:
|
||||
rule.DefaultOptions, err = readDefaultRule(reader, recovery)
|
||||
case 1:
|
||||
rule.LogicalOptions, err = readLogicalRule(reader, recovery)
|
||||
default:
|
||||
err = E.New("unknown rule type: ", ruleType)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeRule(writer io.Writer, rule option.HeadlessRule) error {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
return writeDefaultRule(writer, rule.DefaultOptions)
|
||||
case C.RuleTypeLogical:
|
||||
return writeLogicalRule(writer, rule.LogicalOptions)
|
||||
default:
|
||||
panic("unknown rule type: " + rule.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) {
|
||||
var lastItemType uint8
|
||||
for {
|
||||
var itemType uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &itemType)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch itemType {
|
||||
case ruleItemQueryType:
|
||||
var rawQueryType []uint16
|
||||
rawQueryType, err = readRuleItemUint16(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType {
|
||||
return option.DNSQueryType(it)
|
||||
})
|
||||
case ruleItemNetwork:
|
||||
rule.Network, err = readRuleItemString(reader)
|
||||
case ruleItemDomain:
|
||||
var matcher *domain.Matcher
|
||||
matcher, err = domain.ReadMatcher(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rule.DomainMatcher = matcher
|
||||
case ruleItemDomainKeyword:
|
||||
rule.DomainKeyword, err = readRuleItemString(reader)
|
||||
case ruleItemDomainRegex:
|
||||
rule.DomainRegex, err = readRuleItemString(reader)
|
||||
case ruleItemSourceIPCIDR:
|
||||
rule.SourceIPSet, err = readIPSet(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if recovery {
|
||||
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
|
||||
}
|
||||
case ruleItemIPCIDR:
|
||||
rule.IPSet, err = readIPSet(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if recovery {
|
||||
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
|
||||
}
|
||||
case ruleItemSourcePort:
|
||||
rule.SourcePort, err = readRuleItemUint16(reader)
|
||||
case ruleItemSourcePortRange:
|
||||
rule.SourcePortRange, err = readRuleItemString(reader)
|
||||
case ruleItemPort:
|
||||
rule.Port, err = readRuleItemUint16(reader)
|
||||
case ruleItemPortRange:
|
||||
rule.PortRange, err = readRuleItemString(reader)
|
||||
case ruleItemProcessName:
|
||||
rule.ProcessName, err = readRuleItemString(reader)
|
||||
case ruleItemProcessPath:
|
||||
rule.ProcessPath, err = readRuleItemString(reader)
|
||||
case ruleItemPackageName:
|
||||
rule.PackageName, err = readRuleItemString(reader)
|
||||
case ruleItemWIFISSID:
|
||||
rule.WIFISSID, err = readRuleItemString(reader)
|
||||
case ruleItemWIFIBSSID:
|
||||
rule.WIFIBSSID, err = readRuleItemString(reader)
|
||||
case ruleItemFinal:
|
||||
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
|
||||
return
|
||||
default:
|
||||
err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lastItemType = itemType
|
||||
}
|
||||
}
|
||||
|
||||
func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
|
||||
err := binary.Write(writer, binary.BigEndian, uint8(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rule.QueryType) > 0 {
|
||||
err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 {
|
||||
return uint16(it)
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.Network) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemNetwork, rule.Network)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 {
|
||||
err = binary.Write(writer, binary.BigEndian, ruleItemDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.DomainKeyword) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.DomainRegex) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.SourceIPCIDR) > 0 {
|
||||
err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR)
|
||||
if err != nil {
|
||||
return E.Cause(err, "source_ipcidr")
|
||||
}
|
||||
}
|
||||
if len(rule.IPCIDR) > 0 {
|
||||
err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR)
|
||||
if err != nil {
|
||||
return E.Cause(err, "ipcidr")
|
||||
}
|
||||
}
|
||||
if len(rule.SourcePort) > 0 {
|
||||
err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.SourcePortRange) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.Port) > 0 {
|
||||
err = writeRuleItemUint16(writer, ruleItemPort, rule.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.PortRange) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.ProcessName) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.ProcessPath) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.PackageName) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.WIFISSID) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.WIFIBSSID) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, ruleItemFinal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, rule.Invert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readRuleItemString(reader io.Reader) ([]string, error) {
|
||||
length, err := rw.ReadUVariant(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value := make([]string, length)
|
||||
for i := uint64(0); i < length; i++ {
|
||||
value[i], err = rw.ReadVString(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error {
|
||||
err := binary.Write(writer, binary.BigEndian, itemType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteUVariant(writer, uint64(len(value)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range value {
|
||||
err = rw.WriteVString(writer, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readRuleItemUint16(reader io.Reader) ([]uint16, error) {
|
||||
length, err := rw.ReadUVariant(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value := make([]uint16, length)
|
||||
for i := uint64(0); i < length; i++ {
|
||||
err = binary.Read(reader, binary.BigEndian, &value[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error {
|
||||
err := binary.Write(writer, binary.BigEndian, itemType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteUVariant(writer, uint64(len(value)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range value {
|
||||
err = binary.Write(writer, binary.BigEndian, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
|
||||
var builder netipx.IPSetBuilder
|
||||
for i, prefixString := range value {
|
||||
prefix, err := netip.ParsePrefix(prefixString)
|
||||
if err == nil {
|
||||
builder.AddPrefix(prefix)
|
||||
continue
|
||||
}
|
||||
addr, addrErr := netip.ParseAddr(prefixString)
|
||||
if addrErr == nil {
|
||||
builder.Add(addr)
|
||||
continue
|
||||
}
|
||||
return E.Cause(err, "parse [", i, "]")
|
||||
}
|
||||
ipSet, err := builder.IPSet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, itemType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeIPSet(writer, ipSet)
|
||||
}
|
||||
|
||||
func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
|
||||
var mode uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &mode)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch mode {
|
||||
case 0:
|
||||
logicalRule.Mode = C.LogicalTypeAnd
|
||||
case 1:
|
||||
logicalRule.Mode = C.LogicalTypeOr
|
||||
default:
|
||||
err = E.New("unknown logical mode: ", mode)
|
||||
return
|
||||
}
|
||||
length, err := rw.ReadUVariant(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logicalRule.Rules = make([]option.HeadlessRule, length)
|
||||
for i := uint64(0); i < length; i++ {
|
||||
logicalRule.Rules[i], err = readRule(reader, recovery)
|
||||
if err != nil {
|
||||
err = E.Cause(err, "read logical rule [", i, "]")
|
||||
return
|
||||
}
|
||||
}
|
||||
err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error {
|
||||
err := binary.Write(writer, binary.BigEndian, uint8(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch logicalRule.Mode {
|
||||
case C.LogicalTypeAnd:
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(0))
|
||||
case C.LogicalTypeOr:
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(1))
|
||||
default:
|
||||
panic("unknown logical mode: " + logicalRule.Mode)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rule := range logicalRule.Rules {
|
||||
err = writeRule(writer, rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, logicalRule.Invert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
116
common/srs/ip_set.go
Normal file
116
common/srs/ip_set.go
Normal 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
|
||||
}
|
@ -9,3 +9,11 @@ const (
|
||||
LogicalTypeAnd = "and"
|
||||
LogicalTypeOr = "or"
|
||||
)
|
||||
|
||||
const (
|
||||
RuleSetTypeLocal = "local"
|
||||
RuleSetTypeRemote = "remote"
|
||||
RuleSetVersion1 = 1
|
||||
RuleSetFormatSource = "source"
|
||||
RuleSetFormatBinary = "binary"
|
||||
)
|
||||
|
@ -1,3 +1,13 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.8.0"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
@ -85,6 +95,10 @@
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
],
|
||||
"invert": false,
|
||||
"outbound": [
|
||||
"direct"
|
||||
@ -166,15 +180,23 @@ Match domain using regular expression.
|
||||
|
||||
#### geosite
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set).
|
||||
|
||||
Match geosite.
|
||||
|
||||
#### source_geoip
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set).
|
||||
|
||||
Match source geoip.
|
||||
|
||||
#### source_ip_cidr
|
||||
|
||||
Match source ip cidr.
|
||||
Match source IP CIDR.
|
||||
|
||||
#### source_port
|
||||
|
||||
@ -250,6 +272,12 @@ Match WiFi SSID.
|
||||
|
||||
Match WiFi BSSID.
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
Match [Rule Set](/configuration/route/#rule_set).
|
||||
|
||||
#### invert
|
||||
|
||||
Invert match result.
|
||||
@ -286,4 +314,4 @@ Rewrite TTL in DNS responses.
|
||||
|
||||
#### rules
|
||||
|
||||
Included default rules.
|
||||
Included rules.
|
@ -1,3 +1,13 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.8.0 中的更改"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@ -84,6 +94,10 @@
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
],
|
||||
"invert": false,
|
||||
"outbound": [
|
||||
"direct"
|
||||
@ -163,10 +177,18 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
#### geosite
|
||||
|
||||
匹配 GeoSite。
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。
|
||||
|
||||
匹配 Geosite。
|
||||
|
||||
#### source_geoip
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。
|
||||
|
||||
匹配源 GeoIP。
|
||||
|
||||
#### source_ip_cidr
|
||||
@ -245,6 +267,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
匹配 WiFi BSSID。
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
匹配[规则集](/zh/configuration/route/#rule_set)。
|
||||
|
||||
#### invert
|
||||
|
||||
反选匹配结果。
|
||||
@ -281,4 +309,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
#### rules
|
||||
|
||||
包括的默认规则。
|
||||
包括的规则。
|
34
docs/configuration/experimental/cache-file.md
Normal file
34
docs/configuration/experimental/cache-file.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"path": "",
|
||||
"cache_id": "",
|
||||
"store_fakeip": false
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### enabled
|
||||
|
||||
Enable cache file.
|
||||
|
||||
#### path
|
||||
|
||||
Path to the cache file.
|
||||
|
||||
`cache.db` will be used if empty.
|
||||
|
||||
#### cache_id
|
||||
|
||||
Identifier in cache file.
|
||||
|
||||
If not empty, configuration specified data will use a separate store keyed by it.
|
121
docs/configuration/experimental/clash-api.md
Normal file
121
docs/configuration/experimental/clash-api.md
Normal file
@ -0,0 +1,121 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.8.0"
|
||||
|
||||
:material-delete-alert: [store_mode](#store_mode)
|
||||
:material-delete-alert: [store_selected](#store_selected)
|
||||
:material-delete-alert: [store_fakeip](#store_fakeip)
|
||||
:material-delete-alert: [cache_file](#cache_file)
|
||||
:material-delete-alert: [cache_id](#cache_id)
|
||||
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Clash API is not included by default, see [Installation](./#installation).
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"external_controller": "127.0.0.1:9090",
|
||||
"external_ui": "",
|
||||
"external_ui_download_url": "",
|
||||
"external_ui_download_detour": "",
|
||||
"secret": "",
|
||||
"default_mode": "",
|
||||
|
||||
// Deprecated
|
||||
|
||||
"store_mode": false,
|
||||
"store_selected": false,
|
||||
"store_fakeip": false,
|
||||
"cache_file": "",
|
||||
"cache_id": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### external_controller
|
||||
|
||||
RESTful web API listening address. Clash API will be disabled if empty.
|
||||
|
||||
#### external_ui
|
||||
|
||||
A relative path to the configuration directory or an absolute path to a
|
||||
directory in which you put some static web resource. sing-box will then
|
||||
serve it at `http://{{external-controller}}/ui`.
|
||||
|
||||
|
||||
|
||||
#### external_ui_download_url
|
||||
|
||||
ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.
|
||||
|
||||
`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.
|
||||
|
||||
#### external_ui_download_detour
|
||||
|
||||
The tag of the outbound to download the external UI.
|
||||
|
||||
Default outbound will be used if empty.
|
||||
|
||||
#### secret
|
||||
|
||||
Secret for the RESTful API (optional)
|
||||
Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
|
||||
ALWAYS set a secret if RESTful API is listening on 0.0.0.0
|
||||
|
||||
#### default_mode
|
||||
|
||||
Default mode in clash, `Rule` will be used if empty.
|
||||
|
||||
This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item.
|
||||
|
||||
#### store_mode
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
`store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`.
|
||||
|
||||
Store Clash mode in cache file.
|
||||
|
||||
#### store_selected
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
`store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`.
|
||||
|
||||
!!! note ""
|
||||
|
||||
The tag must be set for target outbounds.
|
||||
|
||||
Store selected outbound for the `Selector` outbound in cache file.
|
||||
|
||||
#### store_fakeip
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
`store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`.
|
||||
|
||||
Store fakeip in cache file.
|
||||
|
||||
#### cache_file
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
`cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`.
|
||||
|
||||
Cache file path, `cache.db` will be used if empty.
|
||||
|
||||
#### cache_id
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
`cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`.
|
||||
|
||||
Identifier in cache file.
|
||||
|
||||
If not empty, configuration specified data will use a separate store keyed by it.
|
@ -1,139 +1,30 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
# Experimental
|
||||
|
||||
!!! quote "Changes in sing-box 1.8.0"
|
||||
|
||||
:material-plus: [cache_file](#cache_file)
|
||||
:material-alert-decagram: [clash_api](#clash_api)
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"clash_api": {
|
||||
"external_controller": "127.0.0.1:9090",
|
||||
"external_ui": "",
|
||||
"external_ui_download_url": "",
|
||||
"external_ui_download_detour": "",
|
||||
"secret": "",
|
||||
"default_mode": "",
|
||||
"store_mode": false,
|
||||
"store_selected": false,
|
||||
"store_fakeip": false,
|
||||
"cache_file": "",
|
||||
"cache_id": ""
|
||||
},
|
||||
"v2ray_api": {
|
||||
"listen": "127.0.0.1:8080",
|
||||
"stats": {
|
||||
"enabled": true,
|
||||
"inbounds": [
|
||||
"socks-in"
|
||||
],
|
||||
"outbounds": [
|
||||
"proxy",
|
||||
"direct"
|
||||
],
|
||||
"users": [
|
||||
"sekai"
|
||||
]
|
||||
}
|
||||
}
|
||||
"cache_file": {},
|
||||
"clash_api": {},
|
||||
"v2ray_api": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note ""
|
||||
### Fields
|
||||
|
||||
Traffic statistics and connection management can degrade performance.
|
||||
|
||||
### Clash API Fields
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Clash API is not included by default, see [Installation](./#installation).
|
||||
|
||||
#### external_controller
|
||||
|
||||
RESTful web API listening address. Clash API will be disabled if empty.
|
||||
|
||||
#### external_ui
|
||||
|
||||
A relative path to the configuration directory or an absolute path to a
|
||||
directory in which you put some static web resource. sing-box will then
|
||||
serve it at `http://{{external-controller}}/ui`.
|
||||
|
||||
#### external_ui_download_url
|
||||
|
||||
ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.
|
||||
|
||||
`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.
|
||||
|
||||
#### external_ui_download_detour
|
||||
|
||||
The tag of the outbound to download the external UI.
|
||||
|
||||
Default outbound will be used if empty.
|
||||
|
||||
#### secret
|
||||
|
||||
Secret for the RESTful API (optional)
|
||||
Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
|
||||
ALWAYS set a secret if RESTful API is listening on 0.0.0.0
|
||||
|
||||
#### default_mode
|
||||
|
||||
Default mode in clash, `Rule` will be used if empty.
|
||||
|
||||
This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item.
|
||||
|
||||
#### store_mode
|
||||
|
||||
Store Clash mode in cache file.
|
||||
|
||||
#### store_selected
|
||||
|
||||
!!! note ""
|
||||
|
||||
The tag must be set for target outbounds.
|
||||
|
||||
Store selected outbound for the `Selector` outbound in cache file.
|
||||
|
||||
#### store_fakeip
|
||||
|
||||
Store fakeip in cache file.
|
||||
|
||||
#### cache_file
|
||||
|
||||
Cache file path, `cache.db` will be used if empty.
|
||||
|
||||
#### cache_id
|
||||
|
||||
Cache ID.
|
||||
|
||||
If not empty, `store_selected` will use a separate store keyed by it.
|
||||
|
||||
### V2Ray API Fields
|
||||
|
||||
!!! quote ""
|
||||
|
||||
V2Ray API is not included by default, see [Installation](./#installation).
|
||||
|
||||
#### listen
|
||||
|
||||
gRPC API listening address. V2Ray API will be disabled if empty.
|
||||
|
||||
#### stats
|
||||
|
||||
Traffic statistics service settings.
|
||||
|
||||
#### stats.enabled
|
||||
|
||||
Enable statistics service.
|
||||
|
||||
#### stats.inbounds
|
||||
|
||||
Inbound list to count traffic.
|
||||
|
||||
#### stats.outbounds
|
||||
|
||||
Outbound list to count traffic.
|
||||
|
||||
#### stats.users
|
||||
|
||||
User list to count traffic.
|
||||
| Key | Format |
|
||||
|--------------|----------------------------|
|
||||
| `cache_file` | [Cache File](./cache-file) |
|
||||
| `clash_api` | [Clash API](./clash-api) |
|
||||
| `v2ray_api` | [V2Ray API](./v2ray-api) |
|
@ -1,137 +0,0 @@
|
||||
# 实验性
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"clash_api": {
|
||||
"external_controller": "127.0.0.1:9090",
|
||||
"external_ui": "",
|
||||
"external_ui_download_url": "",
|
||||
"external_ui_download_detour": "",
|
||||
"secret": "",
|
||||
"default_mode": "",
|
||||
"store_mode": false,
|
||||
"store_selected": false,
|
||||
"store_fakeip": false,
|
||||
"cache_file": "",
|
||||
"cache_id": ""
|
||||
},
|
||||
"v2ray_api": {
|
||||
"listen": "127.0.0.1:8080",
|
||||
"stats": {
|
||||
"enabled": true,
|
||||
"inbounds": [
|
||||
"socks-in"
|
||||
],
|
||||
"outbounds": [
|
||||
"proxy",
|
||||
"direct"
|
||||
],
|
||||
"users": [
|
||||
"sekai"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note ""
|
||||
|
||||
流量统计和连接管理会降低性能。
|
||||
|
||||
### Clash API 字段
|
||||
|
||||
!!! quote ""
|
||||
|
||||
默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。
|
||||
|
||||
#### external_controller
|
||||
|
||||
RESTful web API 监听地址。如果为空,则禁用 Clash API。
|
||||
|
||||
#### external_ui
|
||||
|
||||
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
|
||||
|
||||
#### external_ui_download_url
|
||||
|
||||
静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。
|
||||
|
||||
默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。
|
||||
|
||||
#### external_ui_download_detour
|
||||
|
||||
用于下载静态网页资源的出站的标签。
|
||||
|
||||
如果为空,将使用默认出站。
|
||||
|
||||
#### secret
|
||||
|
||||
RESTful API 的密钥(可选)
|
||||
通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证
|
||||
如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。
|
||||
|
||||
#### default_mode
|
||||
|
||||
Clash 中的默认模式,默认使用 `Rule`。
|
||||
|
||||
此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。
|
||||
|
||||
#### store_mode
|
||||
|
||||
将 Clash 模式存储在缓存文件中。
|
||||
|
||||
#### store_selected
|
||||
|
||||
!!! note ""
|
||||
|
||||
必须为目标出站设置标签。
|
||||
|
||||
将 `Selector` 中出站的选定的目标出站存储在缓存文件中。
|
||||
|
||||
#### store_fakeip
|
||||
|
||||
将 fakeip 存储在缓存文件中。
|
||||
|
||||
#### cache_file
|
||||
|
||||
缓存文件路径,默认使用`cache.db`。
|
||||
|
||||
#### cache_id
|
||||
|
||||
缓存 ID。
|
||||
|
||||
如果不为空,`store_selected` 将会使用以此为键的独立存储。
|
||||
|
||||
### V2Ray API 字段
|
||||
|
||||
!!! quote ""
|
||||
|
||||
默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。
|
||||
|
||||
#### listen
|
||||
|
||||
gRPC API 监听地址。如果为空,则禁用 V2Ray API。
|
||||
|
||||
#### stats
|
||||
|
||||
流量统计服务设置。
|
||||
|
||||
#### stats.enabled
|
||||
|
||||
启用统计服务。
|
||||
|
||||
#### stats.inbounds
|
||||
|
||||
统计流量的入站列表。
|
||||
|
||||
#### stats.outbounds
|
||||
|
||||
统计流量的出站列表。
|
||||
|
||||
#### stats.users
|
||||
|
||||
统计流量的用户列表。
|
50
docs/configuration/experimental/v2ray-api.md
Normal file
50
docs/configuration/experimental/v2ray-api.md
Normal file
@ -0,0 +1,50 @@
|
||||
### Structure
|
||||
|
||||
!!! quote ""
|
||||
|
||||
V2Ray API is not included by default, see [Installation](./#installation).
|
||||
|
||||
```json
|
||||
{
|
||||
"listen": "127.0.0.1:8080",
|
||||
"stats": {
|
||||
"enabled": true,
|
||||
"inbounds": [
|
||||
"socks-in"
|
||||
],
|
||||
"outbounds": [
|
||||
"proxy",
|
||||
"direct"
|
||||
],
|
||||
"users": [
|
||||
"sekai"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### listen
|
||||
|
||||
gRPC API listening address. V2Ray API will be disabled if empty.
|
||||
|
||||
#### stats
|
||||
|
||||
Traffic statistics service settings.
|
||||
|
||||
#### stats.enabled
|
||||
|
||||
Enable statistics service.
|
||||
|
||||
#### stats.inbounds
|
||||
|
||||
Inbound list to count traffic.
|
||||
|
||||
#### stats.outbounds
|
||||
|
||||
Outbound list to count traffic.
|
||||
|
||||
#### stats.users
|
||||
|
||||
User list to count traffic.
|
@ -1,3 +1,11 @@
|
||||
---
|
||||
icon: material/delete-clock
|
||||
---
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set).
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
|
@ -1,33 +0,0 @@
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"route": {
|
||||
"geoip": {
|
||||
"path": "",
|
||||
"download_url": "",
|
||||
"download_detour": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### path
|
||||
|
||||
指定 GeoIP 资源的路径。
|
||||
|
||||
默认 `geoip.db`。
|
||||
|
||||
#### download_url
|
||||
|
||||
指定 GeoIP 资源的下载链接。
|
||||
|
||||
默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`。
|
||||
|
||||
#### download_detour
|
||||
|
||||
用于下载 GeoIP 资源的出站的标签。
|
||||
|
||||
如果为空,将使用默认出站。
|
@ -1,3 +1,11 @@
|
||||
---
|
||||
icon: material/delete-clock
|
||||
---
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set).
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
|
@ -1,33 +0,0 @@
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"route": {
|
||||
"geosite": {
|
||||
"path": "",
|
||||
"download_url": "",
|
||||
"download_detour": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### path
|
||||
|
||||
指定 GeoSite 资源的路径。
|
||||
|
||||
默认 `geosite.db`。
|
||||
|
||||
#### download_url
|
||||
|
||||
指定 GeoSite 资源的下载链接。
|
||||
|
||||
默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`。
|
||||
|
||||
#### download_detour
|
||||
|
||||
用于下载 GeoSite 资源的出站的标签。
|
||||
|
||||
如果为空,将使用默认出站。
|
@ -1,5 +1,15 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
# Route
|
||||
|
||||
!!! quote "Changes in sing-box 1.8.0"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
@ -8,6 +18,7 @@
|
||||
"geoip": {},
|
||||
"geosite": {},
|
||||
"rules": [],
|
||||
"rule_set": [],
|
||||
"final": "",
|
||||
"auto_detect_interface": false,
|
||||
"override_android_vpn": false,
|
||||
@ -20,10 +31,19 @@
|
||||
### Fields
|
||||
|
||||
| Key | Format |
|
||||
|------------|------------------------------------|
|
||||
|-----------|----------------------|
|
||||
| `geoip` | [GeoIP](./geoip) |
|
||||
| `geosite` | [Geosite](./geosite) |
|
||||
| `rules` | List of [Route Rule](./rule) |
|
||||
|
||||
#### rules
|
||||
|
||||
List of [Route Rule](./rule)
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
List of [Rule Set](/configuration/rule-set)
|
||||
|
||||
#### final
|
||||
|
||||
|
@ -1,5 +1,15 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
# 路由
|
||||
|
||||
!!! quote "sing-box 1.8.0 中的更改"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@ -7,8 +17,8 @@
|
||||
"route": {
|
||||
"geoip": {},
|
||||
"geosite": {},
|
||||
"ip_rules": [],
|
||||
"rules": [],
|
||||
"rule_set": [],
|
||||
"final": "",
|
||||
"auto_detect_interface": false,
|
||||
"override_android_vpn": false,
|
||||
@ -21,10 +31,20 @@
|
||||
### 字段
|
||||
|
||||
| 键 | 格式 |
|
||||
|------------|-------------------------|
|
||||
|------------|-----------------------------------|
|
||||
| `geoip` | [GeoIP](./geoip) |
|
||||
| `geosite` | [GeoSite](./geosite) |
|
||||
| `rules` | 一组 [路由规则](./rule) |
|
||||
| `geosite` | [Geosite](./geosite) |
|
||||
|
||||
|
||||
#### rule
|
||||
|
||||
一组 [路由规则](./rule)。
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
一组 [规则集](/configuration/rule-set)。
|
||||
|
||||
#### final
|
||||
|
||||
|
@ -1,3 +1,15 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.8.0"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
||||
:material-delete-clock: [source_geoip](#source_geoip)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
@ -89,6 +101,10 @@
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
],
|
||||
"invert": false,
|
||||
"outbound": "direct"
|
||||
},
|
||||
@ -160,23 +176,35 @@ Match domain using regular expression.
|
||||
|
||||
#### geosite
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set).
|
||||
|
||||
Match geosite.
|
||||
|
||||
#### source_geoip
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set).
|
||||
|
||||
Match source geoip.
|
||||
|
||||
#### geoip
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set).
|
||||
|
||||
Match geoip.
|
||||
|
||||
#### source_ip_cidr
|
||||
|
||||
Match source ip cidr.
|
||||
Match source IP CIDR.
|
||||
|
||||
#### ip_cidr
|
||||
|
||||
Match ip cidr.
|
||||
Match IP CIDR.
|
||||
|
||||
#### source_port
|
||||
|
||||
@ -250,6 +278,18 @@ Match WiFi SSID.
|
||||
|
||||
Match WiFi BSSID.
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
Match [Rule Set](/configuration/route/#rule_set).
|
||||
|
||||
#### rule_set_ipcidr_match_source
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
Make `ipcidr` in rule sets match the source IP.
|
||||
|
||||
#### invert
|
||||
|
||||
Invert match result.
|
||||
@ -276,4 +316,4 @@ Tag of the target outbound.
|
||||
|
||||
==Required==
|
||||
|
||||
Included default rules.
|
||||
Included rules.
|
||||
|
@ -1,3 +1,15 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.8.0 中的更改"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
||||
:material-delete-clock: [source_geoip](#source_geoip)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@ -87,6 +99,10 @@
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
],
|
||||
"invert": false,
|
||||
"outbound": "direct"
|
||||
},
|
||||
@ -158,14 +174,26 @@
|
||||
|
||||
#### geosite
|
||||
|
||||
匹配 GeoSite。
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。
|
||||
|
||||
匹配 Geosite。
|
||||
|
||||
#### source_geoip
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。
|
||||
|
||||
匹配源 GeoIP。
|
||||
|
||||
#### geoip
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。
|
||||
|
||||
匹配 GeoIP。
|
||||
|
||||
#### source_ip_cidr
|
||||
@ -248,6 +276,18 @@
|
||||
|
||||
匹配 WiFi BSSID。
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
匹配[规则集](/zh/configuration/route/#rule_set)。
|
||||
|
||||
#### rule_set_ipcidr_match_source
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
使规则集中的 `ipcidr` 规则匹配源 IP。
|
||||
|
||||
#### invert
|
||||
|
||||
反选匹配结果。
|
||||
@ -274,4 +314,4 @@
|
||||
|
||||
==必填==
|
||||
|
||||
包括的默认规则。
|
||||
包括的规则。
|
207
docs/configuration/rule-set/headless-rule.md
Normal file
207
docs/configuration/rule-set/headless-rule.md
Normal file
@ -0,0 +1,207 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
### Structure
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"query_type": [
|
||||
"A",
|
||||
"HTTPS",
|
||||
32768
|
||||
],
|
||||
"network": [
|
||||
"tcp"
|
||||
],
|
||||
"domain": [
|
||||
"test.com"
|
||||
],
|
||||
"domain_suffix": [
|
||||
".cn"
|
||||
],
|
||||
"domain_keyword": [
|
||||
"test"
|
||||
],
|
||||
"domain_regex": [
|
||||
"^stun\\..+"
|
||||
],
|
||||
"source_ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
"source_port_range": [
|
||||
"1000:2000",
|
||||
":3000",
|
||||
"4000:"
|
||||
],
|
||||
"port": [
|
||||
80,
|
||||
443
|
||||
],
|
||||
"port_range": [
|
||||
"1000:2000",
|
||||
":3000",
|
||||
"4000:"
|
||||
],
|
||||
"process_name": [
|
||||
"curl"
|
||||
],
|
||||
"process_path": [
|
||||
"/usr/bin/curl"
|
||||
],
|
||||
"package_name": [
|
||||
"com.termux"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"invert": false
|
||||
},
|
||||
{
|
||||
"type": "logical",
|
||||
"mode": "and",
|
||||
"rules": [],
|
||||
"invert": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
!!! note ""
|
||||
|
||||
You can ignore the JSON Array [] tag when the content is only one item
|
||||
|
||||
### Default Fields
|
||||
|
||||
!!! note ""
|
||||
|
||||
The default rule uses the following matching logic:
|
||||
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) &&
|
||||
(`port` || `port_range`) &&
|
||||
(`source_port` || `source_port_range`) &&
|
||||
`other fields`
|
||||
|
||||
#### query_type
|
||||
|
||||
DNS query type. Values can be integers or type name strings.
|
||||
|
||||
#### network
|
||||
|
||||
`tcp` or `udp`.
|
||||
|
||||
#### domain
|
||||
|
||||
Match full domain.
|
||||
|
||||
#### domain_suffix
|
||||
|
||||
Match domain suffix.
|
||||
|
||||
#### domain_keyword
|
||||
|
||||
Match domain using keyword.
|
||||
|
||||
#### domain_regex
|
||||
|
||||
Match domain using regular expression.
|
||||
|
||||
#### source_ip_cidr
|
||||
|
||||
Match source IP CIDR.
|
||||
|
||||
#### ip_cidr
|
||||
|
||||
!!! info ""
|
||||
|
||||
`ip_cidr` is an alias for `source_ip_cidr` when the Rule Set is used in DNS rules or `rule_set_ipcidr_match_source` enabled in route rules.
|
||||
|
||||
Match IP CIDR.
|
||||
|
||||
#### source_port
|
||||
|
||||
Match source port.
|
||||
|
||||
#### source_port_range
|
||||
|
||||
Match source port range.
|
||||
|
||||
#### port
|
||||
|
||||
Match port.
|
||||
|
||||
#### port_range
|
||||
|
||||
Match port range.
|
||||
|
||||
#### process_name
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, Windows, and macOS.
|
||||
|
||||
Match process name.
|
||||
|
||||
#### process_path
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, Windows, and macOS.
|
||||
|
||||
Match process path.
|
||||
|
||||
#### package_name
|
||||
|
||||
Match android package name.
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported in graphical clients on Android and iOS.
|
||||
|
||||
Match WiFi SSID.
|
||||
|
||||
#### wifi_bssid
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported in graphical clients on Android and iOS.
|
||||
|
||||
Match WiFi BSSID.
|
||||
|
||||
#### invert
|
||||
|
||||
Invert match result.
|
||||
|
||||
### Logical Fields
|
||||
|
||||
#### type
|
||||
|
||||
`logical`
|
||||
|
||||
#### mode
|
||||
|
||||
==Required==
|
||||
|
||||
`and` or `or`
|
||||
|
||||
#### rules
|
||||
|
||||
==Required==
|
||||
|
||||
Included rules.
|
97
docs/configuration/rule-set/index.md
Normal file
97
docs/configuration/rule-set/index.md
Normal file
@ -0,0 +1,97 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
# Rule Set
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "",
|
||||
"tag": "",
|
||||
"format": "",
|
||||
|
||||
... // Typed Fields
|
||||
}
|
||||
```
|
||||
|
||||
#### Local Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "local",
|
||||
|
||||
...
|
||||
|
||||
"path": ""
|
||||
}
|
||||
```
|
||||
|
||||
#### Remote Structure
|
||||
|
||||
!!! info ""
|
||||
|
||||
Remote rule-set will be cached if `experimental.cache_file.enabled`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "remote",
|
||||
|
||||
...,
|
||||
|
||||
"url": "",
|
||||
"download_detour": "",
|
||||
"update_interval": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### type
|
||||
|
||||
==Required==
|
||||
|
||||
Type of Rule Set, `local` or `remote`.
|
||||
|
||||
#### tag
|
||||
|
||||
==Required==
|
||||
|
||||
Tag of Rule Set.
|
||||
|
||||
#### format
|
||||
|
||||
==Required==
|
||||
|
||||
Format of Rule Set, `source` or `binary`.
|
||||
|
||||
### Local Fields
|
||||
|
||||
#### path
|
||||
|
||||
==Required==
|
||||
|
||||
File path of Rule Set.
|
||||
|
||||
### Remote Fields
|
||||
|
||||
#### url
|
||||
|
||||
==Required==
|
||||
|
||||
Download URL of Rule Set.
|
||||
|
||||
#### download_detour
|
||||
|
||||
Tag of the outbound to download rule-set.
|
||||
|
||||
Default outbound will be used if empty.
|
||||
|
||||
#### update_interval
|
||||
|
||||
Update interval of Rule Set.
|
||||
|
||||
`1d` will be used if empty.
|
34
docs/configuration/rule-set/source-format.md
Normal file
34
docs/configuration/rule-set/source-format.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
# Source Format
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"rules": []
|
||||
}
|
||||
```
|
||||
|
||||
### Compile
|
||||
|
||||
Use `sing-box rule-set compile [--output <file-name>.srs] <file-name>.json` to compile source to binary rule-set.
|
||||
|
||||
### Fields
|
||||
|
||||
#### version
|
||||
|
||||
==Required==
|
||||
|
||||
Version of Rule Set, must be `1`.
|
||||
|
||||
#### rules
|
||||
|
||||
==Required==
|
||||
|
||||
List of [Headless Rule](./headless-rule.md).
|
187
docs/migration.md
Normal file
187
docs/migration.md
Normal file
@ -0,0 +1,187 @@
|
||||
---
|
||||
icon: material/arrange-bring-forward
|
||||
---
|
||||
|
||||
# Migration
|
||||
|
||||
## 1.8.0
|
||||
|
||||
!!! warning "Unstable"
|
||||
|
||||
This version is still under development, and the following migration guide may be changed in the future.
|
||||
|
||||
### :material-close-box: Migrate cache file from Clash API to independent options
|
||||
|
||||
!!! info "Reference"
|
||||
|
||||
[Clash API](/configuration/experimental/clash-api) /
|
||||
[Cache File](/configuration/experimental/cache-file)
|
||||
|
||||
=== ":material-card-remove: Deprecated"
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"clash_api": {
|
||||
"cache_file": "cache.db", // default value
|
||||
"cahce_id": "my_profile2",
|
||||
"store_mode": true,
|
||||
"store_selected": true,
|
||||
"store_fakeip": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: New"
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental" : {
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"path": "cache.db", // default value
|
||||
"cache_id": "my_profile2",
|
||||
"store_fakeip": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### :material-checkbox-intermediate: Migrate GeoIP to rule sets
|
||||
|
||||
!!! info "Reference"
|
||||
|
||||
[GeoIP](/configuration/route/geoip) /
|
||||
[Route](/configuration/route) /
|
||||
[Route Rule](/configuration/route/rule) /
|
||||
[DNS Rule](/configuration/dns/rule) /
|
||||
[Rule Set](/configuration/rule-set)
|
||||
|
||||
!!! tip
|
||||
|
||||
`sing-box geoip` commands can help you convert custom GeoIP into rule sets.
|
||||
|
||||
=== ":material-card-remove: Deprecated"
|
||||
|
||||
```json
|
||||
{
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"geoip": "cn",
|
||||
"outbound": "direct"
|
||||
},
|
||||
{
|
||||
"source_geoip": "cn",
|
||||
"outbound": "block"
|
||||
}
|
||||
],
|
||||
"geoip": {
|
||||
"download_detour": "proxy"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: New"
|
||||
|
||||
```json
|
||||
{
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"rule_set": "geoip-cn",
|
||||
"outbound": "direct"
|
||||
},
|
||||
{
|
||||
"rule_set": "geoip-us",
|
||||
"rule_set_ipcidr_match_source": true,
|
||||
"outbound": "block"
|
||||
}
|
||||
],
|
||||
"rule_set": [
|
||||
{
|
||||
"tag": "geoip-cn",
|
||||
"type": "remote",
|
||||
"format": "binary",
|
||||
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
|
||||
"download_detour": "proxy"
|
||||
},
|
||||
{
|
||||
"tag": "geoip-us",
|
||||
"type": "remote",
|
||||
"format": "binary",
|
||||
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs",
|
||||
"download_detour": "proxy"
|
||||
}
|
||||
]
|
||||
},
|
||||
"experimental": {
|
||||
"cache_file": {
|
||||
"enabled": true // required to save Rule Set cache
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### :material-checkbox-intermediate: Migrate Geosite to rule sets
|
||||
|
||||
!!! info "Reference"
|
||||
|
||||
[Geosite](/configuration/route/geosite) /
|
||||
[Route](/configuration/route) /
|
||||
[Route Rule](/configuration/route/rule) /
|
||||
[DNS Rule](/configuration/dns/rule) /
|
||||
[Rule Set](/configuration/rule-set)
|
||||
|
||||
!!! tip
|
||||
|
||||
`sing-box geosite` commands can help you convert custom Geosite into rule sets.
|
||||
|
||||
=== ":material-card-remove: Deprecated"
|
||||
|
||||
```json
|
||||
{
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"geosite": "cn",
|
||||
"outbound": "direct"
|
||||
}
|
||||
],
|
||||
"geosite": {
|
||||
"download_detour": "proxy"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: New"
|
||||
|
||||
```json
|
||||
{
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"rule_set": "geosite-cn",
|
||||
"outbound": "direct"
|
||||
}
|
||||
],
|
||||
"rule_set": [
|
||||
{
|
||||
"tag": "geosite-cn",
|
||||
"type": "remote",
|
||||
"format": "binary",
|
||||
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
|
||||
"download_detour": "proxy"
|
||||
}
|
||||
]
|
||||
},
|
||||
"experimental": {
|
||||
"cache_file": {
|
||||
"enabled": true // required to save Rule Set cache
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
@ -22,11 +22,13 @@ var (
|
||||
bucketSelected = []byte("selected")
|
||||
bucketExpand = []byte("group_expand")
|
||||
bucketMode = []byte("clash_mode")
|
||||
bucketRuleSet = []byte("rule_set")
|
||||
|
||||
bucketNameList = []string{
|
||||
string(bucketSelected),
|
||||
string(bucketExpand),
|
||||
string(bucketMode),
|
||||
string(bucketRuleSet),
|
||||
}
|
||||
|
||||
cacheIDDefault = []byte("default")
|
||||
@ -49,13 +51,19 @@ type CacheFile struct {
|
||||
}
|
||||
|
||||
func NewCacheFile(ctx context.Context, options option.CacheFileOptions) *CacheFile {
|
||||
var path string
|
||||
if options.Path != "" {
|
||||
path = filemanager.BasePath(ctx, options.Path)
|
||||
} else {
|
||||
path = "cache.db"
|
||||
}
|
||||
var cacheIDBytes []byte
|
||||
if options.CacheID != "" {
|
||||
cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...)
|
||||
}
|
||||
return &CacheFile{
|
||||
ctx: ctx,
|
||||
path: options.Path,
|
||||
path: path,
|
||||
cacheID: cacheIDBytes,
|
||||
storeFakeIP: options.StoreFakeIP,
|
||||
saveDomain: make(map[netip.Addr]string),
|
||||
@ -131,6 +139,9 @@ func (c *CacheFile) Start() error {
|
||||
}
|
||||
|
||||
func (c *CacheFile) Close() error {
|
||||
if c.DB == nil {
|
||||
return nil
|
||||
}
|
||||
return c.DB.Close()
|
||||
}
|
||||
|
||||
@ -248,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)
|
||||
})
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
|
||||
err := c.DB.Batch(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket(bucketFakeIP)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
return os.ErrNotExist
|
||||
}
|
||||
metadataBinary := bucket.Get(keyMetadata)
|
||||
if len(metadataBinary) == 0 {
|
||||
|
@ -100,8 +100,10 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite
|
||||
allProxies = append(allProxies, detour.Tag())
|
||||
}
|
||||
|
||||
defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag()
|
||||
if defaultTag == "" {
|
||||
var defaultTag string
|
||||
if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
|
||||
defaultTag = defaultOutbound.Tag()
|
||||
} else {
|
||||
defaultTag = allProxies[0]
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,11 @@ func (s *Server) downloadExternalUI() error {
|
||||
}
|
||||
detour = outbound
|
||||
} else {
|
||||
detour = s.router.DefaultOutbound(N.NetworkTCP)
|
||||
outbound, err := s.router.DefaultOutbound(N.NetworkTCP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
detour = outbound
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
|
@ -94,7 +94,9 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
|
||||
var chain []string
|
||||
var next string
|
||||
if rule == nil {
|
||||
next = router.DefaultOutbound(N.NetworkTCP).Tag()
|
||||
if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
|
||||
next = defaultOutbound.Tag()
|
||||
}
|
||||
} else {
|
||||
next = rule.Outbound()
|
||||
}
|
||||
@ -181,7 +183,9 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
|
||||
var chain []string
|
||||
var next string
|
||||
if rule == nil {
|
||||
next = router.DefaultOutbound(N.NetworkUDP).Tag()
|
||||
if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
|
||||
next = defaultOutbound.Tag()
|
||||
}
|
||||
} else {
|
||||
next = rule.Outbound()
|
||||
}
|
||||
|
10
go.mod
10
go.mod
@ -12,7 +12,7 @@ require (
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/gofrs/uuid/v5 v5.0.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231126010706-b0416c0f187a
|
||||
github.com/libdns/alidns v1.0.3
|
||||
github.com/libdns/cloudflare v0.1.0
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
@ -26,7 +26,7 @@ require (
|
||||
github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930
|
||||
github.com/sagernet/quic-go v0.40.0
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||
github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc
|
||||
github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be
|
||||
github.com/sagernet/sing-dns v0.1.11
|
||||
github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07
|
||||
github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203
|
||||
@ -44,9 +44,9 @@ require (
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.uber.org/zap v1.26.0
|
||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925
|
||||
golang.org/x/crypto v0.15.0
|
||||
golang.org/x/net v0.18.0
|
||||
golang.org/x/sys v0.14.0
|
||||
golang.org/x/crypto v0.16.0
|
||||
golang.org/x/net v0.19.0
|
||||
golang.org/x/sys v0.15.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.31.0
|
||||
|
22
go.sum
22
go.sum
@ -47,8 +47,8 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c h1:PgxFEySCI41sH0mB7/2XswdXbUykQsRUGod8Rn+NubM=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231126010706-b0416c0f187a h1:biHpNhTkyeXNEzLqTOM6DkIkMedNh/j+ft+POj0Xcko=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231126010706-b0416c0f187a/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
@ -110,8 +110,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||
github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
|
||||
github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
|
||||
github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc h1:vESVuxHgbd2EzHxd+TYTpNACIEGBOhp5n3KG7bgbcws=
|
||||
github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be h1:FigAM9kq7RRXmHvgn8w2a8tqCY5CMV5GIk0id84dI0o=
|
||||
github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE=
|
||||
github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE=
|
||||
github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM=
|
||||
@ -169,16 +169,16 @@ go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0Eq
|
||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
||||
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -189,10 +189,10 @@ golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
|
26
mkdocs.yml
26
mkdocs.yml
@ -32,12 +32,16 @@ theme:
|
||||
- content.code.copy
|
||||
- content.code.select
|
||||
- content.code.annotate
|
||||
icon:
|
||||
admonition:
|
||||
question: material/new-box
|
||||
nav:
|
||||
- Home:
|
||||
- index.md
|
||||
- Change Log: changelog.md
|
||||
- Migration: migration.md
|
||||
- Deprecated: deprecated.md
|
||||
- Support: support.md
|
||||
- Change Log: changelog.md
|
||||
- Installation:
|
||||
- Package Manager: installation/package-manager.md
|
||||
- Docker: installation/docker.md
|
||||
@ -79,8 +83,15 @@ nav:
|
||||
- Geosite: configuration/route/geosite.md
|
||||
- Route Rule: configuration/route/rule.md
|
||||
- Protocol Sniff: configuration/route/sniff.md
|
||||
- Rule Set:
|
||||
- configuration/rule-set/index.md
|
||||
- Source Format: configuration/rule-set/source-format.md
|
||||
- Headless Rule: configuration/rule-set/headless-rule.md
|
||||
- Experimental:
|
||||
- configuration/experimental/index.md
|
||||
- Cache File: configuration/experimental/cache-file.md
|
||||
- Clash API: configuration/experimental/clash-api.md
|
||||
- V2Ray API: configuration/experimental/v2ray-api.md
|
||||
- Shared:
|
||||
- Listen Fields: configuration/shared/listen.md
|
||||
- Dial Fields: configuration/shared/dial.md
|
||||
@ -180,9 +191,10 @@ plugins:
|
||||
name: 简体中文
|
||||
nav_translations:
|
||||
Home: 开始
|
||||
Change Log: 更新日志
|
||||
Migration: 迁移指南
|
||||
Deprecated: 废弃功能列表
|
||||
Support: 支持
|
||||
Change Log: 更新日志
|
||||
|
||||
Installation: 安装
|
||||
Package Manager: 包管理器
|
||||
@ -203,6 +215,10 @@ plugins:
|
||||
Route Rule: 路由规则
|
||||
Protocol Sniff: 协议探测
|
||||
|
||||
Rule Set: 规则集
|
||||
Source Format: 源文件格式
|
||||
Headless Rule: 无头规则
|
||||
|
||||
Experimental: 实验性
|
||||
|
||||
Shared: 通用
|
||||
@ -215,10 +231,6 @@ plugins:
|
||||
Inbound: 入站
|
||||
Outbound: 出站
|
||||
|
||||
FAQ: 常见问题
|
||||
Known Issues: 已知问题
|
||||
Examples: 示例
|
||||
Linux Server Installation: Linux 服务器安装
|
||||
DNS Hijack: DNS 劫持
|
||||
Manual: 手册
|
||||
reconfigure_material: true
|
||||
reconfigure_search: true
|
@ -23,16 +23,16 @@ type ClashAPIOptions struct {
|
||||
DefaultMode string `json:"default_mode,omitempty"`
|
||||
ModeList []string `json:"-"`
|
||||
|
||||
// Deprecated: migrated to global cache file
|
||||
CacheFile string `json:"cache_file,omitempty"`
|
||||
// Deprecated: migrated to global cache file
|
||||
CacheID string `json:"cache_id,omitempty"`
|
||||
// Deprecated: migrated to global cache file
|
||||
StoreMode bool `json:"store_mode,omitempty"`
|
||||
// Deprecated: migrated to global cache file
|
||||
StoreSelected bool `json:"store_selected,omitempty"`
|
||||
// Deprecated: migrated to global cache file
|
||||
StoreFakeIP bool `json:"store_fakeip,omitempty"`
|
||||
// Deprecated: migrated to global cache file
|
||||
CacheFile string `json:"cache_file,omitempty"`
|
||||
// Deprecated: migrated to global cache file
|
||||
CacheID string `json:"cache_id,omitempty"`
|
||||
}
|
||||
|
||||
type V2RayAPIOptions struct {
|
||||
|
@ -4,6 +4,7 @@ type RouteOptions struct {
|
||||
GeoIP *GeoIPOptions `json:"geoip,omitempty"`
|
||||
Geosite *GeositeOptions `json:"geosite,omitempty"`
|
||||
Rules []Rule `json:"rules,omitempty"`
|
||||
RuleSet []RuleSet `json:"rule_set,omitempty"`
|
||||
Final string `json:"final,omitempty"`
|
||||
FindProcess bool `json:"find_process,omitempty"`
|
||||
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
|
||||
|
@ -91,6 +91,8 @@ type DefaultRule struct {
|
||||
ClashMode string `json:"clash_mode,omitempty"`
|
||||
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
RuleSet Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
Outbound string `json:"outbound,omitempty"`
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ type DefaultDNSRule struct {
|
||||
ClashMode string `json:"clash_mode,omitempty"`
|
||||
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
RuleSet Listable[string] `json:"rule_set,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
Server string `json:"server,omitempty"`
|
||||
DisableCache bool `json:"disable_cache,omitempty"`
|
||||
|
230
option/rule_set.go
Normal file
230
option/rule_set.go
Normal file
@ -0,0 +1,230 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/domain"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
type _RuleSet struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag"`
|
||||
Format string `json:"format"`
|
||||
LocalOptions LocalRuleSet `json:"-"`
|
||||
RemoteOptions RemoteRuleSet `json:"-"`
|
||||
}
|
||||
|
||||
type RuleSet _RuleSet
|
||||
|
||||
func (r RuleSet) MarshalJSON() ([]byte, error) {
|
||||
var v any
|
||||
switch r.Type {
|
||||
case C.RuleSetTypeLocal:
|
||||
v = r.LocalOptions
|
||||
case C.RuleSetTypeRemote:
|
||||
v = r.RemoteOptions
|
||||
default:
|
||||
return nil, E.New("unknown rule set type: " + r.Type)
|
||||
}
|
||||
return MarshallObjects((_RuleSet)(r), v)
|
||||
}
|
||||
|
||||
func (r *RuleSet) UnmarshalJSON(bytes []byte) error {
|
||||
err := json.Unmarshal(bytes, (*_RuleSet)(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Tag == "" {
|
||||
return E.New("missing rule_set.[].tag")
|
||||
}
|
||||
switch r.Format {
|
||||
case "":
|
||||
return E.New("missing rule_set.[].format")
|
||||
case C.RuleSetFormatSource, C.RuleSetFormatBinary:
|
||||
default:
|
||||
return E.New("unknown rule set format: " + r.Format)
|
||||
}
|
||||
var v any
|
||||
switch r.Type {
|
||||
case C.RuleSetTypeLocal:
|
||||
v = &r.LocalOptions
|
||||
case C.RuleSetTypeRemote:
|
||||
v = &r.RemoteOptions
|
||||
case "":
|
||||
return E.New("missing rule_set.[].type")
|
||||
default:
|
||||
return E.New("unknown rule set type: " + r.Type)
|
||||
}
|
||||
err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v)
|
||||
if err != nil {
|
||||
return E.Cause(err, "rule set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LocalRuleSet struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
type RemoteRuleSet struct {
|
||||
URL string `json:"url"`
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
UpdateInterval Duration `json:"update_interval,omitempty"`
|
||||
}
|
||||
|
||||
type _HeadlessRule struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
DefaultOptions DefaultHeadlessRule `json:"-"`
|
||||
LogicalOptions LogicalHeadlessRule `json:"-"`
|
||||
}
|
||||
|
||||
type HeadlessRule _HeadlessRule
|
||||
|
||||
func (r HeadlessRule) MarshalJSON() ([]byte, error) {
|
||||
var v any
|
||||
switch r.Type {
|
||||
case C.RuleTypeDefault:
|
||||
r.Type = ""
|
||||
v = r.DefaultOptions
|
||||
case C.RuleTypeLogical:
|
||||
v = r.LogicalOptions
|
||||
default:
|
||||
return nil, E.New("unknown rule type: " + r.Type)
|
||||
}
|
||||
return MarshallObjects((_HeadlessRule)(r), v)
|
||||
}
|
||||
|
||||
func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error {
|
||||
err := json.Unmarshal(bytes, (*_HeadlessRule)(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v any
|
||||
switch r.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
r.Type = C.RuleTypeDefault
|
||||
v = &r.DefaultOptions
|
||||
case C.RuleTypeLogical:
|
||||
v = &r.LogicalOptions
|
||||
default:
|
||||
return E.New("unknown rule type: " + r.Type)
|
||||
}
|
||||
err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v)
|
||||
if err != nil {
|
||||
return E.Cause(err, "route rule-set rule")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r HeadlessRule) IsValid() bool {
|
||||
switch r.Type {
|
||||
case C.RuleTypeDefault, "":
|
||||
return r.DefaultOptions.IsValid()
|
||||
case C.RuleTypeLogical:
|
||||
return r.LogicalOptions.IsValid()
|
||||
default:
|
||||
panic("unknown rule type: " + r.Type)
|
||||
}
|
||||
}
|
||||
|
||||
type DefaultHeadlessRule struct {
|
||||
QueryType Listable[DNSQueryType] `json:"query_type,omitempty"`
|
||||
Network Listable[string] `json:"network,omitempty"`
|
||||
Domain Listable[string] `json:"domain,omitempty"`
|
||||
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
|
||||
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
|
||||
SourcePort Listable[uint16] `json:"source_port,omitempty"`
|
||||
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange Listable[string] `json:"port_range,omitempty"`
|
||||
ProcessName Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath Listable[string] `json:"process_path,omitempty"`
|
||||
PackageName Listable[string] `json:"package_name,omitempty"`
|
||||
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
|
||||
DomainMatcher *domain.Matcher `json:"-"`
|
||||
SourceIPSet *netipx.IPSet `json:"-"`
|
||||
IPSet *netipx.IPSet `json:"-"`
|
||||
}
|
||||
|
||||
func (r DefaultHeadlessRule) IsValid() bool {
|
||||
var defaultValue DefaultHeadlessRule
|
||||
defaultValue.Invert = r.Invert
|
||||
return !reflect.DeepEqual(r, defaultValue)
|
||||
}
|
||||
|
||||
type LogicalHeadlessRule struct {
|
||||
Mode string `json:"mode"`
|
||||
Rules []HeadlessRule `json:"rules,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
}
|
||||
|
||||
func (r LogicalHeadlessRule) IsValid() bool {
|
||||
return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid)
|
||||
}
|
||||
|
||||
type _PlainRuleSetCompat struct {
|
||||
Version int `json:"version"`
|
||||
Options PlainRuleSet `json:"-"`
|
||||
}
|
||||
|
||||
type PlainRuleSetCompat _PlainRuleSetCompat
|
||||
|
||||
func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) {
|
||||
var v any
|
||||
switch r.Version {
|
||||
case C.RuleSetVersion1:
|
||||
v = r.Options
|
||||
default:
|
||||
return nil, E.New("unknown rule set version: ", r.Version)
|
||||
}
|
||||
return MarshallObjects((_PlainRuleSetCompat)(r), v)
|
||||
}
|
||||
|
||||
func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
|
||||
err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v any
|
||||
switch r.Version {
|
||||
case C.RuleSetVersion1:
|
||||
v = &r.Options
|
||||
case 0:
|
||||
return E.New("missing rule set version")
|
||||
default:
|
||||
return E.New("unknown rule set version: ", r.Version)
|
||||
}
|
||||
err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v)
|
||||
if err != nil {
|
||||
return E.Cause(err, "rule set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r PlainRuleSetCompat) Upgrade() PlainRuleSet {
|
||||
var result PlainRuleSet
|
||||
switch r.Version {
|
||||
case C.RuleSetVersion1:
|
||||
result = r.Options
|
||||
default:
|
||||
panic("unknown rule set version: " + F.ToString(r.Version))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type PlainRuleSet struct {
|
||||
Rules []HeadlessRule `json:"rules,omitempty"`
|
||||
}
|
@ -174,6 +174,14 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error {
|
||||
|
||||
type DNSQueryType uint16
|
||||
|
||||
func (t DNSQueryType) String() string {
|
||||
typeName, loaded := mDNS.TypeToString[uint16(t)]
|
||||
if loaded {
|
||||
return typeName
|
||||
}
|
||||
return F.ToString(uint16(t))
|
||||
}
|
||||
|
||||
func (t DNSQueryType) MarshalJSON() ([]byte, error) {
|
||||
typeName, loaded := mDNS.TypeToString[uint16(t)]
|
||||
if loaded {
|
||||
|
@ -67,6 +67,8 @@ type Router struct {
|
||||
dnsClient *dns.Client
|
||||
defaultDomainStrategy dns.DomainStrategy
|
||||
dnsRules []adapter.DNSRule
|
||||
ruleSets []adapter.RuleSet
|
||||
ruleSetMap map[string]adapter.RuleSet
|
||||
defaultTransport dns.Transport
|
||||
transports []dns.Transport
|
||||
transportMap map[string]dns.Transport
|
||||
@ -106,6 +108,7 @@ func NewRouter(
|
||||
outboundByTag: make(map[string]adapter.Outbound),
|
||||
rules: make([]adapter.Rule, 0, len(options.Rules)),
|
||||
dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)),
|
||||
ruleSetMap: make(map[string]adapter.RuleSet),
|
||||
needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule),
|
||||
needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule),
|
||||
geoIPOptions: common.PtrValueOrDefault(options.GeoIP),
|
||||
@ -127,19 +130,27 @@ func NewRouter(
|
||||
Logger: router.dnsLogger,
|
||||
})
|
||||
for i, ruleOptions := range options.Rules {
|
||||
routeRule, err := NewRule(router, router.logger, ruleOptions)
|
||||
routeRule, err := NewRule(router, router.logger, ruleOptions, true)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse rule[", i, "]")
|
||||
}
|
||||
router.rules = append(router.rules, routeRule)
|
||||
}
|
||||
for i, dnsRuleOptions := range dnsOptions.Rules {
|
||||
dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions)
|
||||
dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions, true)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse dns rule[", i, "]")
|
||||
}
|
||||
router.dnsRules = append(router.dnsRules, dnsRule)
|
||||
}
|
||||
for i, ruleSetOptions := range options.RuleSet {
|
||||
ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse rule-set[", i, "]")
|
||||
}
|
||||
router.ruleSets = append(router.ruleSets, ruleSet)
|
||||
router.ruleSetMap[ruleSetOptions.Tag] = ruleSet
|
||||
}
|
||||
|
||||
transports := make([]dns.Transport, len(dnsOptions.Servers))
|
||||
dummyTransportMap := make(map[string]dns.Transport)
|
||||
@ -479,6 +490,12 @@ func (r *Router) Start() error {
|
||||
if r.needWIFIState {
|
||||
r.updateWIFIState()
|
||||
}
|
||||
for i, ruleSet := range r.ruleSets {
|
||||
err := ruleSet.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize rule-set[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, rule := range r.rules {
|
||||
err := rule.Start()
|
||||
if err != nil {
|
||||
@ -576,11 +593,17 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
return outbound, loaded
|
||||
}
|
||||
|
||||
func (r *Router) DefaultOutbound(network string) adapter.Outbound {
|
||||
func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) {
|
||||
if network == N.NetworkTCP {
|
||||
return r.defaultOutboundForConnection
|
||||
if r.defaultOutboundForConnection == nil {
|
||||
return nil, E.New("missing default outbound for TCP connections")
|
||||
}
|
||||
return r.defaultOutboundForConnection, nil
|
||||
} else {
|
||||
return r.defaultOutboundForPacketConnection
|
||||
if r.defaultOutboundForPacketConnection == nil {
|
||||
return nil, E.New("missing default outbound for UDP connections")
|
||||
}
|
||||
return r.defaultOutboundForPacketConnection, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -588,6 +611,11 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore {
|
||||
return r.fakeIPStore
|
||||
}
|
||||
|
||||
func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) {
|
||||
ruleSet, loaded := r.ruleSetMap[tag]
|
||||
return ruleSet, loaded
|
||||
}
|
||||
|
||||
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||
if metadata.InboundDetour != "" {
|
||||
if metadata.LastInbound == metadata.InboundDetour {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@ -135,7 +136,7 @@ func (r *abstractDefaultRule) String() string {
|
||||
}
|
||||
|
||||
type abstractLogicalRule struct {
|
||||
rules []adapter.Rule
|
||||
rules []adapter.HeadlessRule
|
||||
mode string
|
||||
invert bool
|
||||
outbound string
|
||||
@ -146,7 +147,10 @@ func (r *abstractLogicalRule) Type() string {
|
||||
}
|
||||
|
||||
func (r *abstractLogicalRule) UpdateGeosite() error {
|
||||
for _, rule := range r.rules {
|
||||
for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (adapter.Rule, bool) {
|
||||
rule, loaded := it.(adapter.Rule)
|
||||
return rule, loaded
|
||||
}) {
|
||||
err := rule.UpdateGeosite()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -156,7 +160,10 @@ func (r *abstractLogicalRule) UpdateGeosite() error {
|
||||
}
|
||||
|
||||
func (r *abstractLogicalRule) Start() error {
|
||||
for _, rule := range r.rules {
|
||||
for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) {
|
||||
rule, loaded := it.(common.Starter)
|
||||
return rule, loaded
|
||||
}) {
|
||||
err := rule.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -166,7 +173,10 @@ func (r *abstractLogicalRule) Start() error {
|
||||
}
|
||||
|
||||
func (r *abstractLogicalRule) Close() error {
|
||||
for _, rule := range r.rules {
|
||||
for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) {
|
||||
rule, loaded := it.(io.Closer)
|
||||
return rule, loaded
|
||||
}) {
|
||||
err := rule.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -177,11 +187,11 @@ func (r *abstractLogicalRule) Close() error {
|
||||
|
||||
func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
|
||||
if r.mode == C.LogicalTypeAnd {
|
||||
return common.All(r.rules, func(it adapter.Rule) bool {
|
||||
return common.All(r.rules, func(it adapter.HeadlessRule) bool {
|
||||
return it.Match(metadata)
|
||||
}) != r.invert
|
||||
} else {
|
||||
return common.Any(r.rules, func(it adapter.Rule) bool {
|
||||
return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
|
||||
return it.Match(metadata)
|
||||
}) != r.invert
|
||||
}
|
||||
|
@ -8,13 +8,13 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule) (adapter.Rule, error) {
|
||||
func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) {
|
||||
switch options.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
if !options.DefaultOptions.IsValid() {
|
||||
return nil, E.New("missing conditions")
|
||||
}
|
||||
if options.DefaultOptions.Outbound == "" {
|
||||
if options.DefaultOptions.Outbound == "" && checkOutbound {
|
||||
return nil, E.New("missing outbound field")
|
||||
}
|
||||
return NewDefaultRule(router, logger, options.DefaultOptions)
|
||||
@ -22,7 +22,7 @@ func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rul
|
||||
if !options.LogicalOptions.IsValid() {
|
||||
return nil, E.New("missing conditions")
|
||||
}
|
||||
if options.LogicalOptions.Outbound == "" {
|
||||
if options.LogicalOptions.Outbound == "" && checkOutbound {
|
||||
return nil, E.New("missing outbound field")
|
||||
}
|
||||
return NewLogicalRule(router, logger, options.LogicalOptions)
|
||||
@ -194,6 +194,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.RuleSet) > 0 {
|
||||
item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
@ -206,7 +211,7 @@ type LogicalRule struct {
|
||||
func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) {
|
||||
r := &LogicalRule{
|
||||
abstractLogicalRule{
|
||||
rules: make([]adapter.Rule, len(options.Rules)),
|
||||
rules: make([]adapter.HeadlessRule, len(options.Rules)),
|
||||
invert: options.Invert,
|
||||
outbound: options.Outbound,
|
||||
},
|
||||
@ -220,7 +225,7 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt
|
||||
return nil, E.New("unknown logical mode: ", options.Mode)
|
||||
}
|
||||
for i, subRule := range options.Rules {
|
||||
rule, err := NewRule(router, logger, subRule)
|
||||
rule, err := NewRule(router, logger, subRule, false)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "sub rule[", i, "]")
|
||||
}
|
||||
|
@ -8,13 +8,13 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule) (adapter.DNSRule, error) {
|
||||
func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) {
|
||||
switch options.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
if !options.DefaultOptions.IsValid() {
|
||||
return nil, E.New("missing conditions")
|
||||
}
|
||||
if options.DefaultOptions.Server == "" {
|
||||
if options.DefaultOptions.Server == "" && checkServer {
|
||||
return nil, E.New("missing server field")
|
||||
}
|
||||
return NewDefaultDNSRule(router, logger, options.DefaultOptions)
|
||||
@ -22,7 +22,7 @@ func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.
|
||||
if !options.LogicalOptions.IsValid() {
|
||||
return nil, E.New("missing conditions")
|
||||
}
|
||||
if options.LogicalOptions.Server == "" {
|
||||
if options.LogicalOptions.Server == "" && checkServer {
|
||||
return nil, E.New("missing server field")
|
||||
}
|
||||
return NewLogicalDNSRule(router, logger, options.LogicalOptions)
|
||||
@ -190,6 +190,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.RuleSet) > 0 {
|
||||
item := NewRuleSetItem(router, options.RuleSet, false)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
@ -212,7 +217,7 @@ type LogicalDNSRule struct {
|
||||
func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
|
||||
r := &LogicalDNSRule{
|
||||
abstractLogicalRule: abstractLogicalRule{
|
||||
rules: make([]adapter.Rule, len(options.Rules)),
|
||||
rules: make([]adapter.HeadlessRule, len(options.Rules)),
|
||||
invert: options.Invert,
|
||||
outbound: options.Server,
|
||||
},
|
||||
@ -228,7 +233,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options
|
||||
return nil, E.New("unknown logical mode: ", options.Mode)
|
||||
}
|
||||
for i, subRule := range options.Rules {
|
||||
rule, err := NewDNSRule(router, logger, subRule)
|
||||
rule, err := NewDNSRule(router, logger, subRule, false)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "sub rule[", i, "]")
|
||||
}
|
||||
|
173
route/rule_headless.go
Normal file
173
route/rule_headless.go
Normal 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
|
||||
}
|
@ -31,7 +31,7 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) {
|
||||
builder.Add(addr)
|
||||
continue
|
||||
}
|
||||
return nil, E.Cause(err, "parse ip_cidr [", i, "]")
|
||||
return nil, E.Cause(err, "parse [", i, "]")
|
||||
}
|
||||
var description string
|
||||
if isSource {
|
||||
@ -57,8 +57,23 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem {
|
||||
var description string
|
||||
if isSource {
|
||||
description = "source_ipcidr="
|
||||
} else {
|
||||
description = "ipcidr="
|
||||
}
|
||||
description += "<binary>"
|
||||
return &IPCIDRItem{
|
||||
ipSet: ipSet,
|
||||
isSource: isSource,
|
||||
description: description,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if r.isSource {
|
||||
if r.isSource || metadata.QueryType != 0 || metadata.IPCIDRMatchSource {
|
||||
return r.ipSet.Contains(metadata.Source.Addr)
|
||||
} else {
|
||||
if metadata.Destination.IsIP() {
|
||||
|
@ -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 {
|
||||
var domainHost string
|
||||
if metadata.Domain != "" {
|
||||
|
55
route/rule_item_rule_set.go
Normal file
55
route/rule_item_rule_set.go
Normal 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, " "), "]")
|
||||
}
|
||||
}
|
22
route/rule_set.go
Normal file
22
route/rule_set.go
Normal file
@ -0,0 +1,22 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
69
route/rule_set_local.go
Normal file
69
route/rule_set_local.go
Normal file
@ -0,0 +1,69 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.RuleSet = (*LocalRuleSet)(nil)
|
||||
|
||||
type LocalRuleSet struct {
|
||||
rules []adapter.HeadlessRule
|
||||
}
|
||||
|
||||
func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) {
|
||||
setFile, err := os.Open(options.LocalOptions.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var plainRuleSet option.PlainRuleSet
|
||||
switch options.Format {
|
||||
case C.RuleSetFormatSource, "":
|
||||
var compat option.PlainRuleSetCompat
|
||||
decoder := json.NewDecoder(json.NewCommentFilter(setFile))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&compat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plainRuleSet = compat.Upgrade()
|
||||
case C.RuleSetFormatBinary:
|
||||
plainRuleSet, err = srs.Read(setFile, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, E.New("unknown rule set format: ", options.Format)
|
||||
}
|
||||
rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules))
|
||||
for i, ruleOptions := range plainRuleSet.Rules {
|
||||
rules[i], err = NewHeadlessRule(router, ruleOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||
}
|
||||
}
|
||||
return &LocalRuleSet{rules}, nil
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
|
||||
for _, rule := range s.rules {
|
||||
if rule.Match(metadata) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Close() error {
|
||||
return nil
|
||||
}
|
218
route/rule_set_remote.go
Normal file
218
route/rule_set_remote.go
Normal file
@ -0,0 +1,218 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
|
||||
|
||||
type RemoteRuleSet struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
router adapter.Router
|
||||
logger logger.ContextLogger
|
||||
options option.RuleSet
|
||||
updateInterval time.Duration
|
||||
dialer N.Dialer
|
||||
rules []adapter.HeadlessRule
|
||||
lastUpdated time.Time
|
||||
lastEtag string
|
||||
updateTicker *time.Ticker
|
||||
}
|
||||
|
||||
func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
var updateInterval time.Duration
|
||||
if options.RemoteOptions.UpdateInterval > 0 {
|
||||
updateInterval = time.Duration(options.RemoteOptions.UpdateInterval)
|
||||
} else {
|
||||
updateInterval = 12 * time.Hour
|
||||
}
|
||||
return &RemoteRuleSet{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
router: router,
|
||||
logger: logger,
|
||||
options: options,
|
||||
updateInterval: updateInterval,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
|
||||
for _, rule := range s.rules {
|
||||
if rule.Match(metadata) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) Start() error {
|
||||
var dialer N.Dialer
|
||||
if s.options.RemoteOptions.DownloadDetour != "" {
|
||||
outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour)
|
||||
if !loaded {
|
||||
return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour)
|
||||
}
|
||||
dialer = outbound
|
||||
} else {
|
||||
outbound, err := s.router.DefaultOutbound(N.NetworkTCP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dialer = outbound
|
||||
}
|
||||
s.dialer = dialer
|
||||
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
|
||||
if cacheFile != nil {
|
||||
if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil {
|
||||
err := s.loadBytes(savedSet.Content)
|
||||
if err != nil {
|
||||
return E.Cause(err, "restore cached rule-set")
|
||||
}
|
||||
s.lastUpdated = savedSet.LastUpdated
|
||||
s.lastEtag = savedSet.LastEtag
|
||||
}
|
||||
}
|
||||
if s.lastUpdated.IsZero() || time.Since(s.lastUpdated) > s.updateInterval {
|
||||
err := s.fetchOnce()
|
||||
if err != nil {
|
||||
return E.Cause(err, "fetch rule-set ", s.options.Tag)
|
||||
}
|
||||
}
|
||||
s.updateTicker = time.NewTicker(s.updateInterval)
|
||||
go s.loopUpdate()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) loadBytes(content []byte) error {
|
||||
var (
|
||||
plainRuleSet option.PlainRuleSet
|
||||
err error
|
||||
)
|
||||
switch s.options.Format {
|
||||
case C.RuleSetFormatSource, "":
|
||||
var compat option.PlainRuleSetCompat
|
||||
decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content)))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&compat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plainRuleSet = compat.Upgrade()
|
||||
case C.RuleSetFormatBinary:
|
||||
plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return E.New("unknown rule set format: ", s.options.Format)
|
||||
}
|
||||
rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules))
|
||||
for i, ruleOptions := range plainRuleSet.Rules {
|
||||
rules[i], err = NewHeadlessRule(s.router, ruleOptions)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||
}
|
||||
}
|
||||
s.rules = rules
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) loopUpdate() {
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-s.updateTicker.C:
|
||||
err := s.fetchOnce()
|
||||
if err != nil {
|
||||
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) fetchOnce() error {
|
||||
s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL)
|
||||
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))
|
||||
},
|
||||
},
|
||||
}
|
||||
defer httpClient.CloseIdleConnections()
|
||||
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(s.ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch response.StatusCode {
|
||||
case http.StatusOK:
|
||||
case http.StatusNotModified:
|
||||
s.logger.Info("update rule-set ", s.options.Tag, ": not modified")
|
||||
return nil
|
||||
default:
|
||||
return E.New("unexpected status: ", response.Status)
|
||||
}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
response.Body.Close()
|
||||
return err
|
||||
}
|
||||
err = s.loadBytes(content)
|
||||
if err != nil {
|
||||
response.Body.Close()
|
||||
return err
|
||||
}
|
||||
response.Body.Close()
|
||||
eTagHeader := response.Header.Get("Etag")
|
||||
if eTagHeader != "" {
|
||||
s.lastEtag = eTagHeader
|
||||
}
|
||||
s.lastUpdated = time.Now()
|
||||
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
|
||||
if cacheFile != nil {
|
||||
err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{
|
||||
LastUpdated: s.lastUpdated,
|
||||
Content: content,
|
||||
LastEtag: s.lastEtag,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("save rule-set cache: ", err)
|
||||
}
|
||||
}
|
||||
s.logger.Info("updated rule-set ", s.options.Tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) Close() error {
|
||||
s.updateTicker.Stop()
|
||||
s.cancel()
|
||||
return nil
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user