From 22cb6bee2f9ecbfd5842fa42e4680a1d605a8690 Mon Sep 17 00:00:00 2001 From: SuperKali Date: Wed, 19 Feb 2025 22:29:09 +0000 Subject: [PATCH] Improve mirror selection logic in Closest with full-server fallback and enhanced logging --- config.go | 42 +------------ servers.go | 174 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 105 insertions(+), 111 deletions(-) diff --git a/config.go b/config.go index ed19f76..aac7476 100644 --- a/config.go +++ b/config.go @@ -94,7 +94,6 @@ func (r *Redirector) ReloadConfig() error { // Load maxmind database if r.db != nil { err = r.db.Close() - if err != nil { return errors.Wrap(err, "Unable to close database") } @@ -102,7 +101,6 @@ func (r *Redirector) ReloadConfig() error { if r.asnDB != nil { err = r.asnDB.Close() - if err != nil { return errors.Wrap(err, "Unable to close asn database") } @@ -110,14 +108,12 @@ func (r *Redirector) ReloadConfig() error { // db can be hot-reloaded if the file changed r.db, err = maxminddb.Open(r.config.GeoDBPath) - if err != nil { return errors.Wrap(err, "Unable to open database") } if r.config.ASNDBPath != "" { r.asnDB, err = maxminddb.Open(r.config.ASNDBPath) - if err != nil { return errors.Wrap(err, "Unable to open asn database") } @@ -145,21 +141,16 @@ func (r *Redirector) ReloadConfig() error { // Create mirror map mirrors := make(map[string][]*Server) - for _, server := range r.servers { mirrors[server.Continent] = append(mirrors[server.Continent], server) } - mirrors["default"] = append(mirrors["NA"], mirrors["EU"]...) - r.regionMap = mirrors hosts := make(map[string]*Server) - for _, server := range r.servers { hosts[server.Host] = server } - r.hostMap = hosts // Check top choices size @@ -180,26 +171,20 @@ func (r *Redirector) reloadServers() error { var wg sync.WaitGroup existing := make(map[string]int) - for i, server := range r.servers { existing[server.Host] = i } hosts := make(map[string]bool) - var hostsLock sync.Mutex for _, server := range r.config.ServerList { wg.Add(1) - var prefix string - if !strings.HasPrefix(server.Server, "http") { prefix = "https://" } - u, err := url.Parse(prefix + server.Server) - if err != nil { log.WithFields(log.Fields{ "error": err, @@ -209,42 +194,35 @@ func (r *Redirector) reloadServers() error { } i := -1 - if v, exists := existing[u.Host]; exists { i = v } go func(i int, server ServerConfig, u *url.URL) { defer wg.Done() - s, err := r.addServer(server, u) - if err != nil { log.WithError(err).Warning("Unable to add server") return } - hostsLock.Lock() hosts[u.Host] = true hostsLock.Unlock() - if _, ok := existing[u.Host]; ok { s.Redirects = r.servers[i].Redirects - r.servers[i] = s } else { s.Redirects = promauto.NewCounter(prometheus.CounterOpts{ Name: "armbian_router_redirects_" + metricReplacer.Replace(u.Host), Help: "The number of redirects for server " + u.Host, }) - r.servers = append(r.servers, s) - log.WithFields(log.Fields{ "server": u.Host, "path": u.Path, "latitude": s.Latitude, "longitude": s.Longitude, + "country": s.Country, }).Info("Added server") } }(i, server, u) @@ -257,11 +235,9 @@ func (r *Redirector) reloadServers() error { if _, exists := hosts[r.servers[i].Host]; exists { continue } - log.WithFields(log.Fields{ "server": r.servers[i].Host, }).Info("Removed server") - r.servers = append(r.servers[:i], r.servers[i+1:]...) } @@ -284,7 +260,6 @@ func (r *Redirector) addServer(server ServerConfig, u *url.URL) (*Server, error) Protocols: []string{"http", "https"}, Rules: server.Rules, } - if len(server.Protocols) > 0 { for _, proto := range server.Protocols { if !lo.Contains(s.Protocols, proto) { @@ -292,14 +267,11 @@ func (r *Redirector) addServer(server ServerConfig, u *url.URL) (*Server, error) } } } - // Defaults to 10 to allow servers to be set lower for lower priority if s.Weight == 0 { s.Weight = 10 } - ips, err := net.LookupIP(u.Host) - if err != nil { log.WithFields(log.Fields{ "error": err, @@ -307,10 +279,8 @@ func (r *Redirector) addServer(server ServerConfig, u *url.URL) (*Server, error) }).Warning("Could not resolve address") return nil, err } - var city db.City err = r.db.Lookup(ips[0], &city) - if err != nil { log.WithFields(log.Fields{ "error": err, @@ -319,35 +289,27 @@ func (r *Redirector) addServer(server ServerConfig, u *url.URL) (*Server, error) }).Warning("Could not geolocate address") return nil, err } - + s.Country = city.Country.IsoCode if s.Continent == "" { s.Continent = city.Continent.Code } - if s.Latitude == 0 && s.Longitude == 0 { s.Latitude = city.Location.Latitude s.Longitude = city.Location.Longitude } - return s, nil } func (r *Redirector) reloadMap() error { mapFile := r.config.MapFile - if mapFile == "" { return nil } - log.WithField("file", mapFile).Info("Loading download map") - newMap, err := loadMapFile(mapFile) - if err != nil { return err } - r.dlMap = newMap - return nil } diff --git a/servers.go b/servers.go index f31d3ea..0204bea 100644 --- a/servers.go +++ b/servers.go @@ -27,6 +27,7 @@ type Server struct { Longitude float64 `json:"longitude"` Weight int `json:"weight"` Continent string `json:"continent"` + Country string `json:"country"` Protocols []string `json:"protocols"` Rules []Rule `json:"rules,omitempty"` Redirects prometheus.Counter `json:"-"` @@ -189,105 +190,136 @@ type ComputedDistance struct { Distance float64 } -// Closest will use GeoIP on the IP provided and find the closest servers. -// When we have a list of x servers closest, we can choose a random or weighted one. -// Return values are the closest server, the distance, and if an error occurred. +// Closest uses GeoIP on the client's IP and compares the client's location +// with that of the servers. If there are servers with the same country code, +// it computes the distances. If the nearest server is within a threshold (e.g. 50km), +// it is selected deterministically; otherwise, a weighted selection is used. +// If no local servers exist, it falls back to a weighted selection among all valid servers. func (s ServerList) Closest(r *Redirector, scheme string, ip net.IP) (*Server, float64, error) { - choiceInterface, exists := r.serverCache.Get(scheme + "_" + ip.String()) + cacheKey := scheme + "_" + ip.String() - if !exists { - var city db.City - err := r.db.Lookup(ip, &city) + if cached, exists := r.serverCache.Get(cacheKey); exists { + if comp, ok := cached.(ComputedDistance); ok { + log.Infof("Cache hit: %s", comp.Server.Host) + return comp.Server, comp.Distance, nil + } + r.serverCache.Remove(cacheKey) + } - if err != nil { - log.WithError(err).Warning("Unable to lookup location information") + var city db.City + if err := r.db.Lookup(ip, &city); err != nil { + log.WithError(err).Warning("Unable to lookup client location") + return nil, -1, err + } + clientCountry := city.Country.IsoCode + var asn db.ASN + if r.asnDB != nil { + if err := r.asnDB.Lookup(ip, &asn); err != nil { + log.WithError(err).Warning("Unable to load ASN information") return nil, -1, err } + } - var asn db.ASN + ruleInput := RuleInput{ + IP: ip.String(), + ASN: asn, + Location: city, + } - if r.asnDB != nil { - err = r.asnDB.Lookup(ip, &asn) - - if err != nil { - log.WithError(err).Warning("Unable to load ASN information") - return nil, -1, err - } + validServers := lo.Filter(s, func(server *Server, _ int) bool { + if !server.Available || !lo.Contains(server.Protocols, scheme) { + return false } - - ruleInput := RuleInput{ - IP: ip.String(), - ASN: asn, - Location: city, + if len(server.Rules) > 0 && !server.checkRules(ruleInput) { + log.WithField("host", server.Host).Debug("Skipping server due to rules") + return false } + return true + }) - // First, filter our servers to what are actually available/match. - validServers := lo.Filter(s, func(server *Server, _ int) bool { - if !server.Available || !lo.Contains(server.Protocols, scheme) { - return false - } - - if !server.checkRules(ruleInput) { - log.WithField("host", server.Host).Debug("Skipping server due to rules") - return false - } - - return true - }) - - // Then, map them to distances from the client - c := lo.Map(validServers, func(server *Server, _ int) ComputedDistance { - distance := Distance(city.Location.Latitude, city.Location.Longitude, server.Latitude, server.Longitude) - + if len(validServers) < 2 { + log.Warn("Few servers passed filtering; falling back to full server list") + validServers = s + } + + for _, server := range validServers { + log.Infof("Valid server: %s, Country: %s", server.Host, server.Country) + } + + localServers := lo.Filter(validServers, func(server *Server, _ int) bool { + return server.Country == clientCountry + }) + log.Infof("Found %d local servers for country %s", len(localServers), clientCountry) + + const sameCityThreshold = 50000.0 // 50 km + + if len(localServers) > 0 { + computedLocal := lo.Map(localServers, func(server *Server, _ int) ComputedDistance { + d := Distance(city.Location.Latitude, city.Location.Longitude, server.Latitude, server.Longitude) return ComputedDistance{ Server: server, - Distance: distance, + Distance: d, } }) - - // Sort by distance - sort.Slice(c, func(i int, j int) bool { - return c[i].Distance < c[j].Distance + sort.Slice(computedLocal, func(i, j int) bool { + return computedLocal[i].Distance < computedLocal[j].Distance }) - - choiceCount := r.config.TopChoices - - if len(c) < r.config.TopChoices { - choiceCount = len(c) + log.Infof("Nearest local server: %s, distance: %.2f m", computedLocal[0].Server.Host, computedLocal[0].Distance) + if computedLocal[0].Distance < sameCityThreshold { + chosen := computedLocal[0] + r.serverCache.Add(cacheKey, chosen) + return chosen.Server, chosen.Distance, nil + } + choiceCount := r.config.TopChoices + if len(computedLocal) < choiceCount { + choiceCount = len(computedLocal) } - - log.WithFields(log.Fields{"count": len(c)}).Debug("Picking from top choices") - choices := make([]randutil.Choice, choiceCount) - - for i, item := range c[0:choiceCount] { + for i, item := range computedLocal[0:choiceCount] { choices[i] = randutil.Choice{ Weight: item.Server.Weight, Item: item, } } - - choiceInterface = choices - - r.serverCache.Add(scheme+"_"+ip.String(), choiceInterface) + r.serverCache.Add(cacheKey, choices) + choice, err := randutil.WeightedChoice(choices) + if err != nil { + log.WithError(err).Warning("Unable to choose a weighted choice") + return nil, -1, err + } + dist := choice.Item.(ComputedDistance) + return dist.Server, dist.Distance, nil } - - choice, err := randutil.WeightedChoice(choiceInterface.([]randutil.Choice)) - + + // Fallback + computed := lo.Map(validServers, func(server *Server, _ int) ComputedDistance { + d := Distance(city.Location.Latitude, city.Location.Longitude, server.Latitude, server.Longitude) + return ComputedDistance{ + Server: server, + Distance: d, + } + }) + sort.Slice(computed, func(i, j int) bool { + return computed[i].Distance < computed[j].Distance + }) + choiceCount := r.config.TopChoices + if len(computed) < choiceCount { + choiceCount = len(computed) + } + choices := make([]randutil.Choice, choiceCount) + for i, item := range computed[0:choiceCount] { + choices[i] = randutil.Choice{ + Weight: item.Server.Weight, + Item: item, + } + } + r.serverCache.Add(cacheKey, choices) + choice, err := randutil.WeightedChoice(choices) if err != nil { log.WithError(err).Warning("Unable to choose a weighted choice") return nil, -1, err } - dist := choice.Item.(ComputedDistance) - - if !dist.Server.Available { - // Choose a new server and refresh cache - r.serverCache.Remove(scheme + "_" + ip.String()) - - return s.Closest(r, scheme, ip) - } - return dist.Server, dist.Distance, nil }