Add geosite

This commit is contained in:
世界 2022-07-05 09:05:35 +08:00
parent f76102dab5
commit 8392567962
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
13 changed files with 464 additions and 146 deletions

3
.gitignore vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -60,3 +60,44 @@ func Compile(code []Item) option.DefaultRule {
} }
return codeRule 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
}

2
go.mod
View file

@ -7,7 +7,7 @@ require (
github.com/goccy/go-json v0.9.8 github.com/goccy/go-json v0.9.8
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/oschwald/geoip2-golang v1.7.0 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/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.5.0

4
go.sum
View file

@ -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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/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-20220705005401-57d12d875b7a h1:FhrHCkox9scuTzcT5DDh6flVLFuqU+QSk3VONd41I+o=
github.com/sagernet/sing v0.0.0-20220704113227-8b990551511a/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c= 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 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k=
github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw= 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= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=

View file

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

View file

@ -11,6 +11,7 @@ import (
"github.com/oschwald/geoip2-golang" "github.com/oschwald/geoip2-golang"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/geosite"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
@ -35,20 +36,25 @@ type Router struct {
defaultOutboundForConnection adapter.Outbound defaultOutboundForConnection adapter.Outbound
defaultOutboundForPacketConnection adapter.Outbound defaultOutboundForPacketConnection adapter.Outbound
needGeoDatabase bool needGeoIPDatabase bool
geoOptions option.GeoIPOptions geoIPOptions option.GeoIPOptions
geoReader *geoip2.Reader geoIPReader *geoip2.Reader
needGeositeDatabase bool
geositeOptions option.GeositeOptions
geositeReader *geosite.Reader
} }
func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptions) (*Router, error) { func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptions) (*Router, error) {
router := &Router{ router := &Router{
ctx: ctx, ctx: ctx,
logger: logger.WithPrefix("router: "), logger: logger.WithPrefix("router: "),
outboundByTag: make(map[string]adapter.Outbound), outboundByTag: make(map[string]adapter.Outbound),
rules: make([]adapter.Rule, 0, len(options.Rules)), rules: make([]adapter.Rule, 0, len(options.Rules)),
needGeoDatabase: hasGeoRule(options.Rules), needGeoIPDatabase: hasGeoRule(options.Rules, isGeoIPRule),
geoOptions: common.PtrValueOrDefault(options.GeoIP), needGeositeDatabase: hasGeoRule(options.Rules, isGeositeRule),
defaultDetour: options.DefaultDetour, geoIPOptions: common.PtrValueOrDefault(options.GeoIP),
defaultDetour: options.DefaultDetour,
} }
for i, ruleOptions := range options.Rules { for i, ruleOptions := range options.Rules {
rule, err := NewRule(router, logger, ruleOptions) rule, err := NewRule(router, logger, ruleOptions)
@ -60,32 +66,6 @@ func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptio
return router, nil 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 { func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func() adapter.Outbound) error {
outboundByTag := make(map[string]adapter.Outbound) outboundByTag := make(map[string]adapter.Outbound)
for _, detour := range outbounds { for _, detour := range outbounds {
@ -156,107 +136,40 @@ func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func()
} }
func (r *Router) Start() error { func (r *Router) Start() error {
if r.needGeoDatabase { if r.needGeoIPDatabase {
err := r.prepareGeoIPDatabase() err := r.prepareGeoIPDatabase()
if err != nil { if err != nil {
return err 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 return nil
} }
func (r *Router) Close() error { func (r *Router) Close() error {
return common.Close( return common.Close(
common.PtrOrNil(r.geoReader), common.PtrOrNil(r.geoIPReader),
common.PtrOrNil(r.geositeReader),
) )
} }
func (r *Router) GeoIPReader() *geoip2.Reader { func (r *Router) GeoIPReader() *geoip2.Reader {
return r.geoReader return r.geoIPReader
} }
func (r *Router) prepareGeoIPDatabase() error { func (r *Router) GeositeReader() *geosite.Reader {
var geoPath string return r.geositeReader
if r.geoOptions.Path != "" {
geoPath = r.geoOptions.Path
} else {
geoPath = "Country.mmdb"
if foundPath, loaded := C.Find(geoPath); loaded {
geoPath = foundPath
}
}
if !rw.FileExists(geoPath) {
r.logger.Warn("geoip database not exists: ", geoPath)
var err error
for attempts := 0; attempts < 3; attempts++ {
err = r.downloadGeoIPDatabase(geoPath)
if err == nil {
break
}
r.logger.Error("download geoip database: ", err)
os.Remove(geoPath)
time.Sleep(10 * time.Second)
}
if err != nil {
return err
}
}
geoReader, err := geoip2.Open(geoPath)
if err == nil {
r.logger.Info("loaded geoip database")
r.geoReader = geoReader
} else {
return E.Cause(err, "open geoip database")
}
return nil
}
func (r *Router) downloadGeoIPDatabase(savePath string) error {
var downloadURL string
if r.geoOptions.DownloadURL != "" {
downloadURL = r.geoOptions.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 !loaded {
return E.New("detour outbound not found: ", r.geoOptions.DownloadDetour)
}
detour = outbound
} else {
detour = r.defaultOutboundForConnection
}
if parentDir := filepath.Dir(savePath); parentDir != "" {
os.MkdirAll(parentDir, 0o755)
}
saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return E.Cause(err, "open output file: ", downloadURL)
}
defer saveFile.Close()
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))
},
},
}
response, err := httpClient.Get(downloadURL)
if err != nil {
return err
}
defer response.Body.Close()
_, err = io.Copy(saveFile, response.Body)
return err
} }
func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
@ -296,3 +209,201 @@ func (r *Router) match(ctx context.Context, metadata adapter.InboundContext, def
r.logger.WithContext(ctx).Info("no match") r.logger.WithContext(ctx).Info("no match")
return defaultOutbound 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.geoIPOptions.Path != "" {
geoPath = r.geoIPOptions.Path
} else {
geoPath = "Country.mmdb"
if foundPath, loaded := C.Find(geoPath); loaded {
geoPath = foundPath
}
}
if !rw.FileExists(geoPath) {
r.logger.Warn("geoip database not exists: ", geoPath)
var err error
for attempts := 0; attempts < 3; attempts++ {
err = r.downloadGeoIPDatabase(geoPath)
if err == nil {
break
}
r.logger.Error("download geoip database: ", err)
os.Remove(geoPath)
time.Sleep(10 * time.Second)
}
if err != nil {
return err
}
}
geoReader, err := geoip2.Open(geoPath)
if err == nil {
r.logger.Info("loaded geoip database")
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.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.geoIPOptions.DownloadDetour != "" {
outbound, loaded := r.Outbound(r.geoIPOptions.DownloadDetour)
if !loaded {
return E.New("detour outbound not found: ", r.geoIPOptions.DownloadDetour)
}
detour = outbound
} else {
detour = r.defaultOutboundForConnection
}
if parentDir := filepath.Dir(savePath); parentDir != "" {
os.MkdirAll(parentDir, 0o755)
}
saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return E.Cause(err, "open output file: ", downloadURL)
}
defer saveFile.Close()
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))
},
},
}
response, err := httpClient.Get(downloadURL)
if err != nil {
return err
}
defer response.Body.Close()
_, err = io.Copy(saveFile, response.Body)
return err
}
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
}
if parentDir := filepath.Dir(savePath); parentDir != "" {
os.MkdirAll(parentDir, 0o755)
}
saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return E.Cause(err, "open output file: ", downloadURL)
}
defer saveFile.Close()
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))
},
},
}
response, err := httpClient.Get(downloadURL)
if err != nil {
return err
}
defer response.Body.Close()
_, err = io.Copy(saveFile, response.Body)
return err
}

View file

@ -91,6 +91,9 @@ func NewDefaultRule(router adapter.Router, logger log.Logger, options option.Def
} }
rule.destinationAddressItems = append(rule.destinationAddressItems, item) 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 { if len(options.SourceGeoIP) > 0 {
rule.sourceAddressItems = append(rule.sourceAddressItems, NewGeoIPItem(router, logger, true, options.SourceGeoIP)) 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 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 { func (r *DefaultRule) Match(metadata *adapter.InboundContext) bool {
for _, item := range r.items { for _, item := range r.items {
if !item.Match(metadata) { if !item.Match(metadata) {

67
route/rule_geosite.go Normal file
View file

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

View file

@ -20,6 +20,26 @@ type LogicalRule struct {
outbound string 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) { func NewLogicalRule(router adapter.Router, logger log.Logger, options option.LogicalRule) (*LogicalRule, error) {
r := &LogicalRule{ r := &LogicalRule{
rules: make([]*DefaultRule, len(options.Rules)), rules: make([]*DefaultRule, len(options.Rules)),