From c54a5a0c30b02931426dcc75b24bf9f8f9e4eaa7 Mon Sep 17 00:00:00 2001 From: mzz <30586220+mzz2017@users.noreply.github.com> Date: Fri, 10 Feb 2023 11:04:16 +0800 Subject: [PATCH] feat: support subscription tag and subtag filter (#13) --- README.md | 1 - cmd/internal/subscription.go | 31 +++++++++++++----- cmd/run.go | 17 ++++++---- component/outbound/filter.go | 61 +++++++++++++++++++++++++++--------- control/control_plane.go | 6 ++-- control/dns_upstream.go | 2 +- control/udp.go | 2 +- go.mod | 2 +- go.sum | 4 +-- 9 files changed, 89 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 82990b8..9a2c756 100644 --- a/README.md +++ b/README.md @@ -121,5 +121,4 @@ See [example.dae](https://github.com/v2rayA/dae/blob/main/example.dae). But the problem is, after the Linux network stack, before entering the network card, we modify the source IP of this packet, causing the Linux network stack to only make a simple checksum, and the NIC also assumes that this packet is not sent from local, so no further checksum completing. 1. MACv2 extension extraction. 1. Log to userspace. -1. Subscription section supports key. And support to filter by subscription key. 1. ... diff --git a/cmd/internal/subscription.go b/cmd/internal/subscription.go index 35c08e0..72fba1b 100644 --- a/cmd/internal/subscription.go +++ b/cmd/internal/subscription.go @@ -94,7 +94,7 @@ func resolveFile(u *url.URL, configDir string) (b []byte, err error) { if err = common.EnsureFileInSubDir(path, configDir); err != nil { return nil, err } - /// Read and resolve + /// Read and resolve. f, err := os.Open(path) if err != nil { return nil, err @@ -131,10 +131,25 @@ func resolveFile(u *url.URL, configDir string) (b []byte, err error) { return bytes.TrimSpace(b), err } -func ResolveSubscription(log *logrus.Logger, configDir string, subscription string) (nodes []string, err error) { +func ResolveSubscription(log *logrus.Logger, configDir string, subscription string) (tag string, nodes []string, err error) { + /// Get tag. + iColon := strings.Index(subscription, ":") + if iColon == -1 { + goto parseUrl + } + // If first colon is like "://" in "scheme://linkbody", no tag is present. + if strings.HasPrefix(subscription[iColon:], "://") { + goto parseUrl + } + // Else tag is the part before colon. + tag = subscription[:iColon] + subscription = subscription[iColon+1:] + + /// Parse url. +parseUrl: u, err := url.Parse(subscription) if err != nil { - return nil, fmt.Errorf("failed to parse subscription \"%v\": %w", subscription, err) + return tag, nil, fmt.Errorf("failed to parse subscription \"%v\": %w", subscription, err) } log.Debugf("ResolveSubscription: %v", subscription) var ( @@ -145,25 +160,25 @@ func ResolveSubscription(log *logrus.Logger, configDir string, subscription stri case "file": b, err = resolveFile(u, configDir) if err != nil { - return nil, err + return "", nil, err } goto resolve default: } resp, err = http.Get(subscription) if err != nil { - return nil, err + return "", nil, err } defer resp.Body.Close() b, err = io.ReadAll(resp.Body) if err != nil { - return nil, err + return "", nil, err } resolve: if nodes, err = resolveSubscriptionAsSIP008(log, b); err == nil { - return nodes, nil + return tag, nodes, nil } else { log.Debugln(err) } - return resolveSubscriptionAsBase64(log, b), nil + return tag, resolveSubscriptionAsBase64(log, b), nil } diff --git a/cmd/run.go b/cmd/run.go index e2a5976..56d3035 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -54,17 +54,22 @@ func init() { func Run(log *logrus.Logger, param *config.Params) (err error) { + /// Get tag -> nodeList mapping. + tagToNodeList := map[string][]string{} + if len(param.Node) > 0 { + tagToNodeList[""] = append(tagToNodeList[""], param.Node...) + } // Resolve subscriptions to nodes. - nodeList := make([]string, len(param.Node)) - copy(nodeList, param.Node) for _, sub := range param.Subscription { - nodes, err := internal.ResolveSubscription(log, filepath.Dir(cfgFile), sub) + tag, nodes, err := internal.ResolveSubscription(log, filepath.Dir(cfgFile), sub) if err != nil { log.Warnf(`failed to resolve subscription "%v": %v`, sub, err) } - nodeList = append(nodeList, nodes...) + if len(nodes) > 0 { + tagToNodeList[tag] = append(tagToNodeList[tag], nodes...) + } } - if len(nodeList) == 0 { + if len(tagToNodeList) == 0 { return fmt.Errorf("no node found, which could because all subscription resolving failed") } @@ -75,7 +80,7 @@ func Run(log *logrus.Logger, param *config.Params) (err error) { // New ControlPlane. t, err := control.NewControlPlane( log, - nodeList, + tagToNodeList, param.Group, ¶m.Routing, ¶m.Global, diff --git a/component/outbound/filter.go b/component/outbound/filter.go index 0a414b2..3a16a6d 100644 --- a/component/outbound/filter.go +++ b/component/outbound/filter.go @@ -14,36 +14,47 @@ import ( ) const ( - FilterInput_Name = "name" - FilterInput_Link = "link" + FilterInput_Name = "name" + FilterInput_SubscriptionTag = "subtag" + FilterInput_Link = "link" ) const ( FilterKey_Name_Regex = "regex" FilterKey_Name_Keyword = "keyword" + + FilterInput_SubscriptionTag_Regex = "regex" ) type DialerSet struct { - Dialers []*dialer.Dialer + dialers []*dialer.Dialer + nodeToTagMap map[*dialer.Dialer]string } -func NewDialerSetFromLinks(option *dialer.GlobalOption, nodes []string) *DialerSet { - s := &DialerSet{Dialers: make([]*dialer.Dialer, 0, len(nodes))} - for _, node := range nodes { - d, err := dialer.NewFromLink(option, dialer.InstanceOption{CheckEnabled: false}, node) - if err != nil { - option.Log.Infof("failed to parse node: %v: %v", node, err) - continue +func NewDialerSetFromLinks(option *dialer.GlobalOption, tagToNodeList map[string][]string) *DialerSet { + s := &DialerSet{ + dialers: make([]*dialer.Dialer, 0), + nodeToTagMap: make(map[*dialer.Dialer]string), + } + for subscriptionTag, nodes := range tagToNodeList { + for _, node := range nodes { + d, err := dialer.NewFromLink(option, dialer.InstanceOption{CheckEnabled: false}, node) + if err != nil { + option.Log.Infof("failed to parse node: %v: %v", node, err) + continue + } + s.dialers = append(s.dialers, d) + s.nodeToTagMap[d] = subscriptionTag } - s.Dialers = append(s.Dialers, d) } return s } -func filterHit(dialer *dialer.Dialer, filters []*config_parser.Function) (hit bool, err error) { +func (s *DialerSet) filterHit(dialer *dialer.Dialer, filters []*config_parser.Function) (hit bool, err error) { // Example // filter: name(regex:'^.*hk.*$', keyword:'sg') && name(keyword:'disney') // filter: !name(regex: 'HK|TW|SG') && name(keyword: disney) + // filter: subtag(my_sub, regex:^my_, regex:my_) // And for _, filter := range filters { @@ -72,6 +83,28 @@ func filterHit(dialer *dialer.Dialer, filters []*config_parser.Function) (hit bo return false, fmt.Errorf(`unsupported filter key "%v" in "filter: %v()"`, param.Key, filter.Name) } } + case FilterInput_SubscriptionTag: + // Or + for _, param := range filter.Params { + switch param.Key { + case FilterInput_SubscriptionTag_Regex: + matched, _ := regexp.MatchString(param.Val, s.nodeToTagMap[dialer]) + //logrus.Warnln(param.Val, matched, dialer.Name()) + if matched { + subFilterHit = true + break + } + case "": + // Full + if s.nodeToTagMap[dialer] == param.Val { + subFilterHit = true + break + } + default: + return false, fmt.Errorf(`unsupported filter key "%v" in "filter: %v()"`, param.Key, filter.Name) + } + } + default: return false, fmt.Errorf(`unsupported filter input type: "%v"`, filter.Name) } @@ -84,8 +117,8 @@ func filterHit(dialer *dialer.Dialer, filters []*config_parser.Function) (hit bo } func (s *DialerSet) Filter(filters []*config_parser.Function) (dialers []*dialer.Dialer, err error) { - for _, d := range s.Dialers { - hit, err := filterHit(d, filters) + for _, d := range s.dialers { + hit, err := s.filterHit(d, filters) if err != nil { return nil, err } diff --git a/control/control_plane.go b/control/control_plane.go index 8b91bd9..a43e802 100644 --- a/control/control_plane.go +++ b/control/control_plane.go @@ -56,7 +56,7 @@ type ControlPlane struct { func NewControlPlane( log *logrus.Logger, - nodes []string, + tagToNodeList map[string][]string, groups []config.Group, routingA *config.Routing, global *config.Global, @@ -214,7 +214,7 @@ func NewControlPlane( } // Filter out groups. - dialerSet := outbound.NewDialerSetFromLinks(option, nodes) + dialerSet := outbound.NewDialerSetFromLinks(option, tagToNodeList) for _, group := range groups { // Parse policy. policy, err := outbound.NewDialerSelectionPolicyFromGroupParam(&group.Param) @@ -304,7 +304,7 @@ func NewControlPlane( /// DNS upstream c.dnsUpstream.FinishInitCallback = c.finishInitDnsUpstreamResolve // Try to invoke once to avoid dns leaking at the very beginning. - _, _ = c.dnsUpstream.Upstream() + _, _ = c.dnsUpstream.GetUpstream() return c, nil } diff --git a/control/dns_upstream.go b/control/dns_upstream.go index c9c58bd..0046cda 100644 --- a/control/dns_upstream.go +++ b/control/dns_upstream.go @@ -106,7 +106,7 @@ type DnsUpstreamRaw struct { init bool } -func (u *DnsUpstreamRaw) Upstream() (_ *DnsUpstream, err error) { +func (u *DnsUpstreamRaw) GetUpstream() (_ *DnsUpstream, err error) { u.mu.Lock() defer u.mu.Unlock() if !u.init { diff --git a/control/udp.go b/control/udp.go index d563834..2c1508c 100644 --- a/control/udp.go +++ b/control/udp.go @@ -179,7 +179,7 @@ func (c *ControlPlane) handlePkt(data []byte, src, dst netip.AddrPort, outboundI // For DNS request, modify dst to dns upstream. // NOTICE: We might modify l4proto and ipversion. - dnsUpstream, err := c.dnsUpstream.Upstream() + dnsUpstream, err := c.dnsUpstream.GetUpstream() if err != nil { return err } diff --git a/go.mod b/go.mod index eeb4d8a..1a0a5ba 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/json-iterator/go v1.1.12 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/mzz2017/softwind v0.0.0-20230209162720-f6e9c7790fb1 + github.com/mzz2017/softwind v0.0.0-20230210023945-506610874f4c github.com/safchain/ethtool v0.0.0-20230116090318-67cc41908669 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.6.1 diff --git a/go.sum b/go.sum index bdb6594..78544cf 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,8 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mzz2017/disk-bloom v1.0.1 h1:rEF9MiXd9qMW3ibRpqcerLXULoTgRlM21yqqJl1B90M= github.com/mzz2017/disk-bloom v1.0.1/go.mod h1:JLHETtUu44Z6iBmsqzkOtFlRvXSlKnxjwiBRDapizDI= -github.com/mzz2017/softwind v0.0.0-20230209162720-f6e9c7790fb1 h1:91iNjZEiaDIDedusC5Five6KbEbt2ItwvFE/DylPMWo= -github.com/mzz2017/softwind v0.0.0-20230209162720-f6e9c7790fb1/go.mod h1:K1nXwtBokwEsfOfdT/5zV6R8QabGkyhcR0iuTrRZcYY= +github.com/mzz2017/softwind v0.0.0-20230210023945-506610874f4c h1:NoVnf5PsBMiuZRq+XewAWq61dgypkGfNpPZzDjw9bgY= +github.com/mzz2017/softwind v0.0.0-20230210023945-506610874f4c/go.mod h1:K1nXwtBokwEsfOfdT/5zV6R8QabGkyhcR0iuTrRZcYY= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=