mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-22 08:31:30 +00:00
Add geosite
This commit is contained in:
parent
f76102dab5
commit
8392567962
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
||||||
/vendor/
|
/vendor/
|
||||||
/*.json
|
/*.json
|
||||||
/Country.mmdb
|
/Country.mmdb
|
||||||
|
/geosite.db
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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++
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
2
go.mod
|
@ -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
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 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=
|
||||||
|
|
|
@ -9,12 +9,14 @@ import (
|
||||||
|
|
||||||
type RouteOptions struct {
|
type RouteOptions struct {
|
||||||
GeoIP *GeoIPOptions `json:"geoip,omitempty"`
|
GeoIP *GeoIPOptions `json:"geoip,omitempty"`
|
||||||
|
Geosite *GeositeOptions `json:"geosite,omitempty"`
|
||||||
Rules []Rule `json:"rules,omitempty"`
|
Rules []Rule `json:"rules,omitempty"`
|
||||||
DefaultDetour string `json:"default_detour,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) &&
|
||||||
|
|
343
route/router.go
343
route/router.go
|
@ -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,9 +36,13 @@ 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) {
|
||||||
|
@ -46,8 +51,9 @@ func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptio
|
||||||
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),
|
||||||
|
geoIPOptions: common.PtrValueOrDefault(options.GeoIP),
|
||||||
defaultDetour: options.DefaultDetour,
|
defaultDetour: options.DefaultDetour,
|
||||||
}
|
}
|
||||||
for i, ruleOptions := range options.Rules {
|
for i, ruleOptions := range options.Rules {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
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
|
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)),
|
||||||
|
|
Loading…
Reference in a new issue