Compare commits

...

10 Commits

Author SHA1 Message Date
世界
5f40f1d241
documentation: Bump version 2023-12-01 17:26:06 +08:00
世界
63f936d15f
Update documentation 2023-12-01 17:26:06 +08:00
世界
bd97437e59
Independent source_ip_is_private and ip_is_private rules 2023-12-01 17:26:06 +08:00
世界
2d02ef30c5
Add rule-set 2023-12-01 17:26:06 +08:00
世界
d031aeba76
Update buffer usage 2023-12-01 15:01:09 +08:00
世界
376f119d06
Allow nested logical rules 2023-12-01 15:01:08 +08:00
世界
a99dac8594
Migrate to independent cache file 2023-12-01 15:01:08 +08:00
世界
c9ab3aca31
documentation: Bump version 2023-12-01 14:10:52 +08:00
世界
62a73207b4
Fix URLTest group early start 2023-12-01 13:41:59 +08:00
世界
38f3132bbd
Fix deadline usage 2023-12-01 13:41:56 +08:00
93 changed files with 4477 additions and 803 deletions

1
.gitignore vendored
View File

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

View File

@ -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 {
@ -13,22 +18,83 @@ type ClashServer interface {
PreStarter
Mode() string
ModeList() []string
StoreSelected() bool
StoreFakeIP() bool
CacheFile() ClashCacheFile
HistoryStorage() *urltest.HistoryStorage
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
}
type ClashCacheFile interface {
type CacheFile interface {
Service
PreStarter
StoreFakeIP() bool
FakeIPStorage
LoadMode() string
StoreMode(mode string) error
LoadSelected(group string) string
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 {

View File

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

View File

@ -2,12 +2,14 @@ package adapter
import (
"context"
"net/http"
"net/netip"
"github.com/sagernet/sing-box/common/geoip"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
mdns "github.com/miekg/dns"
@ -18,7 +20,7 @@ type Router interface {
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
DefaultOutbound(network string) Outbound
DefaultOutbound(network string) (Outbound, error)
FakeIPStore() FakeIPStore
@ -27,6 +29,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 +65,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 +84,17 @@ type DNSRule interface {
RewriteTTL() *uint32
}
type RuleSet interface {
StartContext(ctx context.Context, startContext RuleSetStartContext) error
Close() error
HeadlessRule
}
type RuleSetStartContext interface {
HTTPClient(detour string, dialer N.Dialer) *http.Client
Close()
}
type InterfaceUpdateListener interface {
InterfaceUpdated()
}

53
box.go
View File

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

View File

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

@ -0,0 +1,43 @@
package main
import (
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/oschwald/maxminddb-golang"
"github.com/spf13/cobra"
)
var (
geoipReader *maxminddb.Reader
commandGeoIPFlagFile string
)
var commandGeoip = &cobra.Command{
Use: "geoip",
Short: "GeoIP tools",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
err := geoipPreRun()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file")
mainCommand.AddCommand(commandGeoip)
}
func geoipPreRun() error {
reader, err := maxminddb.Open(commandGeoIPFlagFile)
if err != nil {
return err
}
if reader.Metadata.DatabaseType != "sing-geoip" {
reader.Close()
return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType)
}
geoipReader = reader
return nil
}

View File

@ -0,0 +1,98 @@
package main
import (
"io"
"net"
"os"
"strings"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/oschwald/maxminddb-golang"
"github.com/spf13/cobra"
)
var flagGeoipExportOutput string
const flagGeoipExportDefaultOutput = "geoip-<country>.srs"
var commandGeoipExport = &cobra.Command{
Use: "export <country>",
Short: "Export geoip country as rule-set",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := geoipExport(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path")
commandGeoip.AddCommand(commandGeoipExport)
}
func geoipExport(countryCode string) error {
networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks)
countryMap := make(map[string][]*net.IPNet)
var (
ipNet *net.IPNet
nextCountryCode string
err error
)
for networks.Next() {
ipNet, err = networks.Network(&nextCountryCode)
if err != nil {
return err
}
countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet)
}
ipNets := countryMap[strings.ToLower(countryCode)]
if len(ipNets) == 0 {
return E.New("country code not found: ", countryCode)
}
var (
outputFile *os.File
outputWriter io.Writer
)
if flagGeoipExportOutput == "stdout" {
outputWriter = os.Stdout
} else if flagGeoipExportOutput == flagGeoipExportDefaultOutput {
outputFile, err = os.Create("geoip-" + countryCode + ".json")
if err != nil {
return err
}
defer outputFile.Close()
outputWriter = outputFile
} else {
outputFile, err = os.Create(flagGeoipExportOutput)
if err != nil {
return err
}
defer outputFile.Close()
outputWriter = outputFile
}
encoder := json.NewEncoder(outputWriter)
encoder.SetIndent("", " ")
var headlessRule option.DefaultHeadlessRule
headlessRule.IPCIDR = make([]string, 0, len(ipNets))
for _, cidr := range ipNets {
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
}
var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion1
plainRuleSet.Options.Rules = []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: headlessRule,
},
}
return encoder.Encode(plainRuleSet)
}

View File

@ -0,0 +1,31 @@
package main
import (
"os"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var commandGeoipList = &cobra.Command{
Use: "list",
Short: "List geoip country codes",
Run: func(cmd *cobra.Command, args []string) {
err := listGeoip()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoip.AddCommand(commandGeoipList)
}
func listGeoip() error {
for _, code := range geoipReader.Metadata.Languages {
os.Stdout.WriteString(code + "\n")
}
return nil
}

View File

@ -0,0 +1,47 @@
package main
import (
"net/netip"
"os"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"github.com/spf13/cobra"
)
var commandGeoipLookup = &cobra.Command{
Use: "lookup <address>",
Short: "Lookup if an IP address is contained in the GeoIP database",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := geoipLookup(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoip.AddCommand(commandGeoipLookup)
}
func geoipLookup(address string) error {
addr, err := netip.ParseAddr(address)
if err != nil {
return E.Cause(err, "parse address")
}
if !N.IsPublicAddr(addr) {
os.Stdout.WriteString("private\n")
return nil
}
var code string
_ = geoipReader.Lookup(addr.AsSlice(), &code)
if code != "" {
os.Stdout.WriteString(code + "\n")
return nil
}
os.Stdout.WriteString("unknown\n")
return nil
}

View File

@ -0,0 +1,41 @@
package main
import (
"github.com/sagernet/sing-box/common/geosite"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var (
commandGeoSiteFlagFile string
geositeReader *geosite.Reader
geositeCodeList []string
)
var commandGeoSite = &cobra.Command{
Use: "geosite",
Short: "Geosite tools",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
err := geositePreRun()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file")
mainCommand.AddCommand(commandGeoSite)
}
func geositePreRun() error {
reader, codeList, err := geosite.Open(commandGeoSiteFlagFile)
if err != nil {
return E.Cause(err, "open geosite file")
}
geositeReader = reader
geositeCodeList = codeList
return nil
}

View File

@ -0,0 +1,81 @@
package main
import (
"encoding/json"
"io"
"os"
"github.com/sagernet/sing-box/common/geosite"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/spf13/cobra"
)
var commandGeositeExportOutput string
const commandGeositeExportDefaultOutput = "geosite-<category>.json"
var commandGeositeExport = &cobra.Command{
Use: "export <category>",
Short: "Export geosite category as rule-set",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := geositeExport(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path")
commandGeoSite.AddCommand(commandGeositeExport)
}
func geositeExport(category string) error {
sourceSet, err := geositeReader.Read(category)
if err != nil {
return err
}
var (
outputFile *os.File
outputWriter io.Writer
)
if commandGeositeExportOutput == "stdout" {
outputWriter = os.Stdout
} else if commandGeositeExportOutput == commandGeositeExportDefaultOutput {
outputFile, err = os.Create("geosite-" + category + ".json")
if err != nil {
return err
}
defer outputFile.Close()
outputWriter = outputFile
} else {
outputFile, err = os.Create(commandGeositeExportOutput)
if err != nil {
return err
}
defer outputFile.Close()
outputWriter = outputFile
}
encoder := json.NewEncoder(outputWriter)
encoder.SetIndent("", " ")
var headlessRule option.DefaultHeadlessRule
defaultRule := geosite.Compile(sourceSet)
headlessRule.Domain = defaultRule.Domain
headlessRule.DomainSuffix = defaultRule.DomainSuffix
headlessRule.DomainKeyword = defaultRule.DomainKeyword
headlessRule.DomainRegex = defaultRule.DomainRegex
var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion1
plainRuleSet.Options.Rules = []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: headlessRule,
},
}
return encoder.Encode(plainRuleSet)
}

View File

@ -0,0 +1,50 @@
package main
import (
"os"
"sort"
"github.com/sagernet/sing-box/log"
F "github.com/sagernet/sing/common/format"
"github.com/spf13/cobra"
)
var commandGeositeList = &cobra.Command{
Use: "list <category>",
Short: "List geosite categories",
Run: func(cmd *cobra.Command, args []string) {
err := geositeList()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoSite.AddCommand(commandGeositeList)
}
func geositeList() error {
var geositeEntry []struct {
category string
items int
}
for _, category := range geositeCodeList {
sourceSet, err := geositeReader.Read(category)
if err != nil {
return err
}
geositeEntry = append(geositeEntry, struct {
category string
items int
}{category, len(sourceSet)})
}
sort.SliceStable(geositeEntry, func(i, j int) bool {
return geositeEntry[i].items < geositeEntry[j].items
})
for _, entry := range geositeEntry {
os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n"))
}
return nil
}

View File

@ -0,0 +1,97 @@
package main
import (
"os"
"sort"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var commandGeositeLookup = &cobra.Command{
Use: "lookup [category] <domain>",
Short: "Check if a domain is in the geosite",
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
var (
source string
target string
)
switch len(args) {
case 1:
target = args[0]
case 2:
source = args[0]
target = args[1]
}
err := geositeLookup(source, target)
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoSite.AddCommand(commandGeositeLookup)
}
func geositeLookup(source string, target string) error {
var sourceMatcherList []struct {
code string
matcher *searchGeositeMatcher
}
if source != "" {
sourceSet, err := geositeReader.Read(source)
if err != nil {
return err
}
sourceMatcher, err := newSearchGeositeMatcher(sourceSet)
if err != nil {
return E.Cause(err, "compile code: "+source)
}
sourceMatcherList = []struct {
code string
matcher *searchGeositeMatcher
}{
{
code: source,
matcher: sourceMatcher,
},
}
} else {
for _, code := range geositeCodeList {
sourceSet, err := geositeReader.Read(code)
if err != nil {
return err
}
sourceMatcher, err := newSearchGeositeMatcher(sourceSet)
if err != nil {
return E.Cause(err, "compile code: "+code)
}
sourceMatcherList = append(sourceMatcherList, struct {
code string
matcher *searchGeositeMatcher
}{
code: code,
matcher: sourceMatcher,
})
}
}
sort.SliceStable(sourceMatcherList, func(i, j int) bool {
return sourceMatcherList[i].code < sourceMatcherList[j].code
})
for _, matcherItem := range sourceMatcherList {
if matchRule := matcherItem.matcher.Match(target); matchRule != "" {
os.Stdout.WriteString("Match code (")
os.Stdout.WriteString(matcherItem.code)
os.Stdout.WriteString(") ")
os.Stdout.WriteString(matchRule)
os.Stdout.WriteString("\n")
}
}
return nil
}

View File

@ -0,0 +1,56 @@
package main
import (
"regexp"
"strings"
"github.com/sagernet/sing-box/common/geosite"
)
type searchGeositeMatcher struct {
domainMap map[string]bool
suffixList []string
keywordList []string
regexList []string
}
func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) {
options := geosite.Compile(items)
domainMap := make(map[string]bool)
for _, domain := range options.Domain {
domainMap[domain] = true
}
rule := &searchGeositeMatcher{
domainMap: domainMap,
suffixList: options.DomainSuffix,
keywordList: options.DomainKeyword,
regexList: options.DomainRegex,
}
return rule, nil
}
func (r *searchGeositeMatcher) Match(domain string) string {
if r.domainMap[domain] {
return "domain=" + domain
}
for _, suffix := range r.suffixList {
if strings.HasSuffix(domain, suffix) {
return "domain_suffix=" + suffix
}
}
for _, keyword := range r.keywordList {
if strings.Contains(domain, keyword) {
return "domain_keyword=" + keyword
}
}
for _, regexStr := range r.regexList {
regex, err := regexp.Compile(regexStr)
if err != nil {
continue
}
if regex.MatchString(domain) {
return "domain_regex=" + regexStr
}
}
return ""
}

View File

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

View File

@ -0,0 +1,14 @@
package main
import (
"github.com/spf13/cobra"
)
var commandRuleSet = &cobra.Command{
Use: "rule-set",
Short: "Manage rule sets",
}
func init() {
mainCommand.AddCommand(commandRuleSet)
}

View File

@ -0,0 +1,80 @@
package main
import (
"io"
"os"
"strings"
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/common/srs"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/spf13/cobra"
)
var flagRuleSetCompileOutput string
const flagRuleSetCompileDefaultOutput = "<file_name>.srs"
var commandRuleSetCompile = &cobra.Command{
Use: "compile [source-path]",
Short: "Compile rule-set json to binary",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := compileRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSet.AddCommand(commandRuleSetCompile)
commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
}
func compileRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
decoder := json.NewDecoder(json.NewCommentFilter(reader))
decoder.DisallowUnknownFields()
var plainRuleSet option.PlainRuleSetCompat
err = decoder.Decode(&plainRuleSet)
if err != nil {
return err
}
ruleSet := plainRuleSet.Upgrade()
var outputPath string
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".json") {
outputPath = sourcePath[:len(sourcePath)-5] + ".srs"
} else {
outputPath = sourcePath + ".srs"
}
} else {
outputPath = flagRuleSetCompileOutput
}
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
err = srs.Write(outputFile, ruleSet)
if err != nil {
outputFile.Close()
os.Remove(outputPath)
return err
}
outputFile.Close()
return nil
}

View File

@ -0,0 +1,87 @@
package main
import (
"bytes"
"io"
"os"
"path/filepath"
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var commandRuleSetFormatFlagWrite bool
var commandRuleSetFormat = &cobra.Command{
Use: "format <source-path>",
Short: "Format rule-set json",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := formatRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
commandRuleSet.AddCommand(commandRuleSetFormat)
}
func formatRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
content, err := io.ReadAll(reader)
if err != nil {
return err
}
decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content)))
decoder.DisallowUnknownFields()
var plainRuleSet option.PlainRuleSetCompat
err = decoder.Decode(&plainRuleSet)
if err != nil {
return err
}
ruleSet := plainRuleSet.Upgrade()
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(ruleSet)
if err != nil {
return E.Cause(err, "encode config")
}
outputPath, _ := filepath.Abs(sourcePath)
if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" {
os.Stdout.WriteString(buffer.String() + "\n")
return nil
}
if bytes.Equal(content, buffer.Bytes()) {
return nil
}
output, err := os.Create(sourcePath)
if err != nil {
return E.Cause(err, "open output")
}
_, err = output.Write(buffer.Bytes())
output.Close()
if err != nil {
return E.Cause(err, "write output")
}
os.Stderr.WriteString(outputPath + "\n")
return nil
}

