diff --git a/adapter/router.go b/adapter/router.go index 54dc3396dc..fc15d323b8 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -10,15 +10,18 @@ import ( "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" mdns "github.com/miekg/dns" + "go4.org/netipx" ) type Router interface { Service PreStarter PostStarter + Cleanup() error Outbounds() []Outbound Outbound(tag string) (Outbound, bool) @@ -92,12 +95,21 @@ type DNSRule interface { } type RuleSet interface { + Name() string StartContext(ctx context.Context, startContext RuleSetStartContext) error Metadata() RuleSetMetadata + ExtractIPSet() []*netipx.IPSet + IncRef() + DecRef() + Cleanup() + RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback] + UnregisterCallback(element *list.Element[RuleSetUpdateCallback]) Close() error HeadlessRule } +type RuleSetUpdateCallback func(it RuleSet) + type RuleSetMetadata struct { ContainsProcessRule bool ContainsWIFIRule bool diff --git a/box.go b/box.go index 4e7a9879b8..75646de585 100644 --- a/box.go +++ b/box.go @@ -303,7 +303,11 @@ func (s *Box) start() error { return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") } } - return s.postStart() + err = s.postStart() + if err != nil { + return err + } + return s.router.Cleanup() } func (s *Box) postStart() error { @@ -313,16 +317,28 @@ func (s *Box) postStart() error { return E.Cause(err, "start ", serviceName) } } - for _, outbound := range s.outbounds { - if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound { + // TODO: reorganize ALL start order + for _, out := range s.outbounds { + if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound { err := lateOutbound.PostStart() if err != nil { - return E.Cause(err, "post-start outbound/", outbound.Tag()) + return E.Cause(err, "post-start outbound/", out.Tag()) } } } - - return s.router.PostStart() + err := s.router.PostStart() + if err != nil { + return err + } + for _, in := range s.inbounds { + if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound { + err = lateInbound.PostStart() + if err != nil { + return E.Cause(err, "post-start inbound/", in.Tag()) + } + } + } + return nil } func (s *Box) Close() error { diff --git a/go.mod b/go.mod index 5680cd7f0c..a84f87ea3b 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.6 github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.1.4 - github.com/sagernet/sing-tun v0.4.0-beta.9 + github.com/sagernet/sing-tun v0.4.0-beta.9.0.20240612071945-28df5b4b41f5 github.com/sagernet/sing-vmess v0.1.8 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 @@ -81,7 +81,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect - github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba // indirect + github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.2 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect diff --git a/go.sum b/go.sum index b999efc28a..3dcd821b6a 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,8 @@ github.com/sagernet/gomobile v0.1.3 h1:ohjIb1Ou2+1558PnZour3od69suSuvkdSVOlO1tC4 github.com/sagernet/gomobile v0.1.3/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E= github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I= github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0= -github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba h1:EY5AS7CCtfmARNv2zXUOrsEMPFDGYxaw65JzA2p51Vk= -github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.2 h1:yKqMl4Dpb6nKxAmlE6fXjJRlLO2c1f2wyNFBg4hBr8w= github.com/sagernet/nftables v0.3.0-beta.2/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.45.0-beta.2 h1:nWq9KJTR+cGU8UU4E20XNjdM6QgbLkBgpq+NCExg5RY= @@ -127,8 +127,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wK github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= -github.com/sagernet/sing-tun v0.4.0-beta.9 h1:/5hXQ0u7tHtngfXozRc+o/gt6zfHBHMOwSIHXF0+S3I= -github.com/sagernet/sing-tun v0.4.0-beta.9/go.mod h1:uoRiCzWHzHLw/angVqXDzUNiQcMRl/ZrElJryQLJFhY= +github.com/sagernet/sing-tun v0.4.0-beta.9.0.20240612071945-28df5b4b41f5 h1:OpRvbxi+9zZ4LmWo1GqlWXGLOdCquFg5J3qh6uUwTBU= +github.com/sagernet/sing-tun v0.4.0-beta.9.0.20240612071945-28df5b4b41f5/go.mod h1:yaLwsfgGeRTsgupm3MOz/6r7EJ74qIgZCyKLNW/6aHE= github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc= github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= diff --git a/inbound/tun.go b/inbound/tun.go index a0e7f5fa51..98f54c5859 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -20,25 +20,34 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ranges" + "github.com/sagernet/sing/common/x/list" + + "go4.org/netipx" ) var _ adapter.Inbound = (*Tun)(nil) type Tun struct { - tag string - ctx context.Context - router adapter.Router - logger log.ContextLogger - inboundOptions option.InboundOptions - tunOptions tun.Options - endpointIndependentNat bool - udpTimeout int64 - stack string - tunIf tun.Tun - tunStack tun.Stack - platformInterface platform.Interface - platformOptions option.TunPlatformOptions - autoRedirect tun.AutoRedirect + tag string + ctx context.Context + router adapter.Router + logger log.ContextLogger + inboundOptions option.InboundOptions + tunOptions tun.Options + endpointIndependentNat bool + udpTimeout int64 + stack string + tunIf tun.Tun + tunStack tun.Stack + platformInterface platform.Interface + platformOptions option.TunPlatformOptions + autoRedirect tun.AutoRedirect + routeRuleSet []adapter.RuleSet + routeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback] + routeExcludeRuleSet []adapter.RuleSet + routeExcludeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback] + routeAddressSet []*netipx.IPSet + routeExcludeAddressSet []*netipx.IPSet } func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) { @@ -81,6 +90,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger Inet4Address: options.Inet4Address, Inet6Address: options.Inet6Address, AutoRoute: options.AutoRoute, + AutoRedirect: options.AutoRedirect, StrictRoute: options.StrictRoute, IncludeInterface: options.IncludeInterface, ExcludeInterface: options.ExcludeInterface, @@ -108,15 +118,33 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger } disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES")) inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ - TunOptions: &inbound.tunOptions, - Context: ctx, - Handler: inbound, - Logger: logger, - TableName: "sing-box", - DisableNFTables: dErr == nil && disableNFTables, + TunOptions: &inbound.tunOptions, + Context: ctx, + Handler: inbound, + Logger: logger, + TableName: "sing-box", + DisableNFTables: dErr == nil && disableNFTables, + RouteAddressSet: &inbound.routeAddressSet, + RouteExcludeAddressSet: &inbound.routeExcludeAddressSet, }) if err != nil { - return nil, E.Cause(err, "initialize auto redirect") + return nil, E.Cause(err, "initialize auto-redirect") + } + for _, routeAddressSet := range options.RouteAddressSet { + ruleSet, loaded := router.RuleSet(routeAddressSet) + if !loaded { + return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet) + } + ruleSet.IncRef() + inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet) + } + for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { + ruleSet, loaded := router.RuleSet(routeExcludeAddressSet) + if !loaded { + return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet) + } + ruleSet.IncRef() + inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet) } } return inbound, nil @@ -215,18 +243,45 @@ func (t *Tun) Start() error { if err != nil { return err } + t.logger.Info("started at ", t.tunOptions.Name) + return nil +} + +func (t *Tun) PostStart() error { + monitor := taskmonitor.New(t.logger, C.StartTimeout) + // TODO: add ref counter to rule-set and release them if not needed + for _, ruleSet := range t.routeRuleSet { + t.routeAddressSet = append(t.routeAddressSet, ruleSet.ExtractIPSet()...) + } + for _, ruleSet := range t.routeExcludeRuleSet { + t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ruleSet.ExtractIPSet()...) + } if t.autoRedirect != nil { - monitor.Start("initiating auto redirect") - err = t.autoRedirect.Start() + monitor.Start("initiating auto-redirect") + err := t.autoRedirect.Start() monitor.Finish() if err != nil { - return E.Cause(err, "auto redirect") + return E.Cause(err, "auto-redirect") } } - t.logger.Info("started at ", t.tunOptions.Name) + for _, routeRuleSet := range t.routeRuleSet { + t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + routeRuleSet.DecRef() + } + for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { + t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + routeExcludeRuleSet.DecRef() + } return nil } +func (t *Tun) updateRouteAddressSet(it adapter.RuleSet) { + err := t.autoRedirect.UpdateRouteAddressSet() + if err != nil { + t.logger.Error("update route address set ", it.Name(), ": ", err) + } +} + func (t *Tun) Close() error { return common.Close( t.tunStack, diff --git a/option/tun.go b/option/tun.go index 91930866ac..34cc84ba4a 100644 --- a/option/tun.go +++ b/option/tun.go @@ -3,30 +3,42 @@ package option import "net/netip" type TunInboundOptions struct { - InterfaceName string `json:"interface_name,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - GSO bool `json:"gso,omitempty"` - Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"` - Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"` - AutoRoute bool `json:"auto_route,omitempty"` - AutoRedirect bool `json:"auto_redirect,omitempty"` - StrictRoute bool `json:"strict_route,omitempty"` - Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` - Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` + InterfaceName string `json:"interface_name,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + GSO bool `json:"gso,omitempty"` + Address Listable[netip.Prefix] `json:"address,omitempty"` + AutoRoute bool `json:"auto_route,omitempty"` + AutoRedirect bool `json:"auto_redirect,omitempty"` + StrictRoute bool `json:"strict_route,omitempty"` + RouteAddress Listable[netip.Prefix] `json:"route_address,omitempty"` + RouteAddressSet Listable[string] `json:"route_address_set,omitempty"` + RouteExcludeAddress Listable[netip.Prefix] `json:"route_exclude_address,omitempty"` + RouteExcludeAddressSet Listable[string] `json:"route_exclude_address_set,omitempty"` + IncludeInterface Listable[string] `json:"include_interface,omitempty"` + ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` + IncludeUID Listable[uint32] `json:"include_uid,omitempty"` + IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` + ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` + ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` + IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` + IncludePackage Listable[string] `json:"include_package,omitempty"` + ExcludePackage Listable[string] `json:"exclude_package,omitempty"` + EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` + UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` + Stack string `json:"stack,omitempty"` + Platform *TunPlatformOptions `json:"platform,omitempty"` + InboundOptions + + // Deprecated: merged to `address` + Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"` + // Deprecated: merged to `address` + Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"` + // Deprecated: merged to `route_address` + Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` + // Deprecated: merged to `route_address` + Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` + // Deprecated: merged to `route_exclude_address` Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"` + // Deprecated: merged to `route_exclude_address` Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` - IncludeInterface Listable[string] `json:"include_interface,omitempty"` - ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` - IncludeUID Listable[uint32] `json:"include_uid,omitempty"` - IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` - ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` - ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` - IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` - IncludePackage Listable[string] `json:"include_package,omitempty"` - ExcludePackage Listable[string] `json:"exclude_package,omitempty"` - EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` - UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` - Stack string `json:"stack,omitempty"` - Platform *TunPlatformOptions `json:"platform,omitempty"` - InboundOptions } diff --git a/route/router.go b/route/router.go index bc068b6fd1..21a1d56d35 100644 --- a/route/router.go +++ b/route/router.go @@ -728,6 +728,14 @@ func (r *Router) PostStart() error { return nil } +func (r *Router) Cleanup() error { + for _, ruleSet := range r.ruleSetMap { + ruleSet.Cleanup() + } + runtime.GC() + return nil +} + func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { outbound, loaded := r.outboundByTag[tag] return outbound, loaded diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go index 482a9c7b45..4ecf8c18f0 100644 --- a/route/rule_item_rule_set.go +++ b/route/rule_item_rule_set.go @@ -32,6 +32,7 @@ func (r *RuleSetItem) Start() error { if !loaded { return E.New("rule-set not found: ", tag) } + ruleSet.IncRef() r.setList = append(r.setList, ruleSet) } return nil diff --git a/route/rule_set.go b/route/rule_set.go index f644fb406f..ff28858e58 100644 --- a/route/rule_set.go +++ b/route/rule_set.go @@ -9,10 +9,13 @@ import ( "github.com/sagernet/sing-box/adapter" 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" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + + "go4.org/netipx" ) func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { @@ -26,6 +29,24 @@ func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.Contex } } +func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet { + switch rule := rawRule.(type) { + case *DefaultHeadlessRule: + return common.FlatMap(rule.destinationIPCIDRItems, func(rawItem RuleItem) []*netipx.IPSet { + switch item := rawItem.(type) { + case *IPCIDRItem: + return []*netipx.IPSet{item.ipSet} + default: + return nil + } + }) + case *LogicalHeadlessRule: + return common.FlatMap(rule.rules, extractIPSetFromRule) + default: + panic("unexpected rule type") + } +} + var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil) type RuleSetStartContext struct { diff --git a/route/rule_set_local.go b/route/rule_set_local.go index 3945826708..e6d12882ac 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -9,16 +9,23 @@ import ( "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/atomic" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/x/list" + + "go4.org/netipx" ) var _ adapter.RuleSet = (*LocalRuleSet)(nil) type LocalRuleSet struct { + tag string rules []adapter.HeadlessRule metadata adapter.RuleSetMetadata + refs atomic.Int32 } func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { @@ -58,16 +65,11 @@ func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleS metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) - return &LocalRuleSet{rules, metadata}, nil + return &LocalRuleSet{tag: options.Tag, rules: rules, metadata: 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) Name() string { + return s.tag } func (s *LocalRuleSet) String() string { @@ -82,6 +84,43 @@ func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata { return s.metadata } +func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet { + return common.FlatMap(s.rules, extractIPSetFromRule) +} + +func (s *LocalRuleSet) IncRef() { + s.refs.Add(1) +} + +func (s *LocalRuleSet) DecRef() { + if s.refs.Add(-1) < 0 { + panic("rule-set: negative refs") + } +} + +func (s *LocalRuleSet) Cleanup() { + if s.refs.Load() == 0 { + s.rules = nil + } +} + +func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + return nil +} + +func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { +} + func (s *LocalRuleSet) Close() error { + s.rules = nil return nil } + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 8389c2f46b..a37dc3ff34 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -8,20 +8,26 @@ import ( "net/http" "runtime" "strings" + "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/atomic" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" + + "go4.org/netipx" ) var _ adapter.RuleSet = (*RemoteRuleSet)(nil) @@ -40,6 +46,9 @@ type RemoteRuleSet struct { lastEtag string updateTicker *time.Ticker pauseManager pause.Manager + callbackAccess sync.Mutex + callbacks list.List[adapter.RuleSetUpdateCallback] + refs atomic.Int32 } func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { @@ -61,13 +70,8 @@ func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger. } } -func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { - for _, rule := range s.rules { - if rule.Match(metadata) { - return true - } - } - return false +func (s *RemoteRuleSet) Name() string { + return s.options.Tag } func (s *RemoteRuleSet) String() string { @@ -116,6 +120,38 @@ func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { return s.metadata } +func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet { + return common.FlatMap(s.rules, extractIPSetFromRule) +} + +func (s *RemoteRuleSet) IncRef() { + s.refs.Add(1) +} + +func (s *RemoteRuleSet) DecRef() { + if s.refs.Add(-1) < 0 { + panic("rule-set: negative refs") + } +} + +func (s *RemoteRuleSet) Cleanup() { + if s.refs.Load() == 0 { + s.rules = nil + } +} + +func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + s.callbackAccess.Lock() + defer s.callbackAccess.Unlock() + return s.callbacks.PushBack(callback) +} + +func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { + s.callbackAccess.Lock() + defer s.callbackAccess.Unlock() + s.callbacks.Remove(element) +} + func (s *RemoteRuleSet) loadBytes(content []byte) error { var ( plainRuleSet option.PlainRuleSet @@ -148,6 +184,16 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) s.rules = rules + s.callbackAccess.Lock() + callbacks := s.callbacks.Array() + s.callbackAccess.Unlock() + for _, callback := range callbacks { + callback(s) + } + if s.refs.Load() == 0 { + s.rules = nil + runtime.GC() + } return nil } @@ -253,7 +299,17 @@ func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.Rule } func (s *RemoteRuleSet) Close() error { + s.rules = nil s.updateTicker.Stop() s.cancel() return nil } + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +}