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