package route import ( "context" "io" "net" "net/http" "os" "path/filepath" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geosite" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/service/filemanager" ) func (r *Router) GeoIPReader() *geoip.Reader { return r.geoIPReader } func (r *Router) LoadGeosite(code string) (adapter.Rule, error) { rule, cached := r.geositeCache[code] if cached { return rule, nil } items, err := r.geositeReader.Read(code) if err != nil { return nil, err } rule, err = NewDefaultRule(r, nil, geosite.Compile(items)) if err != nil { return nil, err } r.geositeCache[code] = rule return rule, nil } func (r *Router) prepareGeoIPDatabase() error { var geoPath string if r.geoIPOptions.Path != "" { geoPath = r.geoIPOptions.Path } else { geoPath = "geoip.db" if foundPath, loaded := C.FindPath(geoPath); loaded { geoPath = foundPath } } if !rw.FileExists(geoPath) { geoPath = filemanager.BasePath(r.ctx, geoPath) } if stat, err := os.Stat(geoPath); err == nil { if stat.IsDir() { return E.New("geoip path is a directory: ", geoPath) } if stat.Size() == 0 { os.Remove(geoPath) } } 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, codes, err := geoip.Open(geoPath) if err != nil { return E.Cause(err, "open geoip database") } r.logger.Info("loaded geoip database: ", len(codes), " codes") r.geoIPReader = geoReader return nil } func (r *Router) prepareGeositeDatabase() error { var geoPath string if r.geositeOptions.Path != "" { geoPath = r.geositeOptions.Path } else { geoPath = "geosite.db" if foundPath, loaded := C.FindPath(geoPath); loaded { geoPath = foundPath } } if !rw.FileExists(geoPath) { geoPath = filemanager.BasePath(r.ctx, geoPath) } if stat, err := os.Stat(geoPath); err == nil { if stat.IsDir() { return E.New("geoip path is a directory: ", geoPath) } if stat.Size() == 0 { os.Remove(geoPath) } } 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) } if err != nil { return err } } geoReader, codes, err := geosite.Open(geoPath) if err == nil { r.logger.Info("loaded geosite database: ", len(codes), " codes") 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://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db" } 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 != "" { filemanager.MkdirAll(r.ctx, parentDir, 0o755) } httpClient := &http.Client{ 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)) }, }, } defer httpClient.CloseIdleConnections() request, err := http.NewRequest("GET", downloadURL, nil) if err != nil { return err } response, err := httpClient.Do(request.WithContext(r.ctx)) if err != nil { return err } defer response.Body.Close() saveFile, err := filemanager.Create(r.ctx, savePath) if err != nil { return E.Cause(err, "open output file: ", downloadURL) } _, err = io.Copy(saveFile, response.Body) saveFile.Close() if err != nil { filemanager.Remove(r.ctx, savePath) } 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 geosite 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.geositeOptions.DownloadDetour) } detour = outbound } else { detour = r.defaultOutboundForConnection } if parentDir := filepath.Dir(savePath); parentDir != "" { filemanager.MkdirAll(r.ctx, parentDir, 0o755) } httpClient := &http.Client{ 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)) }, }, } defer httpClient.CloseIdleConnections() request, err := http.NewRequest("GET", downloadURL, nil) if err != nil { return err } response, err := httpClient.Do(request.WithContext(r.ctx)) if err != nil { return err } defer response.Body.Close() saveFile, err := filemanager.Create(r.ctx, savePath) if err != nil { return E.Cause(err, "open output file: ", downloadURL) } _, err = io.Copy(saveFile, response.Body) saveFile.Close() if err != nil { filemanager.Remove(r.ctx, savePath) } return err } func hasRule(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: if hasRule(rule.LogicalOptions.Rules, cond) { return true } } } return false } func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool { for _, rule := range rules { switch rule.Type { case C.RuleTypeDefault: if cond(rule.DefaultOptions) { return true } case C.RuleTypeLogical: if hasDNSRule(rule.LogicalOptions.Rules, cond) { 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 isGeoIPDNSRule(rule option.DefaultDNSRule) bool { return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) } func isGeositeRule(rule option.DefaultRule) bool { return len(rule.Geosite) > 0 } func isGeositeDNSRule(rule option.DefaultDNSRule) bool { return len(rule.Geosite) > 0 } func isProcessRule(rule option.DefaultRule) bool { return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 } func isProcessDNSRule(rule option.DefaultDNSRule) bool { return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 } func notPrivateNode(code string) bool { return code != "private" } func isWIFIRule(rule option.DefaultRule) bool { return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 } func isWIFIDNSRule(rule option.DefaultDNSRule) bool { return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 }