Browse Source

Add geosite

世界 3 years ago
parent
commit
8392567962
13 changed files with 420 additions and 102 deletions
  1. 2 1
      .gitignore
  2. 4 0
      adapter/router.go
  3. 18 5
      common/domain/matcher.go
  4. 0 1
      common/domain/set.go
  5. 17 12
      common/geosite/reader.go
  6. 41 0
      common/geosite/rule.go
  7. 1 1
      go.mod
  8. 2 2
      go.sum
  9. 13 3
      option/route.go
  10. 188 77
      route/router.go
  11. 47 0
      route/rule.go
  12. 67 0
      route/rule_geosite.go
  13. 20 0
      route/rule_logical.go

+ 2 - 1
.gitignore

@@ -1,4 +1,5 @@
 /.idea/
 /vendor/
 /*.json
-/Country.mmdb
+/Country.mmdb
+/geosite.db

+ 4 - 0
adapter/router.go

@@ -5,6 +5,7 @@ import (
 	"net"
 
 	"github.com/oschwald/geoip2-golang"
+	"github.com/sagernet/sing-box/common/geosite"
 	N "github.com/sagernet/sing/common/network"
 )
 
@@ -16,9 +17,12 @@ type Router interface {
 	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
 	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
 	GeoIPReader() *geoip2.Reader
+	GeositeReader() *geosite.Reader
 }
 
 type Rule interface {
+	Start() error
+	Close() error
 	Match(metadata *InboundContext) bool
 	Outbound() string
 	String() string

+ 18 - 5
common/domain/matcher.go

@@ -1,19 +1,32 @@
 package domain
 
-import "unicode/utf8"
+import (
+	"sort"
+	"unicode/utf8"
+)
 
 type Matcher struct {
 	set *succinctSet
 }
 
 func NewMatcher(domains []string, domainSuffix []string) *Matcher {
-	var domainList []string
-	for _, domain := range domains {
-		domainList = append(domainList, reverseDomain(domain))
-	}
+	domainList := make([]string, 0, len(domains)+len(domainSuffix))
+	seen := make(map[string]bool, len(domainList))
 	for _, domain := range domainSuffix {
+		if seen[domain] {
+			continue
+		}
+		seen[domain] = true
 		domainList = append(domainList, reverseDomainSuffix(domain))
 	}
+	for _, domain := range domains {
+		if seen[domain] {
+			continue
+		}
+		seen[domain] = true
+		domainList = append(domainList, reverseDomain(domain))
+	}
+	sort.Strings(domainList)
 	return &Matcher{
 		newSuccinctSet(domainList),
 	}

+ 0 - 1
common/domain/set.go

@@ -35,7 +35,6 @@ func newSuccinctSet(keys []string) *succinctSet {
 			setBit(&ss.labelBitmap, lIdx, 0)
 			lIdx++
 		}
-
 		setBit(&ss.labelBitmap, lIdx, 1)
 		lIdx++
 	}

+ 17 - 12
common/geosite/reader.go

@@ -2,7 +2,7 @@ package geosite
 
 import (
 	"io"
-	"sync"
+	"os"
 
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/rw"
@@ -10,12 +10,26 @@ import (
 
 type Reader struct {
 	reader       io.ReadSeeker
-	access       sync.Mutex
-	metadataRead bool
 	domainIndex  map[string]int
 	domainLength map[string]int
 }
 
+func Open(path string) (*Reader, error) {
+	content, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	reader := &Reader{
+		reader: content,
+	}
+	err = reader.readMetadata()
+	if err != nil {
+		content.Close()
+		return nil, err
+	}
+	return reader, nil
+}
+
 func (r *Reader) readMetadata() error {
 	version, err := rw.ReadByte(r.reader)
 	if err != nil {
@@ -55,19 +69,10 @@ func (r *Reader) readMetadata() error {
 	}
 	r.domainIndex = domainIndex
 	r.domainLength = domainLength
-	r.metadataRead = true
 	return nil
 }
 
 func (r *Reader) Read(code string) ([]Item, error) {
-	r.access.Lock()
-	defer r.access.Unlock()
-	if !r.metadataRead {
-		err := r.readMetadata()
-		if err != nil {
-			return nil, err
-		}
-	}
 	if _, exists := r.domainIndex[code]; !exists {
 		return nil, E.New("code ", code, " not exists!")
 	}

+ 41 - 0
common/geosite/rule.go

@@ -60,3 +60,44 @@ func Compile(code []Item) option.DefaultRule {
 	}
 	return codeRule
 }
+
+func Merge(rules []option.DefaultRule) option.DefaultRule {
+	var domainLength int
+	var domainSuffixLength int
+	var domainKeywordLength int
+	var domainRegexLength int
+	for _, subRule := range rules {
+		domainLength += len(subRule.Domain)
+		domainSuffixLength += len(subRule.DomainSuffix)
+		domainKeywordLength += len(subRule.DomainKeyword)
+		domainRegexLength += len(subRule.DomainRegex)
+	}
+	var rule option.DefaultRule
+	if domainLength > 0 {
+		rule.Domain = make([]string, 0, domainLength)
+	}
+	if domainSuffixLength > 0 {
+		rule.DomainSuffix = make([]string, 0, domainSuffixLength)
+	}
+	if domainKeywordLength > 0 {
+		rule.DomainKeyword = make([]string, 0, domainKeywordLength)
+	}
+	if domainRegexLength > 0 {
+		rule.DomainRegex = make([]string, 0, domainRegexLength)
+	}
+	for _, subRule := range rules {
+		if len(subRule.Domain) > 0 {
+			rule.Domain = append(rule.Domain, subRule.Domain...)
+		}
+		if len(subRule.DomainSuffix) > 0 {
+			rule.DomainSuffix = append(rule.DomainSuffix, subRule.DomainSuffix...)
+		}
+		if len(subRule.DomainKeyword) > 0 {
+			rule.DomainKeyword = append(rule.DomainKeyword, subRule.DomainKeyword...)
+		}
+		if len(subRule.DomainRegex) > 0 {
+			rule.DomainRegex = append(rule.DomainRegex, subRule.DomainRegex...)
+		}
+	}
+	return rule
+}

+ 1 - 1
go.mod

@@ -7,7 +7,7 @@ require (
 	github.com/goccy/go-json v0.9.8
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/oschwald/geoip2-golang v1.7.0
-	github.com/sagernet/sing v0.0.0-20220704113227-8b990551511a
+	github.com/sagernet/sing v0.0.0-20220705005401-57d12d875b7a
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649
 	github.com/sirupsen/logrus v1.8.1
 	github.com/spf13/cobra v1.5.0

+ 2 - 2
go.sum

@@ -20,8 +20,8 @@ github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sagernet/sing v0.0.0-20220704113227-8b990551511a h1:IvYjuvuPNmZzQfBbCxE/uQqGkNWUa5/KrEMIecRMjZk=
-github.com/sagernet/sing v0.0.0-20220704113227-8b990551511a/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
+github.com/sagernet/sing v0.0.0-20220705005401-57d12d875b7a h1:FhrHCkox9scuTzcT5DDh6flVLFuqU+QSk3VONd41I+o=
+github.com/sagernet/sing v0.0.0-20220705005401-57d12d875b7a/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=

+ 13 - 3
option/route.go

@@ -8,13 +8,15 @@ import (
 )
 
 type RouteOptions struct {
-	GeoIP         *GeoIPOptions `json:"geoip,omitempty"`
-	Rules         []Rule        `json:"rules,omitempty"`
-	DefaultDetour string        `json:"default_detour,omitempty"`
+	GeoIP         *GeoIPOptions   `json:"geoip,omitempty"`
+	Geosite       *GeositeOptions `json:"geosite,omitempty"`
+	Rules         []Rule          `json:"rules,omitempty"`
+	DefaultDetour string          `json:"default_detour,omitempty"`
 }
 
 func (o RouteOptions) Equals(other RouteOptions) bool {
 	return common.ComparablePtrEquals(o.GeoIP, other.GeoIP) &&
+		common.ComparablePtrEquals(o.Geosite, other.Geosite) &&
 		common.SliceEquals(o.Rules, other.Rules)
 }
 
@@ -24,6 +26,12 @@ type GeoIPOptions struct {
 	DownloadDetour string `json:"download_detour,omitempty"`
 }
 
+type GeositeOptions struct {
+	Path           string `json:"path,omitempty"`
+	DownloadURL    string `json:"download_url,omitempty"`
+	DownloadDetour string `json:"download_detour,omitempty"`
+}
+
 type _Rule struct {
 	Type           string      `json:"type,omitempty"`
 	DefaultOptions DefaultRule `json:"-"`
@@ -84,6 +92,7 @@ type DefaultRule struct {
 	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"`
@@ -110,6 +119,7 @@ func (r DefaultRule) Equals(other DefaultRule) bool {
 		common.ComparableSliceEquals(r.DomainSuffix, other.DomainSuffix) &&
 		common.ComparableSliceEquals(r.DomainKeyword, other.DomainKeyword) &&
 		common.ComparableSliceEquals(r.DomainRegex, other.DomainRegex) &&
+		common.ComparableSliceEquals(r.Geosite, other.Geosite) &&
 		common.ComparableSliceEquals(r.SourceGeoIP, other.SourceGeoIP) &&
 		common.ComparableSliceEquals(r.GeoIP, other.GeoIP) &&
 		common.ComparableSliceEquals(r.SourceIPCIDR, other.SourceIPCIDR) &&

+ 188 - 77
route/router.go

@@ -11,6 +11,7 @@ import (
 
 	"github.com/oschwald/geoip2-golang"
 	"github.com/sagernet/sing-box/adapter"
+	"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"
@@ -35,20 +36,25 @@ type Router struct {
 	defaultOutboundForConnection       adapter.Outbound
 	defaultOutboundForPacketConnection adapter.Outbound
 
-	needGeoDatabase bool
-	geoOptions      option.GeoIPOptions
-	geoReader       *geoip2.Reader
+	needGeoIPDatabase bool
+	geoIPOptions      option.GeoIPOptions
+	geoIPReader       *geoip2.Reader
+
+	needGeositeDatabase bool
+	geositeOptions      option.GeositeOptions
+	geositeReader       *geosite.Reader
 }
 
 func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptions) (*Router, error) {
 	router := &Router{
-		ctx:             ctx,
-		logger:          logger.WithPrefix("router: "),
-		outboundByTag:   make(map[string]adapter.Outbound),
-		rules:           make([]adapter.Rule, 0, len(options.Rules)),
-		needGeoDatabase: hasGeoRule(options.Rules),
-		geoOptions:      common.PtrValueOrDefault(options.GeoIP),
-		defaultDetour:   options.DefaultDetour,
+		ctx:                 ctx,
+		logger:              logger.WithPrefix("router: "),
+		outboundByTag:       make(map[string]adapter.Outbound),
+		rules:               make([]adapter.Rule, 0, len(options.Rules)),
+		needGeoIPDatabase:   hasGeoRule(options.Rules, isGeoIPRule),
+		needGeositeDatabase: hasGeoRule(options.Rules, isGeositeRule),
+		geoIPOptions:        common.PtrValueOrDefault(options.GeoIP),
+		defaultDetour:       options.DefaultDetour,
 	}
 	for i, ruleOptions := range options.Rules {
 		rule, err := NewRule(router, logger, ruleOptions)
@@ -60,32 +66,6 @@ func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptio
 	return router, nil
 }
 
-func hasGeoRule(rules []option.Rule) bool {
-	for _, rule := range rules {
-		switch rule.Type {
-		case C.RuleTypeDefault:
-			if isGeoRule(rule.DefaultOptions) {
-				return true
-			}
-		case C.RuleTypeLogical:
-			for _, subRule := range rule.LogicalOptions.Rules {
-				if isGeoRule(subRule) {
-					return true
-				}
-			}
-		}
-	}
-	return false
-}
-
-func isGeoRule(rule option.DefaultRule) bool {
-	return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode)
-}
-
-func notPrivateNode(code string) bool {
-	return code != "private"
-}
-
 func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func() adapter.Outbound) error {
 	outboundByTag := make(map[string]adapter.Outbound)
 	for _, detour := range outbounds {
@@ -156,29 +136,114 @@ func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func()
 }
 
 func (r *Router) Start() error {
-	if r.needGeoDatabase {
+	if r.needGeoIPDatabase {
 		err := r.prepareGeoIPDatabase()
 		if err != nil {
 			return err
 		}
 	}
+	if r.needGeositeDatabase {
+		err := r.prepareGeositeDatabase()
+		if err != nil {
+			return err
+		}
+	}
+	for _, rule := range r.rules {
+		err := rule.Start()
+		if err != nil {
+			return err
+		}
+	}
 	return nil
 }
 
 func (r *Router) Close() error {
 	return common.Close(
-		common.PtrOrNil(r.geoReader),
+		common.PtrOrNil(r.geoIPReader),
+		common.PtrOrNil(r.geositeReader),
 	)
 }
 
 func (r *Router) GeoIPReader() *geoip2.Reader {
-	return r.geoReader
+	return r.geoIPReader
+}
+
+func (r *Router) GeositeReader() *geosite.Reader {
+	return r.geositeReader
+}
+
+func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
+	outbound, loaded := r.outboundByTag[tag]
+	return outbound, loaded
+}
+
+func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	detour := r.match(ctx, metadata, r.defaultOutboundForConnection)
+	if !common.Contains(detour.Network(), C.NetworkTCP) {
+		conn.Close()
+		return E.New("missing supported outbound, closing connection")
+	}
+	return detour.NewConnection(ctx, conn, metadata.Destination)
+}
+
+func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	detour := r.match(ctx, metadata, r.defaultOutboundForPacketConnection)
+	if !common.Contains(detour.Network(), C.NetworkUDP) {
+		conn.Close()
+		return E.New("missing supported outbound, closing packet connection")
+	}
+	return detour.NewPacketConnection(ctx, conn, metadata.Destination)
+}
+
+func (r *Router) match(ctx context.Context, metadata adapter.InboundContext, defaultOutbound adapter.Outbound) adapter.Outbound {
+	for i, rule := range r.rules {
+		if rule.Match(&metadata) {
+			detour := rule.Outbound()
+			r.logger.WithContext(ctx).Info("match [", i, "]", rule.String(), " => ", detour)
+			if outbound, loaded := r.Outbound(detour); loaded {
+				return outbound
+			}
+			r.logger.WithContext(ctx).Error("outbound not found: ", detour)
+		}
+	}
+	r.logger.WithContext(ctx).Info("no match")
+	return defaultOutbound
+}
+
+func hasGeoRule(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:
+			for _, subRule := range rule.LogicalOptions.Rules {
+				if cond(subRule) {
+					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 isGeositeRule(rule option.DefaultRule) bool {
+	return len(rule.Geosite) > 0
+}
+
+func notPrivateNode(code string) bool {
+	return code != "private"
 }
 
 func (r *Router) prepareGeoIPDatabase() error {
 	var geoPath string
-	if r.geoOptions.Path != "" {
-		geoPath = r.geoOptions.Path
+	if r.geoIPOptions.Path != "" {
+		geoPath = r.geoIPOptions.Path
 	} else {
 		geoPath = "Country.mmdb"
 		if foundPath, loaded := C.Find(geoPath); loaded {
@@ -204,26 +269,62 @@ func (r *Router) prepareGeoIPDatabase() error {
 	geoReader, err := geoip2.Open(geoPath)
 	if err == nil {
 		r.logger.Info("loaded geoip database")
-		r.geoReader = geoReader
+		r.geoIPReader = geoReader
 	} else {
 		return E.Cause(err, "open geoip database")
 	}
 	return nil
 }
 
+func (r *Router) prepareGeositeDatabase() error {
+	var geoPath string
+	if r.geositeOptions.Path != "" {
+		geoPath = r.geoIPOptions.Path
+	} else {
+		geoPath = "geosite.db"
+		if foundPath, loaded := C.Find(geoPath); loaded {
+			geoPath = foundPath
+		}
+	}
+	if !rw.FileExists(geoPath) {
+		r.logger.Warn("geosite database not exists: ", geoPath)
+		var err error
+		for attempts := 0; attempts < 3; attempts++ {
+			err = r.downloadGeositeDatabase(geoPath)
+			if err == nil {
+				break
+			}
+			r.logger.Error("download geosite database: ", err)
+			os.Remove(geoPath)
+			time.Sleep(10 * time.Second)
+		}
+		if err != nil {
+			return err
+		}
+	}
+	geoReader, err := geosite.Open(geoPath)
+	if err == nil {
+		r.logger.Info("loaded geosite database")
+		r.geositeReader = geoReader
+	} else {
+		return E.Cause(err, "open geosite database")
+	}
+	return nil
+}
+
 func (r *Router) downloadGeoIPDatabase(savePath string) error {
 	var downloadURL string
-	if r.geoOptions.DownloadURL != "" {
-		downloadURL = r.geoOptions.DownloadURL
+	if r.geoIPOptions.DownloadURL != "" {
+		downloadURL = r.geoIPOptions.DownloadURL
 	} else {
 		downloadURL = "https://cdn.jsdelivr.net/gh/Dreamacro/maxmind-geoip@release/Country.mmdb"
 	}
 	r.logger.Info("downloading geoip database")
 	var detour adapter.Outbound
-	if r.geoOptions.DownloadDetour != "" {
-		outbound, loaded := r.Outbound(r.geoOptions.DownloadDetour)
+	if r.geoIPOptions.DownloadDetour != "" {
+		outbound, loaded := r.Outbound(r.geoIPOptions.DownloadDetour)
 		if !loaded {
-			return E.New("detour outbound not found: ", r.geoOptions.DownloadDetour)
+			return E.New("detour outbound not found: ", r.geoIPOptions.DownloadDetour)
 		}
 		detour = outbound
 	} else {
@@ -259,40 +360,50 @@ func (r *Router) downloadGeoIPDatabase(savePath string) error {
 	return err
 }
 
-func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
-	outbound, loaded := r.outboundByTag[tag]
-	return outbound, loaded
-}
+func (r *Router) downloadGeositeDatabase(savePath string) error {
+	var downloadURL string
+	if r.geositeOptions.DownloadURL != "" {
+		downloadURL = r.geositeOptions.DownloadURL
+	} else {
+		downloadURL = "https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db"
+	}
+	r.logger.Info("downloading geoip database")
+	var detour adapter.Outbound
+	if r.geositeOptions.DownloadDetour != "" {
+		outbound, loaded := r.Outbound(r.geositeOptions.DownloadDetour)
+		if !loaded {
+			return E.New("detour outbound not found: ", r.geoIPOptions.DownloadDetour)
+		}
+		detour = outbound
+	} else {
+		detour = r.defaultOutboundForConnection
+	}
 
-func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	detour := r.match(ctx, metadata, r.defaultOutboundForConnection)
-	if !common.Contains(detour.Network(), C.NetworkTCP) {
-		conn.Close()
-		return E.New("missing supported outbound, closing connection")
+	if parentDir := filepath.Dir(savePath); parentDir != "" {
+		os.MkdirAll(parentDir, 0o755)
 	}
-	return detour.NewConnection(ctx, conn, metadata.Destination)
-}
 
-func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	detour := r.match(ctx, metadata, r.defaultOutboundForPacketConnection)
-	if !common.Contains(detour.Network(), C.NetworkUDP) {
-		conn.Close()
-		return E.New("missing supported outbound, closing packet connection")
+	saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
+	if err != nil {
+		return E.Cause(err, "open output file: ", downloadURL)
 	}
-	return detour.NewPacketConnection(ctx, conn, metadata.Destination)
-}
+	defer saveFile.Close()
 
-func (r *Router) match(ctx context.Context, metadata adapter.InboundContext, defaultOutbound adapter.Outbound) adapter.Outbound {
-	for i, rule := range r.rules {
-		if rule.Match(&metadata) {
-			detour := rule.Outbound()
-			r.logger.WithContext(ctx).Info("match [", i, "]", rule.String(), " => ", detour)
-			if outbound, loaded := r.Outbound(detour); loaded {
-				return outbound
-			}
-			r.logger.WithContext(ctx).Error("outbound not found: ", detour)
-		}
+	httpClient := &http.Client{
+		Timeout: 5 * time.Second,
+		Transport: &http.Transport{
+			ForceAttemptHTTP2:   true,
+			TLSHandshakeTimeout: 5 * time.Second,
+			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+				return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
+			},
+		},
 	}
-	r.logger.WithContext(ctx).Info("no match")
-	return defaultOutbound
+	response, err := httpClient.Get(downloadURL)
+	if err != nil {
+		return err
+	}
+	defer response.Body.Close()
+	_, err = io.Copy(saveFile, response.Body)
+	return err
 }

+ 47 - 0
route/rule.go

@@ -91,6 +91,9 @@ func NewDefaultRule(router adapter.Router, logger log.Logger, options option.Def
 		}
 		rule.destinationAddressItems = append(rule.destinationAddressItems, item)
 	}
+	if len(options.Geosite) > 0 {
+		rule.destinationAddressItems = append(rule.destinationAddressItems, NewGeositeItem(router, logger, options.Geosite))
+	}
 	if len(options.SourceGeoIP) > 0 {
 		rule.sourceAddressItems = append(rule.sourceAddressItems, NewGeoIPItem(router, logger, true, options.SourceGeoIP))
 	}
@@ -120,6 +123,50 @@ func NewDefaultRule(router adapter.Router, logger log.Logger, options option.Def
 	return rule, nil
 }
 
+func (r *DefaultRule) Start() error {
+	for _, item := range r.items {
+		err := common.Start(item)
+		if err != nil {
+			return err
+		}
+	}
+	for _, item := range r.sourceAddressItems {
+		err := common.Start(item)
+		if err != nil {
+			return err
+		}
+	}
+	for _, item := range r.destinationAddressItems {
+		err := common.Start(item)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r *DefaultRule) Close() error {
+	for _, item := range r.items {
+		err := common.Close(item)
+		if err != nil {
+			return err
+		}
+	}
+	for _, item := range r.sourceAddressItems {
+		err := common.Close(item)
+		if err != nil {
+			return err
+		}
+	}
+	for _, item := range r.destinationAddressItems {
+		err := common.Close(item)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (r *DefaultRule) Match(metadata *adapter.InboundContext) bool {
 	for _, item := range r.items {
 		if !item.Match(metadata) {

+ 67 - 0
route/rule_geosite.go

@@ -0,0 +1,67 @@
+package route
+
+import (
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/geosite"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+var _ RuleItem = (*GeositeItem)(nil)
+
+type GeositeItem struct {
+	router  adapter.Router
+	logger  log.Logger
+	codes   []string
+	matcher *DefaultRule
+}
+
+func NewGeositeItem(router adapter.Router, logger log.Logger, codes []string) *GeositeItem {
+	return &GeositeItem{
+		router: router,
+		logger: logger,
+		codes:  codes,
+	}
+}
+
+func (r *GeositeItem) Start() error {
+	geositeReader := r.router.GeositeReader()
+	if geositeReader == nil {
+		return E.New("geosite reader is not initialized")
+	}
+	var subRules []option.DefaultRule
+	for _, code := range r.codes {
+		items, err := geositeReader.Read(code)
+		if err != nil {
+			return E.Cause(err, "read geosite")
+		}
+		subRules = append(subRules, geosite.Compile(items))
+	}
+	matcherRule := geosite.Merge(subRules)
+	matcher, err := NewDefaultRule(r.router, r.logger, matcherRule)
+	if err != nil {
+		return E.Cause(err, "compile geosite")
+	}
+	r.matcher = matcher
+	return nil
+}
+
+func (r *GeositeItem) Match(metadata *adapter.InboundContext) bool {
+	return r.matcher.Match(metadata)
+}
+
+func (r *GeositeItem) String() string {
+	description := "geosite="
+	cLen := len(r.codes)
+	if cLen == 1 {
+		description = r.codes[0]
+	} else if cLen > 3 {
+		description = "[" + strings.Join(r.codes[:3], " ") + "...]"
+	} else {
+		description = "[" + strings.Join(r.codes, " ") + "]"
+	}
+	return description
+}

+ 20 - 0
route/rule_logical.go

@@ -20,6 +20,26 @@ type LogicalRule struct {
 	outbound string
 }
 
+func (r *LogicalRule) Start() error {
+	for _, rule := range r.rules {
+		err := rule.Start()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r *LogicalRule) Close() error {
+	for _, rule := range r.rules {
+		err := rule.Close()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func NewLogicalRule(router adapter.Router, logger log.Logger, options option.LogicalRule) (*LogicalRule, error) {
 	r := &LogicalRule{
 		rules:    make([]*DefaultRule, len(options.Rules)),