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 f622b731..28782aae 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" @@ -16,6 +18,9 @@ import ( var StationeersAdvertisementEndpoint = config.GetStationeersServerPingEndpoint() +const maxTransientErrors = 5 +const advertiserIntervalSeconds = 30 + type ServerAdMessage struct { SessionId int Name string @@ -33,6 +38,41 @@ type ServerAdResponse struct { Status string } +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") + 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 ip := net.ParseIP(address); ip != nil { + if ip.To4() != nil { + return ip.To4().String(), 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 + } + // Return the first resolved IPv4 address + for _, ip := range ips { + if ip.To4() != nil { + return ip.To4().String(), nil + } + } + // If the address is invalid, return an error + return "", errors.New("unable to resolve IP from advertiser override") +} + 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...") @@ -40,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 { @@ -60,12 +107,20 @@ func StartAdvertiser() { case "linux": platform = 2 } + // Get IP address + ipAddress, err := getIpFromAdvertiserOverride(config.GetAdvertiserOverride()) + if err != nil { + 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, Name: config.GetServerName(), Password: config.GetServerPassword() != "", Version: config.GetExtractedGameVersion(), - Address: config.GetOverrideAdvertisedIp(), + Address: ipAddress, Port: config.GetGamePort(), Players: players, MaxPlayers: maxplayers, @@ -73,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) } }() } 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..82b472dd 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. 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() @@ -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() }