From edf60ea713d1de2689a254f77ded58fc2d59b5d5 Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 12:52:04 +0100 Subject: [PATCH 1/7] Update advertiser for DNS resolution and auto (public IP) modes --- src/advertiser/advertiser.go | 37 +++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index f622b731..ced924b4 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -3,6 +3,8 @@ package advertiser import ( "bytes" "encoding/json" + "errors" + "net" "net/http" "runtime" "strconv" @@ -33,6 +35,33 @@ type ServerAdResponse struct { Status string } +func getIpFromAddressConfig(address string) (string, error) { + // If the address is "auto", we need to check our public IPv4 via ipify + if address == "auto" { + resp, err := http.Get("https://api4.ipify.org") + if err != nil { + return "", err + } + defer resp.Body.Close() + buf := new(bytes.Buffer) + buf.ReadFrom(resp.Body) + return buf.String(), nil + } + // If the address is an IP quad, return it as is + if net.ParseIP(address) != nil { + return address, nil + } + // If the address is a DNS name, resolve it + ips, err := net.LookupIP(address) + if err != nil { + return "", err + } else if len(ips) > 0 { + return ips[0].String(), nil + } + // If the address is invalid, return an error + return "", errors.New("invalid address") +} + func StartAdvertiser() { if config.GetServerVisible() { logger.Advertiser.Warn("Server advertisement is enabled. Disable it in the config and restart SSUI to use manual advertisement. Skipping for now...") @@ -60,12 +89,18 @@ func StartAdvertiser() { case "linux": platform = 2 } + // Get IP address + ipAddress, err := getIpFromAddressConfig(config.GetOverrideAdvertisedIp()) + if err != nil { + logger.Advertiser.Errorf("ServerAdvertiser failed to get IP address from config value '%s': %v", config.GetOverrideAdvertisedIp(), err) + return + } adMessage := ServerAdMessage{ SessionId: sessionId, Name: config.GetServerName(), Password: config.GetServerPassword() != "", Version: config.GetExtractedGameVersion(), - Address: config.GetOverrideAdvertisedIp(), + Address: ipAddress, Port: config.GetGamePort(), Players: players, MaxPlayers: maxplayers, From 37d3d88479d66fd1f7334a459c2a7dee821f4b43 Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 14:24:10 +0100 Subject: [PATCH 2/7] rename advertiser config variable --- src/advertiser/advertiser.go | 6 +++--- src/config/config.go | 14 +++++++------- src/config/getters.go | 4 ++-- src/config/setters.go | 4 ++-- src/config/vars.go | 2 +- src/core/loader/cmdargs.go | 16 ++++++++-------- src/core/loader/loader.go | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index ced924b4..ffa7168a 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -35,7 +35,7 @@ type ServerAdResponse struct { Status string } -func getIpFromAddressConfig(address string) (string, error) { +func getIpFromAdvertiserOverride(address string) (string, error) { // If the address is "auto", we need to check our public IPv4 via ipify if address == "auto" { resp, err := http.Get("https://api4.ipify.org") @@ -90,9 +90,9 @@ func StartAdvertiser() { platform = 2 } // Get IP address - ipAddress, err := getIpFromAddressConfig(config.GetOverrideAdvertisedIp()) + ipAddress, err := getIpFromAdvertiserOverride(config.GetAdvertiserOverride()) if err != nil { - logger.Advertiser.Errorf("ServerAdvertiser failed to get IP address from config value '%s': %v", config.GetOverrideAdvertisedIp(), err) + logger.Advertiser.Errorf("ServerAdvertiser failed to get IP address from config value '%s': %v", config.GetAdvertiserOverride(), err) return } adMessage := ServerAdMessage{ diff --git a/src/config/config.go b/src/config/config.go index d1a6f52b..d9d01a0b 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -51,11 +51,11 @@ type JsonConfig struct { StartLocation string `json:"StartLocation"` // Logging and debug settings - Debug *bool `json:"Debug"` - CreateSSUILogFile *bool `json:"CreateSSUILogFile"` - LogLevel int `json:"LogLevel"` - SubsystemFilters []string `json:"subsystemFilters"` - OverrideAdvertisedIp string `json:"OverrideAdvertisedIp"` + Debug *bool `json:"Debug"` + CreateSSUILogFile *bool `json:"CreateSSUILogFile"` + LogLevel int `json:"LogLevel"` + SubsystemFilters []string `json:"subsystemFilters"` + AdvertiserOverride string `json:"AdvertiserOverride"` // Authentication Settings Users map[string]string `json:"users"` // Map of username to hashed password @@ -297,7 +297,7 @@ func applyConfig(cfg *JsonConfig) { // use Safebackups folder either way. ConfiguredSafeBackupDir = filepath.Join("./saves/", SaveName, "Safebackups") - OverrideAdvertisedIp = getString(cfg.OverrideAdvertisedIp, "OVERRIDE_ADVERTISED_IP", "") + AdvertiserOverride = getString(cfg.AdvertiserOverride, "ADVERTISER_OVERRIDE", "") safeSaveConfig() } @@ -368,7 +368,7 @@ func safeSaveConfig() error { AutoStartServerOnStartup: &AutoStartServerOnStartup, SSUIIdentifier: SSUIIdentifier, SSUIWebPort: SSUIWebPort, - OverrideAdvertisedIp: OverrideAdvertisedIp, + AdvertiserOverride: AdvertiserOverride, } file, err := os.Create(ConfigPath) diff --git a/src/config/getters.go b/src/config/getters.go index 2b8a7f7f..a71d7ab0 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -519,10 +519,10 @@ func GetIsGameServerRunning() bool { defer ConfigMu.RUnlock() return IsGameServerRunning } -func GetOverrideAdvertisedIp() string { +func GetAdvertiserOverride() string { ConfigMu.RLock() defer ConfigMu.RUnlock() - return OverrideAdvertisedIp + return AdvertiserOverride } func GetStationeersServerPingEndpoint() string { diff --git a/src/config/setters.go b/src/config/setters.go index c0f305b1..3f8b603b 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -703,10 +703,10 @@ func SetAllowAutoGameServerUpdates(value bool) error { return safeSaveConfig() } -func SetOverrideAdvertisedIp(value string) error { +func SetAdvertiserOverride(value string) error { ConfigMu.Lock() defer ConfigMu.Unlock() - OverrideAdvertisedIp = value + AdvertiserOverride = value return safeSaveConfig() } diff --git a/src/config/vars.go b/src/config/vars.go index 50ecb59e..f1e6e192 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -61,7 +61,7 @@ var ( LanguageSetting string AutoStartServerOnStartup bool SSUIIdentifier string - OverrideAdvertisedIp string + AdvertiserOverride string ) // Runtime only variables diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index b6839216..1567c50c 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -21,7 +21,7 @@ var recoveryPasswordFlag string var devModeFlag bool var skipSteamCMDFlag bool var sanityCheckFlag bool -var overrideAdvertisedIpFlag string +var advertiserOverrideFlag string // ParseFlags parses command-line arguments ONCE at startup (called from func main) func ParseFlags() { @@ -40,7 +40,7 @@ func ParseFlags() { flag.BoolVar(&createSSUILogFileFlag, "lf", false, "(Alias) Create log files for SSUI") flag.BoolVar(&skipSteamCMDFlag, "NoSteamCMD", false, "Skips SteamCMD installation") flag.BoolVar(&sanityCheckFlag, "NoSanityCheck", false, "Skips the sanity check. Not recommended.") - flag.StringVar(&overrideAdvertisedIpFlag, "OverrideAdvertisedIp", "", "Override the advertised server IP (to allow server advertisement if you are behind a reverse proxy)") + flag.StringVar(&advertiserOverrideFlag, "AdvertiserOverride", "", "Override the advertised server IP (to allow server advertisement if you are behind a reverse proxy)") // Parse command-line flags flag.Parse() @@ -102,15 +102,15 @@ func HandleFlags() { logger.Main.Info(fmt.Sprintf("Overriding IsDebugMode from command line: Before=%t, Now=true", oldDebug)) } - if overrideAdvertisedIpFlag != "" { - oldOverrideAdvertisedIp := config.GetOverrideAdvertisedIp() + if advertiserOverrideFlag != "" { + oldAdvertiserOverride := config.GetAdvertiserOverride() - if overrideAdvertisedIpFlag == oldOverrideAdvertisedIp { - logger.Advertiser.Info(fmt.Sprintf("Advertised Server IP is already set to %s", overrideAdvertisedIpFlag)) + if advertiserOverrideFlag == oldAdvertiserOverride { + logger.Advertiser.Info(fmt.Sprintf("Advertised Server IP is already set to %s", advertiserOverrideFlag)) return } - config.SetOverrideAdvertisedIp(overrideAdvertisedIpFlag) - logger.Advertiser.Info(fmt.Sprintf("Overriding Advertised Server IP from command line: Before=%s, Now=%s", oldOverrideAdvertisedIp, overrideAdvertisedIpFlag)) + config.SetAdvertiserOverride(advertiserOverrideFlag) + logger.Advertiser.Info(fmt.Sprintf("Overriding Advertised Server IP from command line: Before=%s, Now=%s", oldAdvertiserOverride, advertiserOverrideFlag)) } if createSSUILogFileFlag { diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index 2a47bb47..ca2c3810 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -101,7 +101,7 @@ func ReloadAppInfoPoller() { } func LoadAdvertiser() { - if config.GetOverrideAdvertisedIp() != "" { + if config.GetAdvertiserOverride() != "" { logger.Advertiser.Info("Starting server advertiser...") advertiser.StartAdvertiser() } From f0f06d52c06950c5e748402bacb68f16ca4a212b Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 22:49:46 +0900 Subject: [PATCH 3/7] Update src/advertiser/advertiser.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/advertiser/advertiser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index ffa7168a..6f547d71 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -48,7 +48,7 @@ func getIpFromAdvertiserOverride(address string) (string, error) { return buf.String(), nil } // If the address is an IP quad, return it as is - if net.ParseIP(address) != nil { + if ip := net.ParseIP(address); ip != nil && ip.To4() != nil { return address, nil } // If the address is a DNS name, resolve it From 2847c642547678192a8b7ebde3f570548fb52fd6 Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 15:18:39 +0100 Subject: [PATCH 4/7] Added more checks for IPv6 --- .vscode/launch.json | 10 ---------- src/advertiser/advertiser.go | 17 +++++++++++++---- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 23382b36..ed0a4c87 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,15 +30,5 @@ "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm "args": ["--NoSteamCMD"] }, - { - "name": "Debug Go Server noSteamCMD noSvelte overrideAdvertisedIp", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "${workspaceFolder}/server.go", - "console": "integratedTerminal", - "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm - "args": ["--NoSteamCMD", "--OverrideAdvertisedIp=127.0.0.1"] - } ] } \ No newline at end of file diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index 6f547d71..f76ccece 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -48,18 +48,27 @@ func getIpFromAdvertiserOverride(address string) (string, error) { return buf.String(), nil } // If the address is an IP quad, return it as is - if ip := net.ParseIP(address); ip != nil && ip.To4() != nil { - return address, nil + if ip := net.ParseIP(address); ip != nil { + if ip.To4() != nil { + return address, nil + } else if ip.To16() != nil { + return "", errors.New("IPv6 addresses are not supported for advertiser override") + } } // If the address is a DNS name, resolve it ips, err := net.LookupIP(address) if err != nil { return "", err } else if len(ips) > 0 { - return ips[0].String(), nil + // Return the first resolved IPv4 address + for _, ip := range ips { + if ip.To4() != nil { + return ip.String(), nil + } + } } // If the address is invalid, return an error - return "", errors.New("invalid address") + return "", errors.New("unable to resolve IP from advertiser override") } func StartAdvertiser() { From 8c344b7be5cd2a896e9a7ea52ef9fc8cf534f2f2 Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 23:24:25 +0900 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/advertiser/advertiser.go | 11 +++++------ src/core/loader/cmdargs.go | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index f76ccece..0f3f5ede 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -59,12 +59,11 @@ func getIpFromAdvertiserOverride(address string) (string, error) { ips, err := net.LookupIP(address) if err != nil { return "", err - } else if len(ips) > 0 { - // Return the first resolved IPv4 address - for _, ip := range ips { - if ip.To4() != nil { - return ip.String(), nil - } + } + // Return the first resolved IPv4 address + for _, ip := range ips { + if ip.To4() != nil { + return ip.String(), nil } } // If the address is invalid, return an error diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index 1567c50c..8fd43619 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -40,7 +40,7 @@ func ParseFlags() { flag.BoolVar(&createSSUILogFileFlag, "lf", false, "(Alias) Create log files for SSUI") flag.BoolVar(&skipSteamCMDFlag, "NoSteamCMD", false, "Skips SteamCMD installation") flag.BoolVar(&sanityCheckFlag, "NoSanityCheck", false, "Skips the sanity check. Not recommended.") - flag.StringVar(&advertiserOverrideFlag, "AdvertiserOverride", "", "Override the advertised server IP (to allow server advertisement if you are behind a reverse proxy)") + flag.StringVar(&advertiserOverrideFlag, "AdvertiserOverride", "", "Override the advertised server IP. Use \"auto\" for automatic public IP detection, an IPv4 address, or a DNS hostname (to allow server advertisement if you are behind a reverse proxy)") // Parse command-line flags flag.Parse() From 91b6117a3a4ceda9de109b07da5d127e6925c615 Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 16:02:02 +0100 Subject: [PATCH 6/7] Fixed memory leak and improved resilience --- src/advertiser/advertiser.go | 53 +++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index 0f3f5ede..28782aae 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -18,6 +18,9 @@ import ( var StationeersAdvertisementEndpoint = config.GetStationeersServerPingEndpoint() +const maxTransientErrors = 5 +const advertiserIntervalSeconds = 30 + type ServerAdMessage struct { SessionId int Name string @@ -50,7 +53,7 @@ func getIpFromAdvertiserOverride(address string) (string, error) { // If the address is an IP quad, return it as is if ip := net.ParseIP(address); ip != nil { if ip.To4() != nil { - return address, nil + return ip.To4().String(), nil } else if ip.To16() != nil { return "", errors.New("IPv6 addresses are not supported for advertiser override") } @@ -63,7 +66,7 @@ func getIpFromAdvertiserOverride(address string) (string, error) { // Return the first resolved IPv4 address for _, ip := range ips { if ip.To4() != nil { - return ip.String(), nil + return ip.To4().String(), nil } } // If the address is invalid, return an error @@ -77,9 +80,16 @@ func StartAdvertiser() { } go func() { sessionId := -1 + // Track accumulated transient errors and kill the advertiser if we exceed a threshold + transientErrors := 0 for { // Only advertise if we are running if gamemgr.InternalIsServerRunning() { + // If we have exceeded the max transient errors, exit the advertiser + if transientErrors >= maxTransientErrors { + logger.Advertiser.Errorf("ServerAdvertiser exceeded max transient errors (%d). Stopping advertiser...", maxTransientErrors) + return + } // Get max players maxplayers, err := strconv.Atoi(config.GetServerMaxPlayers()) if err != nil { @@ -100,8 +110,10 @@ func StartAdvertiser() { // Get IP address ipAddress, err := getIpFromAdvertiserOverride(config.GetAdvertiserOverride()) if err != nil { - logger.Advertiser.Errorf("ServerAdvertiser failed to get IP address from config value '%s': %v", config.GetAdvertiserOverride(), err) - return + logger.Advertiser.Warnf("ServerAdvertiser failed to get IP address from config value '%s': %v", config.GetAdvertiserOverride(), err) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + continue } adMessage := ServerAdMessage{ SessionId: sessionId, @@ -116,38 +128,53 @@ func StartAdvertiser() { } body, err := json.Marshal(adMessage) if err != nil { - logger.Advertiser.Errorf("ServerAdvertiser failed to Serialize to JSON from native Go struct type: %v", err) - return + logger.Advertiser.Warnf("ServerAdvertiser failed to Serialize to JSON from native Go struct type: %v", err) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + continue } // Send advertisement resp, err := http.Post(StationeersAdvertisementEndpoint, "application/json", bytes.NewBuffer(body)) // Check for errors if err != nil { - logger.Advertiser.Errorf("ServerAdvertiser failed to send request: %v", err) - return + logger.Advertiser.Warnf("ServerAdvertiser failed to send request: %v", err) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + continue } - defer resp.Body.Close() - // Check the status + // Check for non-200 status codes if resp.StatusCode != 200 { logger.Advertiser.Warnf("ServerAdvertiser received non-200 status: %d", resp.StatusCode) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + resp.Body.Close() // Close the response body + continue } // Read the response and update our sessionId if needed adResponse := ServerAdResponse{} err = json.NewDecoder(resp.Body).Decode(&adResponse) + resp.Body.Close() if err != nil { - logger.Advertiser.Errorf("Failed to decode response body: %v", err) - return + logger.Advertiser.Warnf("Failed to decode response body: %v", err) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + continue } if adResponse.Status != "Success" { logger.Advertiser.Warnf("ServerAdvertiser received unexpected status: %s", adResponse.Status) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + continue } + // Reset transient errors on success sessionId = adResponse.SessionId + transientErrors = 0 } else { // Reset sessionid for the next run sessionId = -1 } // Sleep for 30 seconds to follow the standard advertisement timer - time.Sleep(30 * time.Second) + time.Sleep(advertiserIntervalSeconds * time.Second) } }() } From 62bc5f0e13c118ed69f28ea433acb2f24e828885 Mon Sep 17 00:00:00 2001 From: JLangisch Date: Tue, 28 Oct 2025 21:54:35 +0100 Subject: [PATCH 7/7] Add ServerVisible info to help output of AdvertiserOverride flag --- src/core/loader/cmdargs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index 8fd43619..82b472dd 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -40,7 +40,7 @@ func ParseFlags() { flag.BoolVar(&createSSUILogFileFlag, "lf", false, "(Alias) Create log files for SSUI") flag.BoolVar(&skipSteamCMDFlag, "NoSteamCMD", false, "Skips SteamCMD installation") flag.BoolVar(&sanityCheckFlag, "NoSanityCheck", false, "Skips the sanity check. Not recommended.") - flag.StringVar(&advertiserOverrideFlag, "AdvertiserOverride", "", "Override the advertised server IP. Use \"auto\" for automatic public IP detection, an IPv4 address, or a DNS hostname (to allow server advertisement if you are behind a reverse proxy)") + flag.StringVar(&advertiserOverrideFlag, "AdvertiserOverride", "", "Override the advertised server IP. For this, the ServerVisible setting must be set to false. Use \"auto\" for automatic public IP detection, an IPv4 address, or a DNS hostname (to allow server advertisement if you are behind a reverse proxy)") // Parse command-line flags flag.Parse()