|
|
@@ -148,6 +148,7 @@ type ServerService struct {
|
|
|
cachedIPv4 string
|
|
|
cachedIPv6 string
|
|
|
noIPv6 bool
|
|
|
+ resolvingIPs bool
|
|
|
mu sync.Mutex
|
|
|
lastCPUTimes cpu.TimesStat
|
|
|
hasLastCPUSample bool
|
|
|
@@ -158,6 +159,7 @@ type ServerService struct {
|
|
|
|
|
|
lastStatusMu sync.RWMutex
|
|
|
lastStatus *Status
|
|
|
+ coldStatusMu sync.Mutex
|
|
|
|
|
|
versionsCacheMu sync.Mutex
|
|
|
versionsCache *cachedXrayVersions
|
|
|
@@ -208,6 +210,21 @@ func (s *ServerService) LastStatus() *Status {
|
|
|
return s.lastStatus
|
|
|
}
|
|
|
|
|
|
+// CurrentStatus never reports "no status yet": the @2s ticker leaves LastStatus
|
|
|
+// nil for the first seconds after a restart, and a master probing a node then
|
|
|
+// reads the empty snapshot as an offline panel.
|
|
|
+func (s *ServerService) CurrentStatus() *Status {
|
|
|
+ if status := s.LastStatus(); status != nil {
|
|
|
+ return status
|
|
|
+ }
|
|
|
+ s.coldStatusMu.Lock()
|
|
|
+ defer s.coldStatusMu.Unlock()
|
|
|
+ if status := s.LastStatus(); status != nil {
|
|
|
+ return status
|
|
|
+ }
|
|
|
+ return s.RefreshStatus()
|
|
|
+}
|
|
|
+
|
|
|
// Fail2banStatus tells the frontend whether the per-client IP limit can
|
|
|
// actually be enforced. Enforcement depends on fail2ban, so a limit set
|
|
|
// without it would silently do nothing.
|
|
|
@@ -429,36 +446,79 @@ var publicIPv6Services = []string{
|
|
|
"https://6.ident.me",
|
|
|
}
|
|
|
|
|
|
-// resolvePublicIPs caches the public IPv4/IPv6 addresses on first use. Guarded
|
|
|
-// by s.mu because the bot's ServerService may call it from sendBackup while a
|
|
|
-// status report runs concurrently.
|
|
|
+// resolvePublicIPs caches the public IPv4/IPv6 addresses on first use. The
|
|
|
+// lookups run outside s.mu so a stalling service cannot block a status sample.
|
|
|
func (s *ServerService) resolvePublicIPs() {
|
|
|
s.mu.Lock()
|
|
|
- defer s.mu.Unlock()
|
|
|
-
|
|
|
- if s.cachedIPv4 == "" {
|
|
|
- for _, ip4Service := range publicIPv4Services {
|
|
|
- s.cachedIPv4 = getPublicIP(ip4Service)
|
|
|
- if s.cachedIPv4 != "N/A" {
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
+ wantIPv4 := s.cachedIPv4 == ""
|
|
|
+ wantIPv6 := s.cachedIPv6 == "" && !s.noIPv6
|
|
|
+ s.mu.Unlock()
|
|
|
+ if !wantIPv4 && !wantIPv6 {
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
- if s.cachedIPv6 == "" && !s.noIPv6 {
|
|
|
- for _, ip6Service := range publicIPv6Services {
|
|
|
- s.cachedIPv6 = getPublicIP(ip6Service)
|
|
|
- if s.cachedIPv6 != "N/A" {
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
+ var ipv4, ipv6 string
|
|
|
+ if wantIPv4 {
|
|
|
+ ipv4 = firstPublicIP(publicIPv4Services)
|
|
|
+ }
|
|
|
+ if wantIPv6 {
|
|
|
+ ipv6 = firstPublicIP(publicIPv6Services)
|
|
|
}
|
|
|
|
|
|
+ s.mu.Lock()
|
|
|
+ defer s.mu.Unlock()
|
|
|
+ if wantIPv4 && s.cachedIPv4 == "" {
|
|
|
+ s.cachedIPv4 = ipv4
|
|
|
+ }
|
|
|
+ if wantIPv6 && s.cachedIPv6 == "" {
|
|
|
+ s.cachedIPv6 = ipv6
|
|
|
+ }
|
|
|
if s.cachedIPv6 == "N/A" {
|
|
|
s.noIPv6 = true
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// firstPublicIP returns the first service that answers, or "N/A" when every
|
|
|
+// one of them fails.
|
|
|
+func firstPublicIP(services []string) string {
|
|
|
+ var ip string
|
|
|
+ for _, service := range services {
|
|
|
+ ip = getPublicIP(service)
|
|
|
+ if ip != "N/A" {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return ip
|
|
|
+}
|
|
|
+
|
|
|
+// resolvePublicIPsInBackground keeps a status sample off the lookup path: a box
|
|
|
+// with no IPv6 route spends 3s per service, and the sample is what nodes report.
|
|
|
+func (s *ServerService) resolvePublicIPsInBackground() {
|
|
|
+ s.mu.Lock()
|
|
|
+ settled := s.cachedIPv4 != "" && (s.cachedIPv6 != "" || s.noIPv6)
|
|
|
+ if s.resolvingIPs || settled {
|
|
|
+ s.mu.Unlock()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ s.resolvingIPs = true
|
|
|
+ s.mu.Unlock()
|
|
|
+
|
|
|
+ go func() {
|
|
|
+ defer func() {
|
|
|
+ s.mu.Lock()
|
|
|
+ s.resolvingIPs = false
|
|
|
+ s.mu.Unlock()
|
|
|
+ }()
|
|
|
+ s.resolvePublicIPs()
|
|
|
+ }()
|
|
|
+}
|
|
|
+
|
|
|
+func (s *ServerService) publicIPs() (ipv4 string, ipv6 string) {
|
|
|
+ s.mu.Lock()
|
|
|
+ defer s.mu.Unlock()
|
|
|
+ return s.cachedIPv4, s.cachedIPv6
|
|
|
+}
|
|
|
+
|
|
|
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
|
|
now := time.Now()
|
|
|
status := &Status{
|
|
|
@@ -620,9 +680,8 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
|
|
logger.Warning("get udp connections failed:", err)
|
|
|
}
|
|
|
|
|
|
- s.resolvePublicIPs()
|
|
|
- status.PublicIP.IPv4 = s.cachedIPv4
|
|
|
- status.PublicIP.IPv6 = s.cachedIPv6
|
|
|
+ s.resolvePublicIPsInBackground()
|
|
|
+ status.PublicIP.IPv4, status.PublicIP.IPv6 = s.publicIPs()
|
|
|
|
|
|
// Xray status
|
|
|
if s.xrayService.IsXrayRunning() {
|
|
|
@@ -1549,10 +1608,11 @@ func (s *ServerService) backupHost(requestHost string) string {
|
|
|
}
|
|
|
if host == "" {
|
|
|
s.resolvePublicIPs()
|
|
|
- if ip := s.cachedIPv4; ip != "" && ip != "N/A" {
|
|
|
- host = ip
|
|
|
- } else if ip := s.cachedIPv6; ip != "" && ip != "N/A" {
|
|
|
- host = ip
|
|
|
+ ipv4, ipv6 := s.publicIPs()
|
|
|
+ if ipv4 != "" && ipv4 != "N/A" {
|
|
|
+ host = ipv4
|
|
|
+ } else if ipv6 != "" && ipv6 != "N/A" {
|
|
|
+ host = ipv6
|
|
|
}
|
|
|
}
|
|
|
return sanitizeBackupHost(host)
|