View File

@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) {
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
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 {

View File

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

View File

@ -6,7 +6,6 @@ import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/bufio/deadline"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@ -45,14 +44,7 @@ func (d *DetourDialer) DialContext(ctx context.Context, network string, destinat
if err != nil {
return nil, err
}
conn, err := dialer.DialContext(ctx, network, destination)
if err != nil {
return nil, err
}
if deadline.NeedAdditionalReadDeadline(conn) {
conn = deadline.NewConn(conn)
}
return conn, nil
return dialer.DialContext(ctx, network, destination)
}
func (d *DetourDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {

View File

@ -18,11 +18,19 @@ func NewRouter(router adapter.Router) N.Dialer {
}
func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
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
View File

@ -0,0 +1,485 @@
package srs
import (
"compress/zlib"
"encoding/binary"
"io"
"net/netip"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"go4.org/netipx"
)
var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS
const (
ruleItemQueryType uint8 = iota
ruleItemNetwork
ruleItemDomain
ruleItemDomainKeyword
ruleItemDomainRegex
ruleItemSourceIPCIDR
ruleItemIPCIDR
ruleItemSourcePort
ruleItemSourcePortRange
ruleItemPort
ruleItemPortRange
ruleItemProcessName
ruleItemProcessPath
ruleItemPackageName
ruleItemWIFISSID
ruleItemWIFIBSSID
ruleItemFinal uint8 = 0xFF
)
func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) {
var magicBytes [3]byte
_, err = io.ReadFull(reader, magicBytes[:])
if err != nil {
return
}
if magicBytes != MagicBytes {
err = E.New("invalid sing-box rule set file")
return
}
var version uint8
err = binary.Read(reader, binary.BigEndian, &version)
if err != nil {
return ruleSet, err
}
if version != 1 {
return ruleSet, E.New("unsupported version: ", version)
}
zReader, err := zlib.NewReader(reader)
if err != nil {
return
}
length, err := rw.ReadUVariant(zReader)
if err != nil {
return
}
ruleSet.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ {
ruleSet.Rules[i], err = readRule(zReader, recovery)
if err != nil {
err = E.Cause(err, "read rule[", i, "]")
return
}
}
return
}
func Write(writer io.Writer, ruleSet option.PlainRuleSet) error {
_, err := writer.Write(MagicBytes[:])
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil {
return err
}
zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression)
if err != nil {
return err
}
err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules)))
if err != nil {
return err
}
for _, rule := range ruleSet.Rules {
err = writeRule(zWriter, rule)
if err != nil {
return err
}
}
return zWriter.Close()
}
func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) {
var ruleType uint8
err = binary.Read(reader, binary.BigEndian, &ruleType)
if err != nil {
return
}
switch ruleType {
case 0:
rule.DefaultOptions, err = readDefaultRule(reader, recovery)
case 1:
rule.LogicalOptions, err = readLogicalRule(reader, recovery)
default:
err = E.New("unknown rule type: ", ruleType)
}
return
}
func writeRule(writer io.Writer, rule option.HeadlessRule) error {
switch rule.Type {
case C.RuleTypeDefault:
return writeDefaultRule(writer, rule.DefaultOptions)
case C.RuleTypeLogical:
return writeLogicalRule(writer, rule.LogicalOptions)
default:
panic("unknown rule type: " + rule.Type)
}
}
func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) {
var lastItemType uint8
for {
var itemType uint8
err = binary.Read(reader, binary.BigEndian, &itemType)
if err != nil {
return
}
switch itemType {
case ruleItemQueryType:
var rawQueryType []uint16
rawQueryType, err = readRuleItemUint16(reader)
if err != nil {
return
}
rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType {
return option.DNSQueryType(it)
})
case ruleItemNetwork:
rule.Network, err = readRuleItemString(reader)
case ruleItemDomain:
var matcher *domain.Matcher
matcher, err = domain.ReadMatcher(reader)
if err != nil {
return
}
rule.DomainMatcher = matcher
case ruleItemDomainKeyword:
rule.DomainKeyword, err = readRuleItemString(reader)
case ruleItemDomainRegex:
rule.DomainRegex, err = readRuleItemString(reader)
case ruleItemSourceIPCIDR:
rule.SourceIPSet, err = readIPSet(reader)
if err != nil {
return
}
if recovery {
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
}
case ruleItemIPCIDR:
rule.IPSet, err = readIPSet(reader)
if err != nil {
return
}
if recovery {
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
}
case ruleItemSourcePort:
rule.SourcePort, err = readRuleItemUint16(reader)
case ruleItemSourcePortRange:
rule.SourcePortRange, err = readRuleItemString(reader)
case ruleItemPort:
rule.Port, err = readRuleItemUint16(reader)
case ruleItemPortRange:
rule.PortRange, err = readRuleItemString(reader)
case ruleItemProcessName:
rule.ProcessName, err = readRuleItemString(reader)
case ruleItemProcessPath:
rule.ProcessPath, err = readRuleItemString(reader)
case ruleItemPackageName:
rule.PackageName, err = readRuleItemString(reader)
case ruleItemWIFISSID:
rule.WIFISSID, err = readRuleItemString(reader)
case ruleItemWIFIBSSID:
rule.WIFIBSSID, err = readRuleItemString(reader)
case ruleItemFinal:
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
return
default:
err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType)
}
if err != nil {
return
}
lastItemType = itemType
}
}
func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil {
return err
}
if len(rule.QueryType) > 0 {
err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 {
return uint16(it)
}))
if err != nil {
return err
}
}
if len(rule.Network) > 0 {
err = writeRuleItemString(writer, ruleItemNetwork, rule.Network)
if err != nil {
return err
}
}
if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 {
err = binary.Write(writer, binary.BigEndian, ruleItemDomain)
if err != nil {
return err
}
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer)
if err != nil {
return err
}
}
if len(rule.DomainKeyword) > 0 {
err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword)
if err != nil {
return err
}
}
if len(rule.DomainRegex) > 0 {
err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex)
if err != nil {
return err
}
}
if len(rule.SourceIPCIDR) > 0 {
err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR)
if err != nil {
return E.Cause(err, "source_ipcidr")
}
}
if len(rule.IPCIDR) > 0 {
err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR)
if err != nil {
return E.Cause(err, "ipcidr")
}
}
if len(rule.SourcePort) > 0 {
err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort)
if err != nil {
return err
}
}
if len(rule.SourcePortRange) > 0 {
err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange)
if err != nil {
return err
}
}
if len(rule.Port) > 0 {
err = writeRuleItemUint16(writer, ruleItemPort, rule.Port)
if err != nil {
return err
}
}
if len(rule.PortRange) > 0 {
err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange)
if err != nil {
return err
}
}
if len(rule.ProcessName) > 0 {
err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName)
if err != nil {
return err
}
}
if len(rule.ProcessPath) > 0 {
err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath)
if err != nil {
return err
}
}
if len(rule.PackageName) > 0 {
err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName)
if err != nil {
return err
}
}
if len(rule.WIFISSID) > 0 {
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
if err != nil {
return err
}
}
if len(rule.WIFIBSSID) > 0 {
err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID)
if err != nil {
return err
}
}
err = binary.Write(writer, binary.BigEndian, ruleItemFinal)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, rule.Invert)
if err != nil {
return err
}
return nil
}
func readRuleItemString(reader io.Reader) ([]string, error) {
length, err := rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
value := make([]string, length)
for i := uint64(0); i < length; i++ {
value[i], err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
}
return value, nil
}
func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error {
err := binary.Write(writer, binary.BigEndian, itemType)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(value)))
if err != nil {
return err
}
for _, item := range value {
err = rw.WriteVString(writer, item)
if err != nil {
return err
}
}
return nil
}
func readRuleItemUint16(reader io.Reader) ([]uint16, error) {
length, err := rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
value := make([]uint16, length)
for i := uint64(0); i < length; i++ {
err = binary.Read(reader, binary.BigEndian, &value[i])
if err != nil {
return nil, err
}
}
return value, nil
}
func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error {
err := binary.Write(writer, binary.BigEndian, itemType)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(value)))
if err != nil {
return err
}
for _, item := range value {
err = binary.Write(writer, binary.BigEndian, item)
if err != nil {
return err
}
}
return nil
}
func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
var builder netipx.IPSetBuilder
for i, prefixString := range value {
prefix, err := netip.ParsePrefix(prefixString)
if err == nil {
builder.AddPrefix(prefix)
continue
}
addr, addrErr := netip.ParseAddr(prefixString)
if addrErr == nil {
builder.Add(addr)
continue
}
return E.Cause(err, "parse [", i, "]")
}
ipSet, err := builder.IPSet()
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, itemType)
if err != nil {
return err
}
return writeIPSet(writer, ipSet)
}
func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
var mode uint8
err = binary.Read(reader, binary.BigEndian, &mode)
if err != nil {
return
}
switch mode {
case 0:
logicalRule.Mode = C.LogicalTypeAnd
case 1:
logicalRule.Mode = C.LogicalTypeOr
default:
err = E.New("unknown logical mode: ", mode)
return
}
length, err := rw.ReadUVariant(reader)
if err != nil {
return
}
logicalRule.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ {
logicalRule.Rules[i], err = readRule(reader, recovery)
if err != nil {
err = E.Cause(err, "read logical rule [", i, "]")
return
}
}
err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert)
if err != nil {
return
}
return
}
func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error {
err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil {
return err
}
switch logicalRule.Mode {
case C.LogicalTypeAnd:
err = binary.Write(writer, binary.BigEndian, uint8(0))
case C.LogicalTypeOr:
err = binary.Write(writer, binary.BigEndian, uint8(1))
default:
panic("unknown logical mode: " + logicalRule.Mode)
}
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules)))
if err != nil {
return err
}
for _, rule := range logicalRule.Rules {
err = writeRule(writer, rule)
if err != nil {
return err
}
}
err = binary.Write(writer, binary.BigEndian, logicalRule.Invert)
if err != nil {
return err
}
return nil
}

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

