mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-31 00:14:21 +03:00
Add rule-set
This commit is contained in:
170
route/router.go
170
route/router.go
@@ -39,6 +39,7 @@ import (
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
serviceNTP "github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
"github.com/sagernet/sing/common/uot"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
@@ -64,9 +65,12 @@ type Router struct {
|
||||
geoIPReader *geoip.Reader
|
||||
geositeReader *geosite.Reader
|
||||
geositeCache map[string]adapter.Rule
|
||||
needFindProcess bool
|
||||
dnsClient *dns.Client
|
||||
defaultDomainStrategy dns.DomainStrategy
|
||||
dnsRules []adapter.DNSRule
|
||||
ruleSets []adapter.RuleSet
|
||||
ruleSetMap map[string]adapter.RuleSet
|
||||
defaultTransport dns.Transport
|
||||
transports []dns.Transport
|
||||
transportMap map[string]dns.Transport
|
||||
@@ -107,11 +111,13 @@ func NewRouter(
|
||||
outboundByTag: make(map[string]adapter.Outbound),
|
||||
rules: make([]adapter.Rule, 0, len(options.Rules)),
|
||||
dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)),
|
||||
ruleSetMap: make(map[string]adapter.RuleSet),
|
||||
needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule),
|
||||
needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule),
|
||||
geoIPOptions: common.PtrValueOrDefault(options.GeoIP),
|
||||
geositeOptions: common.PtrValueOrDefault(options.Geosite),
|
||||
geositeCache: make(map[string]adapter.Rule),
|
||||
needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess,
|
||||
defaultDetour: options.Final,
|
||||
defaultDomainStrategy: dns.DomainStrategy(dnsOptions.Strategy),
|
||||
autoDetectInterface: options.AutoDetectInterface,
|
||||
@@ -141,6 +147,17 @@ func NewRouter(
|
||||
}
|
||||
router.dnsRules = append(router.dnsRules, dnsRule)
|
||||
}
|
||||
for i, ruleSetOptions := range options.RuleSet {
|
||||
if _, exists := router.ruleSetMap[ruleSetOptions.Tag]; exists {
|
||||
return nil, E.New("duplicate rule-set tag: ", ruleSetOptions.Tag)
|
||||
}
|
||||
ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse rule-set[", i, "]")
|
||||
}
|
||||
router.ruleSets = append(router.ruleSets, ruleSet)
|
||||
router.ruleSetMap[ruleSetOptions.Tag] = ruleSet
|
||||
}
|
||||
|
||||
transports := make([]dns.Transport, len(dnsOptions.Servers))
|
||||
dummyTransportMap := make(map[string]dns.Transport)
|
||||
@@ -296,34 +313,6 @@ func NewRouter(
|
||||
router.interfaceMonitor = interfaceMonitor
|
||||
}
|
||||
|
||||
needFindProcess := hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess
|
||||
needPackageManager := C.IsAndroid && platformInterface == nil && (needFindProcess || common.Any(inbounds, func(inbound option.Inbound) bool {
|
||||
return len(inbound.TunOptions.IncludePackage) > 0 || len(inbound.TunOptions.ExcludePackage) > 0
|
||||
}))
|
||||
if needPackageManager {
|
||||
packageManager, err := tun.NewPackageManager(router)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create package manager")
|
||||
}
|
||||
router.packageManager = packageManager
|
||||
}
|
||||
if needFindProcess {
|
||||
if platformInterface != nil {
|
||||
router.processSearcher = platformInterface
|
||||
} else {
|
||||
searcher, err := process.NewSearcher(process.Config{
|
||||
Logger: logFactory.NewLogger("router/process"),
|
||||
PackageManager: router.packageManager,
|
||||
})
|
||||
if err != nil {
|
||||
if err != os.ErrInvalid {
|
||||
router.logger.Warn(E.Cause(err, "create process searcher"))
|
||||
}
|
||||
} else {
|
||||
router.processSearcher = searcher
|
||||
}
|
||||
}
|
||||
}
|
||||
if ntpOptions.Enabled {
|
||||
timeService, err := ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions)
|
||||
if err != nil {
|
||||
@@ -332,11 +321,6 @@ func NewRouter(
|
||||
service.ContextWith[serviceNTP.TimeService](ctx, timeService)
|
||||
router.timeService = timeService
|
||||
}
|
||||
if platformInterface != nil && router.interfaceMonitor != nil && router.needWIFIState {
|
||||
router.interfaceMonitor.RegisterCallback(func(_ int) {
|
||||
router.updateWIFIState()
|
||||
})
|
||||
}
|
||||
return router, nil
|
||||
}
|
||||
|
||||
@@ -451,12 +435,6 @@ func (r *Router) Start() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if r.packageManager != nil {
|
||||
err := r.packageManager.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if r.needGeositeDatabase {
|
||||
for _, rule := range r.rules {
|
||||
err := rule.UpdateGeosite()
|
||||
@@ -477,9 +455,89 @@ func (r *Router) Start() error {
|
||||
r.geositeCache = nil
|
||||
r.geositeReader = nil
|
||||
}
|
||||
if r.needWIFIState {
|
||||
if r.fakeIPStore != nil {
|
||||
err := r.fakeIPStore.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(r.ruleSets) > 0 {
|
||||
ruleSetStartContext := NewRuleSetStartContext()
|
||||
var ruleSetStartGroup task.Group
|
||||
for i, ruleSet := range r.ruleSets {
|
||||
ruleSetInPlace := ruleSet
|
||||
ruleSetStartGroup.Append0(func(ctx context.Context) error {
|
||||
err := ruleSetInPlace.StartContext(ctx, ruleSetStartContext)
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize rule-set[", i, "]")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
ruleSetStartGroup.Concurrency(5)
|
||||
ruleSetStartGroup.FastFail()
|
||||
err := ruleSetStartGroup.Run(r.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ruleSetStartContext.Close()
|
||||
}
|
||||
|
||||
var (
|
||||
needProcessFromRuleSet bool
|
||||
needWIFIStateFromRuleSet bool
|
||||
)
|
||||
for _, ruleSet := range r.ruleSets {
|
||||
metadata := ruleSet.Metadata()
|
||||
if metadata.ContainsProcessRule {
|
||||
needProcessFromRuleSet = true
|
||||
}
|
||||
if metadata.ContainsWIFIRule {
|
||||
needWIFIStateFromRuleSet = true
|
||||
}
|
||||
}
|
||||
if needProcessFromRuleSet || r.needFindProcess {
|
||||
needPackageManager := C.IsAndroid && r.platformInterface == nil
|
||||
|
||||
if needPackageManager {
|
||||
packageManager, err := tun.NewPackageManager(r)
|
||||
if err != nil {
|
||||
return E.Cause(err, "create package manager")
|
||||
}
|
||||
if packageManager != nil {
|
||||
err = packageManager.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.packageManager = packageManager
|
||||
}
|
||||
|
||||
if r.platformInterface != nil {
|
||||
r.processSearcher = r.platformInterface
|
||||
} else {
|
||||
searcher, err := process.NewSearcher(process.Config{
|
||||
Logger: r.logger,
|
||||
PackageManager: r.packageManager,
|
||||
})
|
||||
if err != nil {
|
||||
if err != os.ErrInvalid {
|
||||
r.logger.Warn(E.Cause(err, "create process searcher"))
|
||||
}
|
||||
} else {
|
||||
r.processSearcher = searcher
|
||||
}
|
||||
}
|
||||
}
|
||||
if needWIFIStateFromRuleSet || r.needWIFIState {
|
||||
if r.platformInterface != nil && r.interfaceMonitor != nil {
|
||||
r.interfaceMonitor.RegisterCallback(func(_ int) {
|
||||
r.updateWIFIState()
|
||||
})
|
||||
}
|
||||
r.updateWIFIState()
|
||||
}
|
||||
|
||||
for i, rule := range r.rules {
|
||||
err := rule.Start()
|
||||
if err != nil {
|
||||
@@ -492,12 +550,6 @@ func (r *Router) Start() error {
|
||||
return E.Cause(err, "initialize DNS rule[", i, "]")
|
||||
}
|
||||
}
|
||||
if r.fakeIPStore != nil {
|
||||
err := r.fakeIPStore.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for i, transport := range r.transports {
|
||||
err := transport.Start()
|
||||
if err != nil {
|
||||
@@ -573,6 +625,14 @@ func (r *Router) Close() error {
|
||||
}
|
||||
|
||||
func (r *Router) PostStart() error {
|
||||
if len(r.ruleSets) > 0 {
|
||||
for i, ruleSet := range r.ruleSets {
|
||||
err := ruleSet.PostStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "post start rule-set[", i, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
r.started = true
|
||||
return nil
|
||||
}
|
||||
@@ -582,11 +642,17 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
return outbound, loaded
|
||||
}
|
||||
|
||||
func (r *Router) DefaultOutbound(network string) adapter.Outbound {
|
||||
func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) {
|
||||
if network == N.NetworkTCP {
|
||||
return r.defaultOutboundForConnection
|
||||
if r.defaultOutboundForConnection == nil {
|
||||
return nil, E.New("missing default outbound for TCP connections")
|
||||
}
|
||||
return r.defaultOutboundForConnection, nil
|
||||
} else {
|
||||
return r.defaultOutboundForPacketConnection
|
||||
if r.defaultOutboundForPacketConnection == nil {
|
||||
return nil, E.New("missing default outbound for UDP connections")
|
||||
}
|
||||
return r.defaultOutboundForPacketConnection, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +660,11 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore {
|
||||
return r.fakeIPStore
|
||||
}
|
||||
|
||||
func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) {
|
||||
ruleSet, loaded := r.ruleSetMap[tag]
|
||||
return ruleSet, loaded
|
||||
}
|
||||
|
||||
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||
if metadata.InboundDetour != "" {
|
||||
if metadata.LastInbound == metadata.InboundDetour {
|
||||
@@ -882,6 +953,7 @@ func (r *Router) match0(ctx context.Context, metadata *adapter.InboundContext, d
|
||||
}
|
||||
}
|
||||
for i, rule := range r.rules {
|
||||
metadata.ResetRuleCache()
|
||||
if rule.Match(metadata) {
|
||||
detour := rule.Outbound()
|
||||
r.logger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -13,8 +13,6 @@ import (
|
||||
"github.com/sagernet/sing-box/common/geoip"
|
||||
"github.com/sagernet/sing-box/common/geosite"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
@@ -243,71 +241,3 @@ func (r *Router) downloadGeositeDatabase(savePath string) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if cond(rule.DefaultOptions) {
|
||||
return true
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
if hasRule(rule.LogicalOptions.Rules, cond) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool {
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if cond(rule.DefaultOptions) {
|
||||
return true
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
if hasDNSRule(rule.LogicalOptions.Rules, cond) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isGeoIPRule(rule option.DefaultRule) bool {
|
||||
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode)
|
||||
}
|
||||
|
||||
func isGeoIPDNSRule(rule option.DefaultDNSRule) bool {
|
||||
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode)
|
||||
}
|
||||
|
||||
func isGeositeRule(rule option.DefaultRule) bool {
|
||||
return len(rule.Geosite) > 0
|
||||
}
|
||||
|
||||
func isGeositeDNSRule(rule option.DefaultDNSRule) bool {
|
||||
return len(rule.Geosite) > 0
|
||||
}
|
||||
|
||||
func isProcessRule(rule option.DefaultRule) bool {
|
||||
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
|
||||
}
|
||||
|
||||
func isProcessDNSRule(rule option.DefaultDNSRule) bool {
|
||||
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
|
||||
}
|
||||
|
||||
func notPrivateNode(code string) bool {
|
||||
return code != "private"
|
||||
}
|
||||
|
||||
func isWIFIRule(rule option.DefaultRule) bool {
|
||||
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
|
||||
}
|
||||
|
||||
func isWIFIDNSRule(rule option.DefaultDNSRule) bool {
|
||||
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
|
||||
}
|
||||
|
||||
99
route/router_rule.go
Normal file
99
route/router_rule.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if cond(rule.DefaultOptions) {
|
||||
return true
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
if hasRule(rule.LogicalOptions.Rules, cond) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool {
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if cond(rule.DefaultOptions) {
|
||||
return true
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
if hasDNSRule(rule.LogicalOptions.Rules, cond) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool {
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if cond(rule.DefaultOptions) {
|
||||
return true
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
if hasHeadlessRule(rule.LogicalOptions.Rules, cond) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isGeoIPRule(rule option.DefaultRule) bool {
|
||||
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode)
|
||||
}
|
||||
|
||||
func isGeoIPDNSRule(rule option.DefaultDNSRule) bool {
|
||||
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode)
|
||||
}
|
||||
|
||||
func isGeositeRule(rule option.DefaultRule) bool {
|
||||
return len(rule.Geosite) > 0
|
||||
}
|
||||
|
||||
func isGeositeDNSRule(rule option.DefaultDNSRule) bool {
|
||||
return len(rule.Geosite) > 0
|
||||
}
|
||||
|
||||
func isProcessRule(rule option.DefaultRule) bool {
|
||||
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
|
||||
}
|
||||
|
||||
func isProcessDNSRule(rule option.DefaultDNSRule) bool {
|
||||
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
|
||||
}
|
||||
|
||||
func isProcessHeadlessRule(rule option.DefaultHeadlessRule) bool {
|
||||
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0
|
||||
}
|
||||
|
||||
func notPrivateNode(code string) bool {
|
||||
return code != "private"
|
||||
}
|
||||
|
||||
func isWIFIRule(rule option.DefaultRule) bool {
|
||||
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
|
||||
}
|
||||
|
||||
func isWIFIDNSRule(rule option.DefaultDNSRule) bool {
|
||||
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
|
||||
}
|
||||
|
||||
func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool {
|
||||
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
|
||||
}
|
||||
@@ -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,13 @@ func (r *abstractLogicalRule) Close() error {
|
||||
|
||||
func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
|
||||
if r.mode == C.LogicalTypeAnd {
|
||||
return common.All(r.rules, func(it adapter.Rule) bool {
|
||||
return common.All(r.rules, func(it adapter.HeadlessRule) bool {
|
||||
metadata.ResetRuleCache()
|
||||
return it.Match(metadata)
|
||||
}) != r.invert
|
||||
} else {
|
||||
return common.Any(r.rules, func(it adapter.Rule) bool {
|
||||
return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
|
||||
metadata.ResetRuleCache()
|
||||
return it.Match(metadata)
|
||||
}) != r.invert
|
||||
}
|
||||
|
||||
@@ -194,6 +194,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.RuleSet) > 0 {
|
||||
item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
@@ -206,7 +211,7 @@ type LogicalRule struct {
|
||||
func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) {
|
||||
r := &LogicalRule{
|
||||
abstractLogicalRule{
|
||||
rules: make([]adapter.Rule, len(options.Rules)),
|
||||
rules: make([]adapter.HeadlessRule, len(options.Rules)),
|
||||
invert: options.Invert,
|
||||
outbound: options.Outbound,
|
||||
},
|
||||
|
||||
@@ -190,6 +190,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.RuleSet) > 0 {
|
||||
item := NewRuleSetItem(router, options.RuleSet, false)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
@@ -212,7 +217,7 @@ type LogicalDNSRule struct {
|
||||
func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
|
||||
r := &LogicalDNSRule{
|
||||
abstractLogicalRule: abstractLogicalRule{
|
||||
rules: make([]adapter.Rule, len(options.Rules)),
|
||||
rules: make([]adapter.HeadlessRule, len(options.Rules)),
|
||||
invert: options.Invert,
|
||||
outbound: options.Server,
|
||||
},
|
||||
|
||||
173
route/rule_headless.go
Normal file
173
route/rule_headless.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func NewHeadlessRule(router adapter.Router, options option.HeadlessRule) (adapter.HeadlessRule, error) {
|
||||
switch options.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
if !options.DefaultOptions.IsValid() {
|
||||
return nil, E.New("missing conditions")
|
||||
}
|
||||
return NewDefaultHeadlessRule(router, options.DefaultOptions)
|
||||
case C.RuleTypeLogical:
|
||||
if !options.LogicalOptions.IsValid() {
|
||||
return nil, E.New("missing conditions")
|
||||
}
|
||||
return NewLogicalHeadlessRule(router, options.LogicalOptions)
|
||||
default:
|
||||
return nil, E.New("unknown rule type: ", options.Type)
|
||||
}
|
||||
}
|
||||
|
||||
var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil)
|
||||
|
||||
type DefaultHeadlessRule struct {
|
||||
abstractDefaultRule
|
||||
}
|
||||
|
||||
func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) {
|
||||
rule := &DefaultHeadlessRule{
|
||||
abstractDefaultRule{
|
||||
invert: options.Invert,
|
||||
},
|
||||
}
|
||||
if len(options.Network) > 0 {
|
||||
item := NewNetworkItem(options.Network)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
|
||||
item := NewDomainItem(options.Domain, options.DomainSuffix)
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
} else if options.DomainMatcher != nil {
|
||||
item := NewRawDomainItem(options.DomainMatcher)
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.DomainKeyword) > 0 {
|
||||
item := NewDomainKeywordItem(options.DomainKeyword)
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.DomainRegex) > 0 {
|
||||
item, err := NewDomainRegexItem(options.DomainRegex)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "domain_regex")
|
||||
}
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.SourceIPCIDR) > 0 {
|
||||
item, err := NewIPCIDRItem(true, options.SourceIPCIDR)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "source_ipcidr")
|
||||
}
|
||||
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
} else if options.SourceIPSet != nil {
|
||||
item := NewRawIPCIDRItem(true, options.SourceIPSet)
|
||||
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.IPCIDR) > 0 {
|
||||
item, err := NewIPCIDRItem(false, options.IPCIDR)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "ipcidr")
|
||||
}
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
} else if options.IPSet != nil {
|
||||
item := NewRawIPCIDRItem(false, options.IPSet)
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.SourcePort) > 0 {
|
||||
item := NewPortItem(true, options.SourcePort)
|
||||
rule.sourcePortItems = append(rule.sourcePortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.SourcePortRange) > 0 {
|
||||
item, err := NewPortRangeItem(true, options.SourcePortRange)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "source_port_range")
|
||||
}
|
||||
rule.sourcePortItems = append(rule.sourcePortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.Port) > 0 {
|
||||
item := NewPortItem(false, options.Port)
|
||||
rule.destinationPortItems = append(rule.destinationPortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.PortRange) > 0 {
|
||||
item, err := NewPortRangeItem(false, options.PortRange)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "port_range")
|
||||
}
|
||||
rule.destinationPortItems = append(rule.destinationPortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.ProcessName) > 0 {
|
||||
item := NewProcessItem(options.ProcessName)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.ProcessPath) > 0 {
|
||||
item := NewProcessPathItem(options.ProcessPath)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.PackageName) > 0 {
|
||||
item := NewPackageNameItem(options.PackageName)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.WIFISSID) > 0 {
|
||||
item := NewWIFISSIDItem(router, options.WIFISSID)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.WIFIBSSID) > 0 {
|
||||
item := NewWIFIBSSIDItem(router, options.WIFIBSSID)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil)
|
||||
|
||||
type LogicalHeadlessRule struct {
|
||||
abstractLogicalRule
|
||||
}
|
||||
|
||||
func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) {
|
||||
r := &LogicalHeadlessRule{
|
||||
abstractLogicalRule{
|
||||
rules: make([]adapter.HeadlessRule, len(options.Rules)),
|
||||
invert: options.Invert,
|
||||
},
|
||||
}
|
||||
switch options.Mode {
|
||||
case C.LogicalTypeAnd:
|
||||
r.mode = C.LogicalTypeAnd
|
||||
case C.LogicalTypeOr:
|
||||
r.mode = C.LogicalTypeOr
|
||||
default:
|
||||
return nil, E.New("unknown logical mode: ", options.Mode)
|
||||
}
|
||||
for i, subRule := range options.Rules {
|
||||
rule, err := NewHeadlessRule(router, subRule)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "sub rule[", i, "]")
|
||||
}
|
||||
r.rules[i] = rule
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) {
|
||||
builder.Add(addr)
|
||||
continue
|
||||
}
|
||||
return nil, E.Cause(err, "parse ip_cidr [", i, "]")
|
||||
return nil, E.Cause(err, "parse [", i, "]")
|
||||
}
|
||||
var description string
|
||||
if isSource {
|
||||
@@ -57,8 +57,23 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem {
|
||||
var description string
|
||||
if isSource {
|
||||
description = "source_ipcidr="
|
||||
} else {
|
||||
description = "ipcidr="
|
||||
}
|
||||
description += "<binary>"
|
||||
return &IPCIDRItem{
|
||||
ipSet: ipSet,
|
||||
isSource: isSource,
|
||||
description: description,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if r.isSource {
|
||||
if r.isSource || metadata.QueryType != 0 || metadata.IPCIDRMatchSource {
|
||||
return r.ipSet.Contains(metadata.Source.Addr)
|
||||
} else {
|
||||
if metadata.Destination.IsIP() {
|
||||
|
||||
@@ -43,6 +43,13 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem {
|
||||
}
|
||||
}
|
||||
|
||||
func NewRawDomainItem(matcher *domain.Matcher) *DomainItem {
|
||||
return &DomainItem{
|
||||
matcher,
|
||||
"domain/domain_suffix=<binary>",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DomainItem) Match(metadata *adapter.InboundContext) bool {
|
||||
var domainHost string
|
||||
if metadata.Domain != "" {
|
||||
|
||||
55
route/rule_item_rule_set.go
Normal file
55
route/rule_item_rule_set.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*RuleSetItem)(nil)
|
||||
|
||||
type RuleSetItem struct {
|
||||
router adapter.Router
|
||||
tagList []string
|
||||
setList []adapter.HeadlessRule
|
||||
ipcidrMatchSource bool
|
||||
}
|
||||
|
||||
func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem {
|
||||
return &RuleSetItem{
|
||||
router: router,
|
||||
tagList: tagList,
|
||||
ipcidrMatchSource: ipCIDRMatchSource,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) Start() error {
|
||||
for _, tag := range r.tagList {
|
||||
ruleSet, loaded := r.router.RuleSet(tag)
|
||||
if !loaded {
|
||||
return E.New("rule-set not found: ", tag)
|
||||
}
|
||||
r.setList = append(r.setList, ruleSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
|
||||
metadata.IPCIDRMatchSource = r.ipcidrMatchSource
|
||||
for _, ruleSet := range r.setList {
|
||||
if ruleSet.Match(metadata) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) String() string {
|
||||
if len(r.tagList) == 1 {
|
||||
return F.ToString("rule_set=", r.tagList[0])
|
||||
} else {
|
||||
return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]")
|
||||
}
|
||||
}
|
||||
67
route/rule_set.go
Normal file
67
route/rule_set.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) {
|
||||
switch options.Type {
|
||||
case C.RuleSetTypeLocal:
|
||||
return NewLocalRuleSet(router, options)
|
||||
case C.RuleSetTypeRemote:
|
||||
return NewRemoteRuleSet(ctx, router, logger, options), nil
|
||||
default:
|
||||
return nil, E.New("unknown rule set type: ", options.Type)
|
||||
}
|
||||
}
|
||||
|
||||
var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil)
|
||||
|
||||
type RuleSetStartContext struct {
|
||||
access sync.Mutex
|
||||
httpClientCache map[string]*http.Client
|
||||
}
|
||||
|
||||
func NewRuleSetStartContext() *RuleSetStartContext {
|
||||
return &RuleSetStartContext{
|
||||
httpClientCache: make(map[string]*http.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RuleSetStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||
return httpClient
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
},
|
||||
}
|
||||
c.httpClientCache[detour] = httpClient
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *RuleSetStartContext) Close() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
for _, client := range c.httpClientCache {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
84
route/rule_set_local.go
Normal file
84
route/rule_set_local.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.RuleSet = (*LocalRuleSet)(nil)
|
||||
|
||||
type LocalRuleSet struct {
|
||||
rules []adapter.HeadlessRule
|
||||
metadata adapter.RuleSetMetadata
|
||||
}
|
||||
|
||||
func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) {
|
||||
var plainRuleSet option.PlainRuleSet
|
||||
switch options.Format {
|
||||
case C.RuleSetFormatSource, "":
|
||||
content, err := os.ReadFile(options.LocalOptions.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plainRuleSet = compat.Upgrade()
|
||||
case C.RuleSetFormatBinary:
|
||||
setFile, err := os.Open(options.LocalOptions.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plainRuleSet, err = srs.Read(setFile, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, E.New("unknown rule set format: ", options.Format)
|
||||
}
|
||||
rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules))
|
||||
var err error
|
||||
for i, ruleOptions := range plainRuleSet.Rules {
|
||||
rules[i], err = NewHeadlessRule(router, ruleOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||
}
|
||||
}
|
||||
var metadata adapter.RuleSetMetadata
|
||||
metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
|
||||
metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
|
||||
return &LocalRuleSet{rules, metadata}, nil
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
|
||||
for _, rule := range s.rules {
|
||||
if rule.Match(metadata) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) PostStart() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
return s.metadata
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Close() error {
|
||||
return nil
|
||||
}
|
||||
262
route/rule_set_remote.go
Normal file
262
route/rule_set_remote.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
|
||||
var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
|
||||
|
||||
type RemoteRuleSet struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
router adapter.Router
|
||||
logger logger.ContextLogger
|
||||
options option.RuleSet
|
||||
metadata adapter.RuleSetMetadata
|
||||
updateInterval time.Duration
|
||||
dialer N.Dialer
|
||||
rules []adapter.HeadlessRule
|
||||
lastUpdated time.Time
|
||||
lastEtag string
|
||||
updateTicker *time.Ticker
|
||||
pauseManager pause.Manager
|
||||
}
|
||||
|
||||
func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
var updateInterval time.Duration
|
||||
if options.RemoteOptions.UpdateInterval > 0 {
|
||||
updateInterval = time.Duration(options.RemoteOptions.UpdateInterval)
|
||||
} else {
|
||||
updateInterval = 24 * time.Hour
|
||||
}
|
||||
return &RemoteRuleSet{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
router: router,
|
||||
logger: logger,
|
||||
options: options,
|
||||
updateInterval: updateInterval,
|
||||
pauseManager: pause.ManagerFromContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
|
||||
for _, rule := range s.rules {
|
||||
if rule.Match(metadata) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
|
||||
var dialer N.Dialer
|
||||
if s.options.RemoteOptions.DownloadDetour != "" {
|
||||
outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour)
|
||||
if !loaded {
|
||||
return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour)
|
||||
}
|
||||
dialer = outbound
|
||||
} else {
|
||||
outbound, err := s.router.DefaultOutbound(N.NetworkTCP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dialer = outbound
|
||||
}
|
||||
s.dialer = dialer
|
||||
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
|
||||
if cacheFile != nil {
|
||||
if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil {
|
||||
err := s.loadBytes(savedSet.Content)
|
||||
if err != nil {
|
||||
return E.Cause(err, "restore cached rule-set")
|
||||
}
|
||||
s.lastUpdated = savedSet.LastUpdated
|
||||
s.lastEtag = savedSet.LastEtag
|
||||
}
|
||||
}
|
||||
if s.lastUpdated.IsZero() {
|
||||
err := s.fetchOnce(ctx, startContext)
|
||||
if err != nil {
|
||||
return E.Cause(err, "initial rule-set: ", s.options.Tag)
|
||||
}
|
||||
}
|
||||
s.updateTicker = time.NewTicker(s.updateInterval)
|
||||
go s.loopUpdate()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) PostStart() error {
|
||||
if s.lastUpdated.IsZero() {
|
||||
err := s.fetchOnce(s.ctx, nil)
|
||||
if err != nil {
|
||||
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
return s.metadata
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) loadBytes(content []byte) error {
|
||||
var (
|
||||
plainRuleSet option.PlainRuleSet
|
||||
err error
|
||||
)
|
||||
switch s.options.Format {
|
||||
case C.RuleSetFormatSource, "":
|
||||
var compat option.PlainRuleSetCompat
|
||||
compat, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plainRuleSet = compat.Upgrade()
|
||||
case C.RuleSetFormatBinary:
|
||||
plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return E.New("unknown rule set format: ", s.options.Format)
|
||||
}
|
||||
rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules))
|
||||
for i, ruleOptions := range plainRuleSet.Rules {
|
||||
rules[i], err = NewHeadlessRule(s.router, ruleOptions)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||
}
|
||||
}
|
||||
s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
|
||||
s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
|
||||
s.rules = rules
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) loopUpdate() {
|
||||
if time.Since(s.lastUpdated) > s.updateInterval {
|
||||
err := s.fetchOnce(s.ctx, nil)
|
||||
if err != nil {
|
||||
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
||||
}
|
||||
}
|
||||
for {
|
||||
runtime.GC()
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-s.updateTicker.C:
|
||||
s.pauseManager.WaitActive()
|
||||
err := s.fetchOnce(s.ctx, nil)
|
||||
if err != nil {
|
||||
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.RuleSetStartContext) error {
|
||||
s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL)
|
||||
var httpClient *http.Client
|
||||
if startContext != nil {
|
||||
httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer)
|
||||
} else {
|
||||
httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.lastEtag != "" {
|
||||
request.Header.Set("If-None-Match", s.lastEtag)
|
||||
}
|
||||
response, err := httpClient.Do(request.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch response.StatusCode {
|
||||
case http.StatusOK:
|
||||
case http.StatusNotModified:
|
||||
s.lastUpdated = time.Now()
|
||||
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
|
||||
if cacheFile != nil {
|
||||
savedRuleSet := cacheFile.LoadRuleSet(s.options.Tag)
|
||||
if savedRuleSet != nil {
|
||||
savedRuleSet.LastUpdated = s.lastUpdated
|
||||
err = cacheFile.SaveRuleSet(s.options.Tag, savedRuleSet)
|
||||
if err != nil {
|
||||
s.logger.Error("save rule-set updated time: ", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
s.logger.Info("update rule-set ", s.options.Tag, ": not modified")
|
||||
return nil
|
||||
default:
|
||||
return E.New("unexpected status: ", response.Status)
|
||||
}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
response.Body.Close()
|
||||
return err
|
||||
}
|
||||
err = s.loadBytes(content)
|
||||
if err != nil {
|
||||
response.Body.Close()
|
||||
return err
|
||||
}
|
||||
response.Body.Close()
|
||||
eTagHeader := response.Header.Get("Etag")
|
||||
if eTagHeader != "" {
|
||||
s.lastEtag = eTagHeader
|
||||
}
|
||||
s.lastUpdated = time.Now()
|
||||
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
|
||||
if cacheFile != nil {
|
||||
err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{
|
||||
LastUpdated: s.lastUpdated,
|
||||
Content: content,
|
||||
LastEtag: s.lastEtag,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("save rule-set cache: ", err)
|
||||
}
|
||||
}
|
||||
s.logger.Info("updated rule-set ", s.options.Tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) Close() error {
|
||||
s.updateTicker.Stop()
|
||||
s.cancel()
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user