You've already forked armbian-router
mirror of
https://github.com/armbian/armbian-router.git
synced 2026-01-06 10:37:03 -08:00
Improve mirror selection logic in Closest with full-server fallback and enhanced logging
This commit is contained in:
42
config.go
42
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
|
||||
}
|
||||
|
||||
174
servers.go
174
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user