@ -0,0 +1,116 @@
package srs
import (
"encoding/binary"
"io"
"net/netip"
"unsafe"
"github.com/sagernet/sing/common/rw"
"go4.org/netipx"
)
type myIPSet struct {
rr []myIPRange
}
type myIPRange struct {
from netip.Addr
to netip.Addr
}
func readIPSet(reader io.Reader) (*netipx.IPSet, error) {
var version uint8
err := binary.Read(reader, binary.BigEndian, &version)
if err != nil {
return nil, err
}
var length uint64
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return nil, err
}
mySet := &myIPSet{
rr: make([]myIPRange, length),
}
for i := uint64(0); i < length; i++ {
var (
fromLen uint64
toLen uint64
fromAddr netip.Addr
toAddr netip.Addr
)
fromLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
fromBytes := make([]byte, fromLen)
_, err = io.ReadFull(reader, fromBytes)
if err != nil {
return nil, err
}
err = fromAddr.UnmarshalBinary(fromBytes)
if err != nil {
return nil, err
}
toLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
toBytes := make([]byte, toLen)
_, err = io.ReadFull(reader, toBytes)
if err != nil {
return nil, err
}
err = toAddr.UnmarshalBinary(toBytes)
if err != nil {
return nil, err
}
mySet.rr[i] = myIPRange{fromAddr, toAddr}
}
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
}
func writeIPSet(writer io.Writer, set *netipx.IPSet) error {
err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil {
return err
}
mySet := (*myIPSet)(unsafe.Pointer(set))
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
if err != nil {
return err
}
for _, rr := range mySet.rr {
var (
fromBinary []byte
toBinary []byte
)
fromBinary, err = rr.from.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(fromBinary)))
if err != nil {
return err
}
_, err = writer.Write(fromBinary)
if err != nil {
return err
}
toBinary, err = rr.to.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(toBinary)))
if err != nil {
return err
}
_, err = writer.Write(toBinary)
if err != nil {
return err
}
}
return nil
}

View File

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

View File

