fix(backup): name Telegram backups after webDomain/IP instead of x-ui

The bot's ServerService is a separate instance whose mutex-guarded LastStatus is never populated (only RefreshStatus fills it, which the bot never calls), so backupHost's public-IP fallback never fired and bot backups collapsed to x-ui when no webDomain was set.

Resolve the public IP directly via a new mutex-guarded resolvePublicIPs helper (extracted from GetStatus and shared with it) so the bot path gets a real address. Panel downloads keep using the browser request host; the Telegram bot falls back to webDomain then public IP.
This commit is contained in:
MHSanaei
2026-06-24 14:12:41 +02:00
parent 82600936d6
commit a5e865c109
+65 -53
View File
@@ -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)