diff --git a/internal/web/service/server.go b/internal/web/service/server.go index 8775d5a5a..bab17a323 100644 --- a/internal/web/service/server.go +++ b/internal/web/service/server.go @@ -375,6 +375,53 @@ func getPublicIP(url string) string { return ipString } +var publicIPv4Services = []string{ + "https://api4.ipify.org", + "https://ipv4.icanhazip.com", + "https://v4.api.ipinfo.io/ip", + "https://ipv4.myexternalip.com/raw", + "https://4.ident.me", + "https://check-host.net/ip", +} + +var publicIPv6Services = []string{ + "https://api6.ipify.org", + "https://ipv6.icanhazip.com", + "https://v6.api.ipinfo.io/ip", + "https://ipv6.myexternalip.com/raw", + "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. +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 + } + } + } + + if s.cachedIPv6 == "" && !s.noIPv6 { + for _, ip6Service := range publicIPv6Services { + s.cachedIPv6 = getPublicIP(ip6Service) + if s.cachedIPv6 != "N/A" { + break + } + } + } + + if s.cachedIPv6 == "N/A" { + s.noIPv6 = true + } +} + func (s *ServerService) GetStatus(lastStatus *Status) *Status { now := time.Now() status := &Status{ @@ -536,45 +583,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { logger.Warning("get udp connections failed:", err) } - // IP fetching with caching - showIp4ServiceLists := []string{ - "https://api4.ipify.org", - "https://ipv4.icanhazip.com", - "https://v4.api.ipinfo.io/ip", - "https://ipv4.myexternalip.com/raw", - "https://4.ident.me", - "https://check-host.net/ip", - } - showIp6ServiceLists := []string{ - "https://api6.ipify.org", - "https://ipv6.icanhazip.com", - "https://v6.api.ipinfo.io/ip", - "https://ipv6.myexternalip.com/raw", - "https://6.ident.me", - } - - if s.cachedIPv4 == "" { - for _, ip4Service := range showIp4ServiceLists { - s.cachedIPv4 = getPublicIP(ip4Service) - if s.cachedIPv4 != "N/A" { - break - } - } - } - - if s.cachedIPv6 == "" && !s.noIPv6 { - for _, ip6Service := range showIp6ServiceLists { - s.cachedIPv6 = getPublicIP(ip6Service) - if s.cachedIPv6 != "N/A" { - break - } - } - } - - if s.cachedIPv6 == "N/A" { - s.noIPv6 = true - } - + s.resolvePublicIPs() status.PublicIP.IPv4 = s.cachedIPv4 status.PublicIP.IPv6 = s.cachedIPv6 @@ -1282,11 +1291,12 @@ func (s *ServerService) GetDb() ([]byte, error) { // BackupFilename returns the filename for a database backup, named after the // panel's address so a downloaded or Telegram-sent backup identifies the server -// it came from. requestHost is the browser's address (the getDb handler passes -// c.Request.Host, matching the host shown in the panel title); it is preferred -// when present, otherwise the configured web domain and then the server's public -// IP are used. The extension is .dump on PostgreSQL and .db on SQLite; the base -// falls back to "x-ui" when no address is known. +// it came from. requestHost is the browser's address: the getDb handler passes +// c.Request.Host so a panel download is named after whatever address the user +// reached the panel with, no Listen Domain needed. The Telegram bot has no +// request and passes "", falling back to the configured Listen Domain (webDomain) +// and then the public IP. The extension is .dump on PostgreSQL and .db on SQLite; +// the base falls back to "x-ui" when no address is known. func (s *ServerService) BackupFilename(requestHost string) string { ext := ".db" if database.IsPostgres() { @@ -1296,9 +1306,12 @@ func (s *ServerService) BackupFilename(requestHost string) string { } // backupHost picks the address used to name backup files: the browser's request -// host (port stripped, the same value the panel title shows) when available, -// otherwise the configured web domain and then the cached public IP (IPv4 before -// IPv6), reduced to safe filename characters. +// host (port stripped) when available, otherwise the configured Listen Domain +// (webDomain) and then the resolved public IP (IPv4 before IPv6), reduced to safe +// filename characters. The public IP is resolved directly rather than read from +// LastStatus so callers whose ServerService never runs the status ticker — +// notably the Telegram bot — still get a real address instead of the "x-ui" +// fallback. func (s *ServerService) backupHost(requestHost string) string { host := extractHostname(strings.TrimSpace(requestHost)) if host == "" { @@ -1307,12 +1320,11 @@ func (s *ServerService) backupHost(requestHost string) string { } } if host == "" { - if st := s.LastStatus(); st != nil { - if ip := st.PublicIP.IPv4; ip != "" && ip != "N/A" { - host = ip - } else if ip := st.PublicIP.IPv6; ip != "" && ip != "N/A" { - host = ip - } + s.resolvePublicIPs() + if ip := s.cachedIPv4; ip != "" && ip != "N/A" { + host = ip + } else if ip := s.cachedIPv6; ip != "" && ip != "N/A" { + host = ip } } return sanitizeBackupHost(host)