@ -4,6 +4,71 @@ icon: material/alert-decagram
# ChangeLog
#### 1.8.0-alpha.7
* Fixes and improvements
#### 1.7.1
* Fixes and improvements
#### 1.8.0-alpha.6
* Fix rule-set matching logic **1**
* Fixes and improvements
**1**:
Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets,
rather than completely following the AND logic.
#### 1.8.0-alpha.5
* Parallel rule-set initialization
* Independent `source_ip_is_private` and `ip_is_private` rules **1**
**1**:
The `private` GeoIP country never existed and was actually implemented inside V2Ray.
Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets).
#### 1.8.0-alpha.1
* Migrate cache file from Clash API to independent options **1**
* Introducing [Rule Set](/configuration/rule-set) **2**
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
* Allow nested logical rules **4**
**1**:
See [Cache File](/configuration/experimental/cache-file) and
[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options).
**2**:
Rule set is independent collections of rules that can be compiled into binaries to improve performance.
Compared to legacy GeoIP and Geosite resources,
it can include more types of rules, load faster,
use less memory, and update automatically.
See [Route#rule_set](/configuration/route/#rule_set),
[Route Rule](/configuration/route/rule),
[DNS Rule](/configuration/dns/rule),
[Rule Set](/configuration/rule-set),
[Source Format](/configuration/rule-set/source-format) and
[Headless Rule](/configuration/rule-set/headless-rule).
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
**3**:
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets.
**4**:
Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules.
#### 1.7.0
* Fixes and improvements
@ -142,11 +207,13 @@ Only supported in graphical clients on Android and iOS.
**1**:
Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound options.
Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound
options.
**2**
Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, see [TCP Brutal](/configuration/shared/tcp-brutal) for details.
Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server,
see [TCP Brutal](/configuration/shared/tcp-brutal) for details.
#### 1.7.0-alpha.3
@ -213,8 +280,8 @@ When `auto_route` is enabled and `strict_route` is disabled, the device can now
**2**:
Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave.
Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High
Sierra, 10.14 Mojave.
#### 1.6.0-rc.4
@ -227,7 +294,8 @@ Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008
**1**:
Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave.
Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High
Sierra, 10.14 Mojave.
#### 1.6.0-beta.4

View File

@ -1,3 +1,14 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [rule_set](#rule_set)
:material-plus: [source_ip_is_private](#source_ip_is_private)
:material-delete-clock: [geoip](#geoip)
:material-delete-clock: [geosite](#geosite)
### Structure
```json
@ -46,6 +57,7 @@
"10.0.0.0/24",
"192.168.0.1"
],
"source_ip_is_private": false,
"source_port": [
12345
],
@ -85,6 +97,10 @@
"wifi_bssid": [
"00:00:00:00:00:00"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
],
"invert": false,
"outbound": [
"direct"
@ -166,15 +182,29 @@ Match domain using regular expression.
#### geosite
!!! failure "Deprecated in sing-box 1.8.0"
Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets).
Match geosite.
#### source_geoip
!!! failure "Deprecated in sing-box 1.8.0"
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
Match source geoip.
#### source_ip_cidr
Match source ip cidr.
Match source IP CIDR.
#### source_ip_is_private
!!! question "Since sing-box 1.8.0"
Match non-public source IP.
#### source_port
@ -250,6 +280,12 @@ Match WiFi SSID.
Match WiFi BSSID.
#### rule_set
!!! question "Since sing-box 1.8.0"
Match [Rule Set](/configuration/route/#rule_set).
#### invert
Invert match result.
@ -286,4 +322,4 @@ Rewrite TTL in DNS responses.
#### rules
Included default rules.
Included rules.

View File

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

View File

@ -0,0 +1,34 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.8.0"
### Structure
```json
{
"enabled": true,
"path": "",
"cache_id": "",
"store_fakeip": false
}
```
### Fields
#### enabled
Enable cache file.
#### path
Path to the cache file.
`cache.db` will be used if empty.
#### cache_id
Identifier in cache file.
If not empty, configuration specified data will use a separate store keyed by it.

View File

@ -0,0 +1,121 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.8.0"
:material-delete-alert: [store_mode](#store_mode)
:material-delete-alert: [store_selected](#store_selected)
:material-delete-alert: [store_fakeip](#store_fakeip)
:material-delete-alert: [cache_file](#cache_file)
:material-delete-alert: [cache_id](#cache_id)
!!! quote ""
Clash API is not included by default, see [Installation](./#installation).
### Structure
```json
{
"external_controller": "127.0.0.1:9090",
"external_ui": "",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "",
"default_mode": "",
// Deprecated
"store_mode": false,
"store_selected": false,
"store_fakeip": false,
"cache_file": "",
"cache_id": ""
}
```
### Fields
#### external_controller
RESTful web API listening address. Clash API will be disabled if empty.
#### external_ui
A relative path to the configuration directory or an absolute path to a
directory in which you put some static web resource. sing-box will then
serve it at `http://{{external-controller}}/ui`.
#### external_ui_download_url
ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.
`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.
#### external_ui_download_detour
The tag of the outbound to download the external UI.
Default outbound will be used if empty.
#### secret
Secret for the RESTful API (optional)
Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
ALWAYS set a secret if RESTful API is listening on 0.0.0.0
#### default_mode
Default mode in clash, `Rule` will be used if empty.
This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item.
#### store_mode
!!! failure "Deprecated in sing-box 1.8.0"
`store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`.
Store Clash mode in cache file.
#### store_selected
!!! failure "Deprecated in sing-box 1.8.0"
`store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`.
!!! note ""
The tag must be set for target outbounds.
Store selected outbound for the `Selector` outbound in cache file.
#### store_fakeip
!!! failure "Deprecated in sing-box 1.8.0"
`store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`.
Store fakeip in cache file.
#### cache_file
!!! failure "Deprecated in sing-box 1.8.0"
`cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`.
Cache file path, `cache.db` will be used if empty.
#### cache_id
!!! failure "Deprecated in sing-box 1.8.0"
`cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`.
Identifier in cache file.
If not empty, configuration specified data will use a separate store keyed by it.

View File

@ -1,139 +1,30 @@
---
icon: material/alert-decagram
---
# Experimental
!!! 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) |

View File

@ -1,137 +0,0 @@
# 实验性
### 结构
```json
{
"experimental": {
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "",
"default_mode": "",
"store_mode": false,
"store_selected": false,
"store_fakeip": false,
"cache_file": "",
"cache_id": ""
},
"v2ray_api": {
"listen": "127.0.0.1:8080",
"stats": {
"enabled": true,
"inbounds": [
"socks-in"
],
"outbounds": [
"proxy",
"direct"
],
"users": [
"sekai"
]
}
}
}
}
```
!!! note ""
流量统计和连接管理会降低性能。
### Clash API 字段
!!! quote ""
默认安装不包含 Clash API参阅 [安装](/zh/#_2)。
#### external_controller
RESTful web API 监听地址。如果为空,则禁用 Clash API。
#### external_ui
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
#### external_ui_download_url
静态网页资源的 ZIP 下载 URL如果指定的 `external_ui` 目录为空,将使用。
默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`
#### external_ui_download_detour
用于下载静态网页资源的出站的标签。
如果为空,将使用默认出站。
#### secret
RESTful API 的密钥(可选)
通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证
如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。
#### default_mode
Clash 中的默认模式,默认使用 `Rule`
此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。
#### store_mode
将 Clash 模式存储在缓存文件中。
#### store_selected
!!! note ""
必须为目标出站设置标签。
`Selector` 中出站的选定的目标出站存储在缓存文件中。
#### store_fakeip
将 fakeip 存储在缓存文件中。
#### cache_file
缓存文件路径,默认使用`cache.db`
#### cache_id
缓存 ID。
如果不为空,`store_selected` 将会使用以此为键的独立存储。
### V2Ray API 字段
!!! quote ""
默认安装不包含 V2Ray API参阅 [安装](/zh/#_2)。
#### listen
gRPC API 监听地址。如果为空,则禁用 V2Ray API。
#### stats
流量统计服务设置。
#### stats.enabled
启用统计服务。
#### stats.inbounds
统计流量的入站列表。
#### stats.outbounds
统计流量的出站列表。
#### stats.users
统计流量的用户列表。

View File

@ -0,0 +1,50 @@
### Structure
!!! quote ""
V2Ray API is not included by default, see [Installation](./#installation).
```json
{
"listen": "127.0.0.1:8080",
"stats": {
"enabled": true,
"inbounds": [
"socks-in"
],
"outbounds": [
"proxy",
"direct"
],
"users": [
"sekai"
]
}
}
```
### Fields
#### listen
gRPC API listening address. V2Ray API will be disabled if empty.
#### stats
Traffic statistics service settings.
#### stats.enabled
Enable statistics service.
#### stats.inbounds
Inbound list to count traffic.
#### stats.outbounds
Outbound list to count traffic.
#### stats.users
User list to count traffic.

View File

@ -1,3 +1,11 @@
---
icon: material/delete-clock
---
!!! failure "Deprecated in sing-box 1.8.0"
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
### Structure
```json

View File

@ -1,33 +0,0 @@
### 结构
```json
{
"route": {
"geoip": {
"path": "",
"download_url": "",
"download_detour": ""
}
}
}
```
### 字段
#### path
指定 GeoIP 资源的路径。
默认 `geoip.db`
#### download_url
指定 GeoIP 资源的下载链接。
默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`
#### download_detour
用于下载 GeoIP 资源的出站的标签。
如果为空,将使用默认出站。

View File

@ -1,3 +1,11 @@
---
icon: material/delete-clock
---
!!! failure "Deprecated in sing-box 1.8.0"
Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets).
### Structure
```json

View File

@ -1,33 +0,0 @@
### 结构
```json
{
"route": {
"geosite": {
"path": "",
"download_url": "",
"download_detour": ""
}
}
}
```
### 字段
#### path
指定 GeoSite 资源的路径。
默认 `geosite.db`
#### download_url
指定 GeoSite 资源的下载链接。
默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`
#### download_detour
用于下载 GeoSite 资源的出站的标签。
如果为空,将使用默认出站。

View File

@ -1,5 +1,15 @@
---
icon: material/alert-decagram
---
# Route
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [rule_set](#rule_set)
:material-delete-clock: [geoip](#geoip)
:material-delete-clock: [geosite](#geosite)
### Structure
```json
@ -8,6 +18,7 @@
"geoip": {},
"geosite": {},
"rules": [],
"rule_set": [],
"final": "",
"auto_detect_interface": false,
"override_android_vpn": false,
@ -19,11 +30,20 @@
### Fields
| Key | Format |
|------------|------------------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [Geosite](./geosite) |
| `rules` | List of [Route Rule](./rule) |
| Key | Format |
|-----------|----------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [Geosite](./geosite) |
#### rules
List of [Route Rule](./rule)
#### rule_set
!!! question "Since sing-box 1.8.0"
List of [Rule Set](/configuration/rule-set)
#### final

View File

@ -1,5 +1,15 @@
---
icon: material/alert-decagram
---
# 路由
!!! quote "sing-box 1.8.0 中的更改"
:material-plus: [rule_set](#rule_set)
:material-delete-clock: [geoip](#geoip)
:material-delete-clock: [geosite](#geosite)
### 结构
```json
@ -7,8 +17,8 @@
"route": {
"geoip": {},
"geosite": {},
"ip_rules": [],
"rules": [],
"rule_set": [],
"final": "",
"auto_detect_interface": false,
"override_android_vpn": false,
@ -20,11 +30,21 @@
### 字段
| 键 | 格式 |
|------------|-------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [GeoSite](./geosite) |
| `rules` | 一组 [路由规则](./rule) |
| 键 | 格式 |
|------------|-----------------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [Geosite](./geosite) |
#### rule
一组 [路由规则](./rule)。
#### rule_set
!!! question "自 sing-box 1.8.0 起"
一组 [规则集](/configuration/rule-set)。
#### final

View File

@ -1,3 +1,17 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [rule_set](#rule_set)
:material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [source_ip_is_private](#source_ip_is_private)
:material-plus: [ip_is_private](#ip_is_private)
:material-delete-clock: [source_geoip](#source_geoip)
:material-delete-clock: [geoip](#geoip)
:material-delete-clock: [geosite](#geosite)
### Structure
```json
@ -46,10 +60,12 @@
"10.0.0.0/24",
"192.168.0.1"
],
"source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"source_port": [
12345
],
@ -89,6 +105,10 @@
"wifi_bssid": [
"00:00:00:00:00:00"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
],
"invert": false,
"outbound": "direct"
},
@ -160,23 +180,47 @@ Match domain using regular expression.
#### geosite
!!! failure "Deprecated in sing-box 1.8.0"
Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets).
Match geosite.
#### source_geoip
!!! failure "Deprecated in sing-box 1.8.0"
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
Match source geoip.
#### geoip
!!! failure "Deprecated in sing-box 1.8.0"
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
Match geoip.
#### source_ip_cidr
Match source ip cidr.
Match source IP CIDR.
#### ip_is_private
!!! question "Since sing-box 1.8.0"
Match non-public IP.
#### ip_cidr
Match ip cidr.
Match IP CIDR.
#### source_ip_is_private
!!! question "Since sing-box 1.8.0"
Match non-public source IP.
#### source_port
@ -250,6 +294,18 @@ Match WiFi SSID.
Match WiFi BSSID.
#### rule_set
!!! question "Since sing-box 1.8.0"
Match [Rule Set](/configuration/route/#rule_set).
#### rule_set_ipcidr_match_source
!!! question "Since sing-box 1.8.0"
Make `ipcidr` in rule sets match the source IP.
#### invert
Invert match result.
@ -276,4 +332,4 @@ Tag of the target outbound.
==Required==
Included default rules.
Included rules.

View File

@ -1,3 +1,17 @@
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.8.0 中的更改"
:material-plus: [rule_set](#rule_set)
:material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [source_ip_is_private](#source_ip_is_private)
:material-plus: [ip_is_private](#ip_is_private)
:material-delete-clock: [source_geoip](#source_geoip)
:material-delete-clock: [geoip](#geoip)
:material-delete-clock: [geosite](#geosite)
### 结构
```json
@ -45,9 +59,11 @@
"source_ip_cidr": [
"10.0.0.0/24"
],
"source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24"
],
"ip_is_private": false,
"source_port": [
12345
],
@ -87,6 +103,10 @@
"wifi_bssid": [
"00:00:00:00:00:00"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
],
"invert": false,
"outbound": "direct"
},
@ -158,24 +178,48 @@
#### geosite
匹配 GeoSite。
!!! failure "已在 sing-box 1.8.0 废弃"
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-sets)。
匹配 Geosite。
#### source_geoip
!!! failure "已在 sing-box 1.8.0 废弃"
GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。
匹配源 GeoIP。
#### geoip
!!! failure "已在 sing-box 1.8.0 废弃"
GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。
匹配 GeoIP。
#### source_ip_cidr
匹配源 IP CIDR。
#### source_ip_is_private
!!! question "自 sing-box 1.8.0 起"
匹配非公开源 IP。
#### ip_cidr
匹配 IP CIDR。
#### ip_is_private
!!! question "自 sing-box 1.8.0 起"
匹配非公开 IP。
#### source_port
匹配源端口。
@ -248,6 +292,18 @@
匹配 WiFi BSSID。
#### rule_set
!!! question "自 sing-box 1.8.0 起"
匹配[规则集](/zh/configuration/route/#rule_set)。
#### rule_set_ipcidr_match_source
!!! question "自 sing-box 1.8.0 起"
使规则集中的 `ipcidr` 规则匹配源 IP。
#### invert
反选匹配结果。
@ -274,4 +330,4 @@
==必填==
包括的默认规则。
包括的规则。

View File

@ -0,0 +1,207 @@
---
icon: material/new-box
---
### Structure
!!! question "Since sing-box 1.8.0"
```json
{
"rules": [
{
"query_type": [
"A",
"HTTPS",
32768
],
"network": [
"tcp"
],
"domain": [
"test.com"
],
"domain_suffix": [
".cn"
],
"domain_keyword": [
"test"
],
"domain_regex": [
"^stun\\..+"
],
"source_ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"source_port": [
12345
],
"source_port_range": [
"1000:2000",
":3000",
"4000:"
],
"port": [
80,
443
],
"port_range": [
"1000:2000",
":3000",
"4000:"
],
"process_name": [
"curl"
],
"process_path": [
"/usr/bin/curl"
],
"package_name": [
"com.termux"
],
"wifi_ssid": [
"My WIFI"
],
"wifi_bssid": [
"00:00:00:00:00:00"
],
"invert": false
},
{
"type": "logical",
"mode": "and",
"rules": [],
"invert": false
}
]
}
```
!!! note ""
You can ignore the JSON Array [] tag when the content is only one item
### Default Fields
!!! note ""
The default rule uses the following matching logic:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) &&
(`port` || `port_range`) &&
(`source_port` || `source_port_range`) &&
`other fields`
#### query_type
DNS query type. Values can be integers or type name strings.
#### network
`tcp` or `udp`.
#### domain
Match full domain.
#### domain_suffix
Match domain suffix.
#### domain_keyword
Match domain using keyword.
#### domain_regex
Match domain using regular expression.
#### source_ip_cidr
Match source IP CIDR.
#### ip_cidr
!!! info ""
`ip_cidr` is an alias for `source_ip_cidr` when the Rule Set is used in DNS rules or `rule_set_ipcidr_match_source` enabled in route rules.
Match IP CIDR.
#### source_port
Match source port.
#### source_port_range
Match source port range.
#### port
Match port.
#### port_range
Match port range.
#### process_name
!!! quote ""
Only supported on Linux, Windows, and macOS.
Match process name.
#### process_path
!!! quote ""
Only supported on Linux, Windows, and macOS.
Match process path.
#### package_name
Match android package name.
#### wifi_ssid
!!! quote ""
Only supported in graphical clients on Android and iOS.
Match WiFi SSID.
#### wifi_bssid
!!! quote ""
Only supported in graphical clients on Android and iOS.
Match WiFi BSSID.
#### invert
Invert match result.
### Logical Fields
#### type
`logical`
#### mode
==Required==
`and` or `or`
#### rules
==Required==
Included rules.

View File

@ -0,0 +1,97 @@
---
icon: material/new-box
---
# Rule Set
!!! question "Since sing-box 1.8.0"
### Structure
```json
{
"type": "",
"tag": "",
"format": "",
... // Typed Fields
}
```
#### Local Structure
```json
{
"type": "local",
...
"path": ""
}
```
#### Remote Structure
!!! info ""
Remote rule-set will be cached if `experimental.cache_file.enabled`.
```json
{
"type": "remote",
...,
"url": "",
"download_detour": "",
"update_interval": ""
}
```
### Fields
#### type
==Required==
Type of Rule Set, `local` or `remote`.
#### tag
==Required==
Tag of Rule Set.
#### format
==Required==
Format of Rule Set, `source` or `binary`.
### Local Fields
#### path
==Required==
File path of Rule Set.
### Remote Fields
#### url
==Required==
Download URL of Rule Set.
#### download_detour
Tag of the outbound to download rule-set.
Default outbound will be used if empty.
#### update_interval
Update interval of Rule Set.
`1d` will be used if empty.

View File

@ -0,0 +1,34 @@
---
icon: material/new-box
---
# Source Format
!!! question "Since sing-box 1.8.0"
### Structure
```json
{
"version": 1,
"rules": []
}
```
### Compile
Use `sing-box rule-set compile [--output <file-name>.srs] <file-name>.json` to compile source to binary rule-set.
### Fields
#### version
==Required==
Version of Rule Set, must be `1`.
#### rules
==Required==
List of [Headless Rule](./headless-rule.md).

View File

@ -343,6 +343,83 @@ flowchart TB
}
```
=== ":material-dns: DNS rules (1.8.0+)"
!!! info
DNS rules are optional if FakeIP is used.
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
},
{
"clash_mode": "Direct",
"server": "local"
},
{
"clash_mode": "Global",
"server": "google"
},
{
"type": "logical",
"mode": "and",
"rules": [
{
"rule_set": "geosite-geolocation-!cn",
"invert": true
},
{
"rule_set": [
"geosite-cn",
"geosite-category-companies@cn"
]
}
],
"server": "local"
}
]
},
"route": {
"rule_set": [
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs"
},
{
"type": "remote",
"tag": "geosite-geolocation-!cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs"
},
{
"type": "remote",
"tag": "geosite-category-companies@cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs"
}
]
}
}
```
=== ":material-router-network: Route rules"
```json
@ -422,4 +499,111 @@ flowchart TB
]
}
}
```
=== ":material-router-network: Route rules (1.8.0+)"
```json
{
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"route": {
"rules": [
{
"type": "logical",
"mode": "or",
"rules": [
{
"protocol": "dns"
},
{
"port": 53
}
],
"outbound": "dns"
},
{
"ip_is_private": true,
"outbound": "direct"
},
{
"clash_mode": "Direct",
"outbound": "direct"
},
{
"clash_mode": "Global",
"outbound": "default"
},
{
"type": "logical",
"mode": "or",
"rules": [
{
"port": 853
},
{
"network": "udp",
"port": 443
},
{
"protocol": "stun"
}
],
"outbound": "block"
},
{
"type": "logical",
"mode": "and",
"rules": [
{
"rule_set": "geosite-geolocation-!cn",
"invert": true
},
{
"rule_set": [
"geoip-cn",
"geosite-cn",
"geosite-category-companies@cn"
]
}
],
"outbound": "direct"
}
],
"rule_set": [
{
"type": "remote",
"tag": "geoip-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs"
},
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs"
},
{
"type": "remote",
"tag": "geosite-geolocation-!cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs"
},
{
"type": "remote",
"tag": "geosite-category-companies@cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs"
}
]
}
}
```

195
docs/migration.md Normal file
View File

@ -0,0 +1,195 @@
---
icon: material/arrange-bring-forward
---
# Migration
## 1.8.0
!!! warning "Unstable"
This version is still under development, and the following migration guide may be changed in the future.
### :material-close-box: Migrate cache file from Clash API to independent options
!!! info "Reference"
[Clash API](/configuration/experimental/clash-api) /
[Cache File](/configuration/experimental/cache-file)
=== ":material-card-remove: Deprecated"
```json
{
"experimental": {
"clash_api": {
"cache_file": "cache.db", // default value
"cahce_id": "my_profile2",
"store_mode": true,
"store_selected": true,
"store_fakeip": true
}
}
}
```
=== ":material-card-multiple: New"
```json
{
"experimental" : {
"cache_file": {
"enabled": true,
"path": "cache.db", // default value
"cache_id": "my_profile2",
"store_fakeip": true
}
}
}
```
### :material-checkbox-intermediate: Migrate GeoIP to rule sets
!!! info "Reference"
[GeoIP](/configuration/route/geoip) /
[Route](/configuration/route) /
[Route Rule](/configuration/route/rule) /
[DNS Rule](/configuration/dns/rule) /
[Rule Set](/configuration/rule-set)
!!! tip
`sing-box geoip` commands can help you convert custom GeoIP into rule sets.
=== ":material-card-remove: Deprecated"
```json
{
"route": {
"rules": [
{
"geoip": "private",
"outbound": "direct"
},
{
"geoip": "cn",
"outbound": "direct"
},
{
"source_geoip": "cn",
"outbound": "block"
}
],
"geoip": {
"download_detour": "proxy"
}
}
}
```
=== ":material-card-multiple: New"
```json
{
"route": {
"rules": [
{
"ip_is_private": true,
"outbound": "direct"
},
{
"rule_set": "geoip-cn",
"outbound": "direct"
},
{
"rule_set": "geoip-us",
"rule_set_ipcidr_match_source": true,
"outbound": "block"
}
],
"rule_set": [
{
"tag": "geoip-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
"download_detour": "proxy"
},
{
"tag": "geoip-us",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs",
"download_detour": "proxy"
}
]
},
"experimental": {
"cache_file": {
"enabled": true // required to save Rule Set cache
}
}
}
```
### :material-checkbox-intermediate: Migrate Geosite to rule sets
!!! info "Reference"
[Geosite](/configuration/route/geosite) /
[Route](/configuration/route) /
[Route Rule](/configuration/route/rule) /
[DNS Rule](/configuration/dns/rule) /
[Rule Set](/configuration/rule-set)
!!! tip
`sing-box geosite` commands can help you convert custom Geosite into rule sets.
=== ":material-card-remove: Deprecated"
```json
{
"route": {
"rules": [
{
"geosite": "cn",
"outbound": "direct"
}
],
"geosite": {
"download_detour": "proxy"
}
}
}
```
=== ":material-card-multiple: New"
```json
{
"route": {
"rules": [
{
"rule_set": "geosite-cn",
"outbound": "direct"
}
],
"rule_set": [
{
"tag": "geosite-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
"download_detour": "proxy"
}
]
},
"experimental": {
"cache_file": {
"enabled": true // required to save Rule Set cache
}
}
}
```

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import (
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
@ -27,24 +28,37 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O
func CalculateClashModeList(options option.Options) []string {
var clashMode []string
for _, dnsRule := range common.PtrValueOrDefault(options.DNS).Rules {
if dnsRule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, dnsRule.DefaultOptions.ClashMode) {
clashMode = append(clashMode, dnsRule.DefaultOptions.ClashMode)
}
for _, defaultRule := range dnsRule.LogicalOptions.Rules {
if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) {
clashMode = append(clashMode, defaultRule.ClashMode)
}
}
}
for _, rule := range common.PtrValueOrDefault(options.Route).Rules {
if rule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, rule.DefaultOptions.ClashMode) {
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
}
for _, defaultRule := range rule.LogicalOptions.Rules {
if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) {
clashMode = append(clashMode, defaultRule.ClashMode)
clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...)
clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...)
clashMode = common.FilterNotDefault(common.Uniq(clashMode))
return clashMode
}
func extraClashModeFromRule(rules []option.Rule) []string {
var clashMode []string
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if rule.DefaultOptions.ClashMode != "" {
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
}
case C.RuleTypeLogical:
clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...)
}
}
return clashMode
}
func extraClashModeFromDNSRule(rules []option.DNSRule) []string {
var clashMode []string
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if rule.DefaultOptions.ClashMode != "" {
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
}
case C.RuleTypeLogical:
clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...)
}
}
return clashMode

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

@ -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.20231201060417-575186ed63c2
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

4
go.sum
View File

@ -110,8 +110,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/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.20231201060417-575186ed63c2 h1:1ydWkFgLURGlrnwRdjyrpo9lp1g5Qq7XrNBghMntWTs=
github.com/sagernet/sing v0.2.18-0.20231201060417-575186ed63c2/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=

View File

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

View File

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

View File

@ -1,7 +1,48 @@
package option
type ExperimentalOptions struct {
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
Debug *DebugOptions `json:"debug,omitempty"`
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
Debug *DebugOptions `json:"debug,omitempty"`
}
type CacheFileOptions struct {
Enabled bool `json:"enabled,omitempty"`
Path string `json:"path,omitempty"`
CacheID string `json:"cache_id,omitempty"`
StoreFakeIP bool `json:"store_fakeip,omitempty"`
}
type ClashAPIOptions struct {
ExternalController string `json:"external_controller,omitempty"`
ExternalUI string `json:"external_ui,omitempty"`
ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"`
ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"`
Secret string `json:"secret,omitempty"`
DefaultMode string `json:"default_mode,omitempty"`
ModeList []string `json:"-"`
// Deprecated: migrated to global cache file
CacheFile string `json:"cache_file,omitempty"`
// Deprecated: migrated to global cache file
CacheID string `json:"cache_id,omitempty"`
// Deprecated: migrated to global cache file
StoreMode bool `json:"store_mode,omitempty"`
// Deprecated: migrated to global cache file
StoreSelected bool `json:"store_selected,omitempty"`
// Deprecated: migrated to global cache file
StoreFakeIP bool `json:"store_fakeip,omitempty"`
}
type V2RayAPIOptions struct {
Listen string `json:"listen,omitempty"`
Stats *V2RayStatsServiceOptions `json:"stats,omitempty"`
}
type V2RayStatsServiceOptions struct {
Enabled bool `json:"enabled,omitempty"`
Inbounds []string `json:"inbounds,omitempty"`
Outbounds []string `json:"outbounds,omitempty"`
Users []string `json:"users,omitempty"`
}

15
option/group.go Normal file
View File

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

View File

@ -4,6 +4,7 @@ type RouteOptions struct {
GeoIP *GeoIPOptions `json:"geoip,omitempty"`
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"`

View File

@ -53,35 +53,50 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error {
return nil
}
func (r Rule) IsValid() bool {
switch r.Type {
case C.RuleTypeDefault:
return r.DefaultOptions.IsValid()
case C.RuleTypeLogical:
return r.LogicalOptions.IsValid()
default:
panic("unknown rule type: " + r.Type)
}
}
type DefaultRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
Network Listable[string] `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
GeoIP Listable[string] `json:"geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
Network Listable[string] `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
GeoIP Listable[string] `json:"geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
RuleSet Listable[string] `json:"rule_set,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r DefaultRule) IsValid() bool {
@ -92,12 +107,12 @@ func (r DefaultRule) IsValid() bool {
}
type LogicalRule struct {
Mode string `json:"mode"`
Rules []DefaultRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
Mode string `json:"mode"`
Rules []Rule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r LogicalRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid)
return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid)
}

View File

@ -53,37 +53,50 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
return nil
}
func (r DNSRule) IsValid() bool {
switch r.Type {
case C.RuleTypeDefault:
return r.DefaultOptions.IsValid()
case C.RuleTypeLogical:
return r.LogicalOptions.IsValid()
default:
panic("unknown DNS rule type: " + r.Type)
}
}
type DefaultDNSRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
QueryType Listable[DNSQueryType] `json:"query_type,omitempty"`
Network Listable[string] `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
Outbound Listable[string] `json:"outbound,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
QueryType Listable[DNSQueryType] `json:"query_type,omitempty"`
Network Listable[string] `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
Outbound Listable[string] `json:"outbound,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
RuleSet Listable[string] `json:"rule_set,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
}
func (r DefaultDNSRule) IsValid() bool {
@ -96,14 +109,14 @@ func (r DefaultDNSRule) IsValid() bool {
}
type LogicalDNSRule struct {
Mode string `json:"mode"`
Rules []DefaultDNSRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
Mode string `json:"mode"`
Rules []DNSRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
}
func (r LogicalDNSRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid)
return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid)
}

230
option/rule_set.go Normal file
View File

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

226
option/time_unit.go Normal file
View File

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

View File

@ -164,7 +164,7 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error {
if err != nil {
return err
}
duration, err := time.ParseDuration(value)
duration, err := ParseDuration(value)
if err != nil {
return err
}
@ -174,6 +174,14 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error {
type DNSQueryType uint16
func (t DNSQueryType) String() string {
typeName, loaded := mDNS.TypeToString[uint16(t)]
if loaded {
return typeName
}
return F.ToString(uint16(t))
}
func (t DNSQueryType) MarshalJSON() ([]byte, error) {
typeName, loaded := mDNS.TypeToString[uint16(t)]
if loaded {

View File

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

View File

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

View File

@ -194,9 +194,7 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa
group.Append0(func(ctx context.Context) error {
var buffer *buf.Buffer
readWaiter.InitializeReadWaiter(func() *buf.Buffer {
buffer = buf.NewSize(dns.FixedPacketSize)
buffer.FullReset()
return buffer
return buf.NewSize(dns.FixedPacketSize)
})
defer readWaiter.InitializeReadWaiter(nil)
for {

View File

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

View File

@ -38,6 +38,7 @@ type URLTest struct {
tolerance uint16
group *URLTestGroup
interruptExternalConnections bool
started bool
}
func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) {
@ -83,6 +84,7 @@ func (s *URLTest) Start() error {
}
func (s *URLTest) PostStart() error {
s.started = true
go s.CheckOutbounds()
return nil
}
@ -110,7 +112,9 @@ func (s *URLTest) CheckOutbounds() {
}
func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
s.group.Start()
if s.started {
s.group.Start()
}
outbound := s.group.Select(network)
conn, err := outbound.DialContext(ctx, network, destination)
if err == nil {
@ -122,7 +126,9 @@ func (s *URLTest) DialContext(ctx context.Context, network string, destination M
}
func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
s.group.Start()
if s.started {
s.group.Start()
}
outbound := s.group.Select(N.NetworkUDP)
conn, err := outbound.ListenPacket(ctx, destination)
if err == nil {

View File

@ -39,6 +39,7 @@ import (
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
serviceNTP "github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/common/task"
"github.com/sagernet/sing/common/uot"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
@ -67,6 +68,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 +109,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 +131,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)
@ -261,7 +273,7 @@ func NewRouter(
if fakeIPOptions.Inet6Range != nil {
inet6Range = *fakeIPOptions.Inet6Range
}
router.fakeIPStore = fakeip.NewStore(router, router.logger, inet4Range, inet6Range)
router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range)
}
usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor()
@ -479,6 +491,33 @@ func (r *Router) Start() error {
if r.needWIFIState {
r.updateWIFIState()
}
if r.fakeIPStore != nil {
err := r.fakeIPStore.Start()
if err != nil {
return err
}
}
if len(r.ruleSets) > 0 {
ruleSetStartContext := NewRuleSetStartContext()
var ruleSetStartGroup task.Group
for i, ruleSet := range r.ruleSets {
ruleSetInPlace := ruleSet
ruleSetStartGroup.Append0(func(ctx context.Context) error {
err := ruleSetInPlace.StartContext(ctx, ruleSetStartContext)
if err != nil {
return E.Cause(err, "initialize rule-set[", i, "]")
}
return nil
})
}
ruleSetStartGroup.Concurrency(5)
ruleSetStartGroup.FastFail()
err := ruleSetStartGroup.Run(r.ctx)
if err != nil {
return err
}
ruleSetStartContext.Close()
}
for i, rule := range r.rules {
err := rule.Start()
if err != nil {
@ -491,12 +530,6 @@ func (r *Router) Start() error {
return E.Cause(err, "initialize DNS rule[", i, "]")
}
}
if r.fakeIPStore != nil {
err := r.fakeIPStore.Start()
if err != nil {
return err
}
}
for i, transport := range r.transports {
err := transport.Start()
if err != nil {
@ -576,11 +609,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 +627,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 {
@ -646,7 +690,6 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
if metadata.InboundOptions.SniffEnabled {
buffer := buf.NewPacket()
buffer.FullReset()
sniffMetadata, err := sniff.PeekStream(ctx, conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), sniff.StreamDomainNameQuery, sniff.TLSClientHello, sniff.HTTPHost)
if sniffMetadata != nil {
metadata.Protocol = sniffMetadata.Protocol
@ -762,7 +805,6 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() {
buffer := buf.NewPacket()
buffer.FullReset()
destination, err := conn.ReadPacket(buffer)
if err != nil {
buffer.Release()
@ -878,6 +920,7 @@ func (r *Router) match0(ctx context.Context, metadata *adapter.InboundContext, d
}
}
for i, rule := range r.rules {
metadata.ResetRuleCache()
if rule.Match(metadata) {
detour := rule.Outbound()
r.logger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour)

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package route
import (
"io"
"strings"
"github.com/sagernet/sing-box/adapter"
@ -16,6 +17,7 @@ type abstractDefaultRule struct {
destinationAddressItems []RuleItem
destinationPortItems []RuleItem
allItems []RuleItem
ruleSetItem RuleItem
invert bool
outbound string
}
@ -61,62 +63,62 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
return true
}
if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch {
for _, item := range r.sourceAddressItems {
if item.Match(metadata) {
metadata.SourceAddressMatch = true
break
}
}
}
if len(r.sourcePortItems) > 0 && !metadata.SourceAddressMatch {
for _, item := range r.sourcePortItems {
if item.Match(metadata) {
metadata.SourcePortMatch = true
break
}
}
}
if len(r.destinationAddressItems) > 0 && !metadata.SourceAddressMatch {
for _, item := range r.destinationAddressItems {
if item.Match(metadata) {
metadata.DestinationAddressMatch = true
break
}
}
}
if len(r.destinationPortItems) > 0 && !metadata.SourceAddressMatch {
for _, item := range r.destinationPortItems {
if item.Match(metadata) {
metadata.DestinationPortMatch = true
break
}
}
}
for _, item := range r.items {
if !item.Match(metadata) {
return r.invert
}
}
if len(r.sourceAddressItems) > 0 {
var sourceAddressMatch bool
for _, item := range r.sourceAddressItems {
if item.Match(metadata) {
sourceAddressMatch = true
break
}
}
if !sourceAddressMatch {
return r.invert
}
if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch {
return r.invert
}
if len(r.sourcePortItems) > 0 {
var sourcePortMatch bool
for _, item := range r.sourcePortItems {
if item.Match(metadata) {
sourcePortMatch = true
break
}
}
if !sourcePortMatch {
return r.invert
}
if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch {
return r.invert
}
if len(r.destinationAddressItems) > 0 {
var destinationAddressMatch bool
for _, item := range r.destinationAddressItems {
if item.Match(metadata) {
destinationAddressMatch = true
break
}
}
if !destinationAddressMatch {
return r.invert
}
if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch {
return r.invert
}
if len(r.destinationPortItems) > 0 {
var destinationPortMatch bool
for _, item := range r.destinationPortItems {
if item.Match(metadata) {
destinationPortMatch = true
break
}
}
if !destinationPortMatch {
return r.invert
}
if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch {
return r.invert
}
return !r.invert
@ -135,7 +137,7 @@ func (r *abstractDefaultRule) String() string {
}
type abstractLogicalRule struct {
rules []adapter.Rule
rules []adapter.HeadlessRule
mode string
invert bool
outbound string
@ -146,7 +148,10 @@ func (r *abstractLogicalRule) Type() string {
}
func (r *abstractLogicalRule) UpdateGeosite() error {
for _, rule := range r.rules {
for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (adapter.Rule, bool) {
rule, loaded := it.(adapter.Rule)
return rule, loaded
}) {
err := rule.UpdateGeosite()
if err != nil {
return err
@ -156,7 +161,10 @@ func (r *abstractLogicalRule) UpdateGeosite() error {
}
func (r *abstractLogicalRule) Start() error {
for _, rule := range r.rules {
for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) {
rule, loaded := it.(common.Starter)
return rule, loaded
}) {
err := rule.Start()
if err != nil {
return err
@ -166,7 +174,10 @@ func (r *abstractLogicalRule) Start() error {
}
func (r *abstractLogicalRule) Close() error {
for _, rule := range r.rules {
for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) {
rule, loaded := it.(io.Closer)
return rule, loaded
}) {
err := rule.Close()
if err != nil {
return err
@ -177,11 +188,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
}

View File

@ -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)
@ -120,6 +120,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.SourceIPIsPrivate {
item := NewIPIsPrivateItem(true)
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.IPCIDR) > 0 {
item, err := NewIPCIDRItem(false, options.IPCIDR)
if err != nil {
@ -128,6 +133,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.IPIsPrivate {
item := NewIPIsPrivateItem(false)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item)
@ -194,6 +204,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 +221,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 +235,7 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt
return nil, E.New("unknown logical mode: ", options.Mode)
}
for i, subRule := range options.Rules {
rule, err := NewDefaultRule(router, logger, subRule)
rule, err := NewRule(router, logger, subRule, false)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}

View File

@ -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)
@ -119,6 +119,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.SourceIPIsPrivate {
item := NewIPIsPrivateItem(true)
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item)
@ -190,6 +195,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 +222,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 +238,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options
return nil, E.New("unknown logical mode: ", options.Mode)
}
for i, subRule := range options.Rules {
rule, err := NewDefaultDNSRule(router, logger, subRule)
rule, err := NewDNSRule(router, logger, subRule, false)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}

173
route/rule_headless.go Normal file
View File

@ -0,0 +1,173 @@
package route
import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func NewHeadlessRule(router adapter.Router, options option.HeadlessRule) (adapter.HeadlessRule, error) {
switch options.Type {
case "", C.RuleTypeDefault:
if !options.DefaultOptions.IsValid() {
return nil, E.New("missing conditions")
}
return NewDefaultHeadlessRule(router, options.DefaultOptions)
case C.RuleTypeLogical:
if !options.LogicalOptions.IsValid() {
return nil, E.New("missing conditions")
}
return NewLogicalHeadlessRule(router, options.LogicalOptions)
default:
return nil, E.New("unknown rule type: ", options.Type)
}
}
var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil)
type DefaultHeadlessRule struct {
abstractDefaultRule
}
func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) {
rule := &DefaultHeadlessRule{
abstractDefaultRule{
invert: options.Invert,
},
}
if len(options.Network) > 0 {
item := NewNetworkItem(options.Network)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
item := NewDomainItem(options.Domain, options.DomainSuffix)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
} else if options.DomainMatcher != nil {
item := NewRawDomainItem(options.DomainMatcher)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.DomainKeyword) > 0 {
item := NewDomainKeywordItem(options.DomainKeyword)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.DomainRegex) > 0 {
item, err := NewDomainRegexItem(options.DomainRegex)
if err != nil {
return nil, E.Cause(err, "domain_regex")
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceIPCIDR) > 0 {
item, err := NewIPCIDRItem(true, options.SourceIPCIDR)
if err != nil {
return nil, E.Cause(err, "source_ipcidr")
}
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
} else if options.SourceIPSet != nil {
item := NewRawIPCIDRItem(true, options.SourceIPSet)
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.IPCIDR) > 0 {
item, err := NewIPCIDRItem(false, options.IPCIDR)
if err != nil {
return nil, E.Cause(err, "ipcidr")
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
} else if options.IPSet != nil {
item := NewRawIPCIDRItem(false, options.IPSet)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePortRange) > 0 {
item, err := NewPortRangeItem(true, options.SourcePortRange)
if err != nil {
return nil, E.Cause(err, "source_port_range")
}
rule.sourcePortItems = append(rule.sourcePortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Port) > 0 {
item := NewPortItem(false, options.Port)
rule.destinationPortItems = append(rule.destinationPortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.PortRange) > 0 {
item, err := NewPortRangeItem(false, options.PortRange)
if err != nil {
return nil, E.Cause(err, "port_range")
}
rule.destinationPortItems = append(rule.destinationPortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ProcessName) > 0 {
item := NewProcessItem(options.ProcessName)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ProcessPath) > 0 {
item := NewProcessPathItem(options.ProcessPath)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.PackageName) > 0 {
item := NewPackageNameItem(options.PackageName)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.WIFISSID) > 0 {
item := NewWIFISSIDItem(router, options.WIFISSID)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.WIFIBSSID) > 0 {
item := NewWIFIBSSIDItem(router, options.WIFIBSSID)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
return rule, nil
}
var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil)
type LogicalHeadlessRule struct {
abstractLogicalRule
}
func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) {
r := &LogicalHeadlessRule{
abstractLogicalRule{
rules: make([]adapter.HeadlessRule, len(options.Rules)),
invert: options.Invert,
},
}
switch options.Mode {
case C.LogicalTypeAnd:
r.mode = C.LogicalTypeAnd
case C.LogicalTypeOr:
r.mode = C.LogicalTypeOr
default:
return nil, E.New("unknown logical mode: ", options.Mode)
}
for i, subRule := range options.Rules {
rule, err := NewHeadlessRule(router, subRule)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}
r.rules[i] = rule
}
return r, nil
}

View File

@ -31,7 +31,7 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) {
builder.Add(addr)
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() {

View File

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

View File

@ -0,0 +1,44 @@
package route
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network"
)
var _ RuleItem = (*IPIsPrivateItem)(nil)
type IPIsPrivateItem struct {
isSource bool
}
func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem {
return &IPIsPrivateItem{isSource}
}
func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool {
var destination netip.Addr
if r.isSource {
destination = metadata.Source.Addr
} else {
destination = metadata.Destination.Addr
}
if destination.IsValid() && !N.IsPublicAddr(destination) {
return true
}
for _, destinationAddress := range metadata.DestinationAddresses {
if !N.IsPublicAddr(destinationAddress) {
return true
}
}
return false
}
func (r *IPIsPrivateItem) String() string {
if r.isSource {
return "source_ip_is_private=true"
} else {
return "ip_is_private=true"
}
}

View File

@ -0,0 +1,55 @@
package route
import (
"strings"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
var _ RuleItem = (*RuleSetItem)(nil)
type RuleSetItem struct {
router adapter.Router
tagList []string
setList []adapter.HeadlessRule
ipcidrMatchSource bool
}
func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem {
return &RuleSetItem{
router: router,
tagList: tagList,
ipcidrMatchSource: ipCIDRMatchSource,
}
}
func (r *RuleSetItem) Start() error {
for _, tag := range r.tagList {
ruleSet, loaded := r.router.RuleSet(tag)
if !loaded {
return E.New("rule-set not found: ", tag)
}
r.setList = append(r.setList, ruleSet)
}
return nil
}
func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
metadata.IPCIDRMatchSource = r.ipcidrMatchSource
for _, ruleSet := range r.setList {
if ruleSet.Match(metadata) {
return true
}
}
return false
}
func (r *RuleSetItem) String() string {
if len(r.tagList) == 1 {
return F.ToString("rule_set=", r.tagList[0])
} else {
return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]")
}
}

67
route/rule_set.go Normal file
View File

@ -0,0 +1,67 @@
package route
import (
"context"
"net"
"net/http"
"sync"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) {
switch options.Type {
case C.RuleSetTypeLocal:
return NewLocalRuleSet(router, options)
case C.RuleSetTypeRemote:
return NewRemoteRuleSet(ctx, router, logger, options), nil
default:
return nil, E.New("unknown rule set type: ", options.Type)
}
}
var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil)
type RuleSetStartContext struct {
access sync.Mutex
httpClientCache map[string]*http.Client
}
func NewRuleSetStartContext() *RuleSetStartContext {
return &RuleSetStartContext{
httpClientCache: make(map[string]*http.Client),
}
}
func (c *RuleSetStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
c.access.Lock()
defer c.access.Unlock()
if httpClient, loaded := c.httpClientCache[detour]; loaded {
return httpClient
}
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
c.httpClientCache[detour] = httpClient
return httpClient
}
func (c *RuleSetStartContext) Close() {
c.access.Lock()
defer c.access.Unlock()
for _, client := range c.httpClientCache {
client.CloseIdleConnections()
}
}

70
route/rule_set_local.go Normal file
View File

@ -0,0 +1,70 @@
package route
import (
"context"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
var _ adapter.RuleSet = (*LocalRuleSet)(nil)
type LocalRuleSet struct {
rules []adapter.HeadlessRule
}
func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) {
setFile, err := os.Open(options.LocalOptions.Path)
if err != nil {
return nil, err
}
var plainRuleSet option.PlainRuleSet
switch options.Format {
case C.RuleSetFormatSource, "":
var compat option.PlainRuleSetCompat
decoder := json.NewDecoder(json.NewCommentFilter(setFile))
decoder.DisallowUnknownFields()
err = decoder.Decode(&compat)
if err != nil {
return nil, err
}
plainRuleSet = compat.Upgrade()
case C.RuleSetFormatBinary:
plainRuleSet, err = srs.Read(setFile, false)
if err != nil {
return nil, err
}
default:
return nil, E.New("unknown rule set format: ", options.Format)
}
rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules))
for i, ruleOptions := range plainRuleSet.Rules {
rules[i], err = NewHeadlessRule(router, ruleOptions)
if err != nil {
return nil, E.Cause(err, "parse rule_set.rules.[", i, "]")
}
}
return &LocalRuleSet{rules}, nil
}
func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
for _, rule := range s.rules {
if rule.Match(metadata) {
return true
}
}
return false
}
func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
return nil
}
func (s *LocalRuleSet) Close() error {
return nil
}

232
route/rule_set_remote.go Normal file
View File

@ -0,0 +1,232 @@
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"
"github.com/sagernet/sing/service/pause"
)
var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
type RemoteRuleSet struct {
ctx context.Context
cancel context.CancelFunc
router adapter.Router
logger logger.ContextLogger
options option.RuleSet
updateInterval time.Duration
dialer N.Dialer
rules []adapter.HeadlessRule
lastUpdated time.Time
lastEtag string
updateTicker *time.Ticker
pauseManager pause.Manager
}
func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet {
ctx, cancel := context.WithCancel(ctx)
var updateInterval time.Duration
if options.RemoteOptions.UpdateInterval > 0 {
updateInterval = time.Duration(options.RemoteOptions.UpdateInterval)
} else {
updateInterval = 24 * time.Hour
}
return &RemoteRuleSet{
ctx: ctx,
cancel: cancel,
router: router,
logger: logger,
options: options,
updateInterval: updateInterval,
pauseManager: pause.ManagerFromContext(ctx),
}
}
func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
for _, rule := range s.rules {
if rule.Match(metadata) {
return true
}
}
return false
}
func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
var dialer N.Dialer
if s.options.RemoteOptions.DownloadDetour != "" {
outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour)
if !loaded {
return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour)
}
dialer = outbound
} else {
outbound, err := s.router.DefaultOutbound(N.NetworkTCP)
if err != nil {
return err
}
dialer = outbound
}
s.dialer = dialer
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
if cacheFile != nil {
if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil {
err := s.loadBytes(savedSet.Content)
if err != nil {
return E.Cause(err, "restore cached rule-set")
}
s.lastUpdated = savedSet.LastUpdated
s.lastEtag = savedSet.LastEtag
}
}
if s.lastUpdated.IsZero() {
err := s.fetchOnce(ctx, startContext)
if err != nil {
return E.Cause(err, "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() {
if time.Since(s.lastUpdated) > s.updateInterval {
err := s.fetchOnce(s.ctx, nil)
if err != nil {
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
}
}
for {
select {
case <-s.ctx.Done():
return
case <-s.updateTicker.C:
s.pauseManager.WaitActive()
err := s.fetchOnce(s.ctx, nil)
if err != nil {
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
}
}
}
}
func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.RuleSetStartContext) error {
s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL)
var httpClient *http.Client
if startContext != nil {
httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer)
} else {
httpClient = &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
}
request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil)
if err != nil {
return err
}
if s.lastEtag != "" {
request.Header.Set("If-None-Match", s.lastEtag)
}
response, err := httpClient.Do(request.WithContext(ctx))
if err != nil {
return err
}
switch response.StatusCode {
case http.StatusOK:
case http.StatusNotModified:
s.logger.Info("update rule-set ", s.options.Tag, ": not modified")
return nil
default:
return E.New("unexpected status: ", response.Status)
}
content, err := io.ReadAll(response.Body)
if err != nil {
response.Body.Close()
return err
}
err = s.loadBytes(content)
if err != nil {
response.Body.Close()
return err
}
response.Body.Close()
eTagHeader := response.Header.Get("Etag")
if eTagHeader != "" {
s.lastEtag = eTagHeader
}
s.lastUpdated = time.Now()
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
if cacheFile != nil {
err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{
LastUpdated: s.lastUpdated,
Content: content,
LastEtag: s.lastEtag,
})
if err != nil {
s.logger.Error("save rule-set cache: ", err)
}
}
s.logger.Info("updated rule-set ", s.options.Tag)
return nil
}
func (s *RemoteRuleSet) Close() error {
s.updateTicker.Stop()
s.cancel()
return nil
}

View File

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

View File

@ -12,6 +12,9 @@ import (
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/bufio/deadline"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@ -87,18 +90,35 @@ func (c *Client) dialContext(ctx context.Context, requestURL *url.URL, headers h
return nil, err
}
}
conn.SetDeadline(time.Now().Add(C.TCPTimeout))
var deadlineConn net.Conn
if deadline.NeedAdditionalReadDeadline(conn) {
deadlineConn = deadline.NewConn(conn)
} else {
deadlineConn = conn
}
err = deadlineConn.SetDeadline(time.Now().Add(C.TCPTimeout))
if err != nil {
return nil, E.Cause(err, "set read deadline")
}
var protocols []string
if protocolHeader := headers.Get("Sec-WebSocket-Protocol"); protocolHeader != "" {
protocols = []string{protocolHeader}
headers.Del("Sec-WebSocket-Protocol")
}
reader, _, err := ws.Dialer{Header: ws.HandshakeHeaderHTTP(headers), Protocols: protocols}.Upgrade(conn, requestURL)
conn.SetDeadline(time.Time{})
reader, _, err := ws.Dialer{Header: ws.HandshakeHeaderHTTP(headers), Protocols: protocols}.Upgrade(deadlineConn, requestURL)
deadlineConn.SetDeadline(time.Time{})
if err != nil {
return nil, err
}
return NewConn(conn, reader, nil, ws.StateClientSide), nil
if reader.Buffered() > 0 {
buffer := buf.NewSize(reader.Buffered())
_, err = buffer.ReadFullFrom(reader, buffer.Len())
if err != nil {
return nil, err
}
conn = bufio.NewCachedConn(conn, buffer)
}
return NewConn(conn, nil, ws.StateClientSide), nil
}
func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {

View File

@ -1,7 +1,6 @@
package v2raywebsocket
import (
"bufio"
"context"
"encoding/base64"
"io"
@ -28,19 +27,13 @@ type WebsocketConn struct {
remoteAddr net.Addr
}
func NewConn(conn net.Conn, br *bufio.Reader, remoteAddr net.Addr, state ws.State) *WebsocketConn {
func NewConn(conn net.Conn, remoteAddr net.Addr, state ws.State) *WebsocketConn {
controlHandler := wsutil.ControlFrameHandler(conn, state)
var reader io.Reader
if br != nil && br.Buffered() > 0 {
reader = br
} else {
reader = conn
}
return &WebsocketConn{
Conn: conn,
state: state,
reader: &wsutil.Reader{
Source: reader,
Source: conn,
State: state,
SkipHeaderCheck: !debug.Enabled,
OnIntermediate: controlHandler,

View File

@ -88,14 +88,14 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
s.invalidRequest(writer, request, http.StatusBadRequest, E.Cause(err, "decode early data"))
return
}
wsConn, reader, _, err := ws.UpgradeHTTP(request, writer)
wsConn, _, _, err := ws.UpgradeHTTP(request, writer)
if err != nil {
s.invalidRequest(writer, request, 0, E.Cause(err, "upgrade websocket connection"))
return
}
var metadata M.Metadata
metadata.Source = sHttp.SourceAddress(request)
conn = NewConn(wsConn, reader.Reader, metadata.Source.TCPAddr(), ws.StateServerSide)
conn = NewConn(wsConn, metadata.Source.TCPAddr(), ws.StateServerSide)
if len(earlyData) > 0 {
conn = bufio.NewCachedConn(conn, buf.As(earlyData))
}

View File

@ -134,7 +134,7 @@ func (c *VisionConn) Read(p []byte) (n int, err error) {
buffers = common.Map(buffers, func(it *buf.Buffer) *buf.Buffer {
return it.ToOwned()
})
chunkBuffer.FullReset()
chunkBuffer.Reset()
}
if c.remainingContent == 0 && c.remainingPadding == 0 {
if c.currentCommand == commandPaddingEnd {