mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-10 02:53:12 +00:00
Add geosite
This commit is contained in:
parent
f76102dab5
commit
8392567962
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
/vendor/
|
||||
/*.json
|
||||
/Country.mmdb
|
||||
/geosite.db
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ func newSuccinctSet(keys []string) *succinctSet {
|
|||
setBit(&ss.labelBitmap, lIdx, 0)
|
||||
lIdx++
|
||||
}
|
||||
|
||||
setBit(&ss.labelBitmap, lIdx, 1)
|
||||
lIdx++
|
||||
}
|
||||
|
|
|
@ -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!")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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) &&
|
||||
|
|
353
route/router.go
353
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,107 +136,40 @@ 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) prepareGeoIPDatabase() error {
|
||||
var geoPath string
|
||||
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) GeositeReader() *geosite.Reader {
|
||||
return r.geositeReader
|
||||
}
|
||||
|
||||
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")
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
route/rule_geosite.go
Normal file
67
route/rule_geosite.go
Normal 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
|
||||
}
|
|
@ -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)),
|
||||
|
|
Loading…
Reference in a new issue