@@ -38,14 +38,31 @@ const offlineTimeoutThreshold = 3
3838
3939var idleCheckInterval = 5 * time .Second
4040
41- // discoveryPingTimeout bounds how long a LAN probe waits for upstream. Keep this
42- // below compat PingTimeout (2s) so clients pinging the bind port get a timely reply.
43- var discoveryPingTimeout = 1500 * time .Millisecond
41+ // Discovery probe deadlines (nanoseconds). Stored atomically so tests can shorten
42+ // them without racing the background health-check goroutine.
43+ var (
44+ discoveryPingTimeoutNanos atomic.Int64
45+ discoveryRecoveryProbeTimeoutNanos atomic.Int64
46+ discoveryHealthIntervalNanos atomic.Int64
47+ )
48+
49+ func init () {
50+ discoveryPingTimeoutNanos .Store (int64 (1500 * time .Millisecond ))
51+ discoveryRecoveryProbeTimeoutNanos .Store (int64 (500 * time .Millisecond ))
52+ discoveryHealthIntervalNanos .Store (int64 (2 * time .Second ))
53+ }
54+
55+ func discoveryPingTimeout () time.Duration {
56+ return time .Duration (discoveryPingTimeoutNanos .Load ())
57+ }
58+
59+ func discoveryRecoveryProbeTimeout () time.Duration {
60+ return time .Duration (discoveryRecoveryProbeTimeoutNanos .Load ())
61+ }
4462
45- // discoveryRecoveryProbeTimeout is used once the server is already marked offline:
46- // a quick check whether upstream came back without making every LAN ping wait out
47- // the full discoveryPingTimeout.
48- var discoveryRecoveryProbeTimeout = 500 * time .Millisecond
63+ func discoveryHealthInterval () time.Duration {
64+ return time .Duration (discoveryHealthIntervalNanos .Load ())
65+ }
4966
5067type ProxyServer struct {
5168 bindAddress * net.UDPAddr
@@ -185,6 +202,10 @@ func (proxy *ProxyServer) listen() error {
185202 }
186203 proxy .server .Store (proxyServer )
187204
205+ // Learn offline/online before the first console ping, and recover without
206+ // making OfflinePong wait on a synchronous probe.
207+ proxy .startUpstreamHealthCheck ()
208+
188209 return nil
189210}
190211
@@ -438,6 +459,18 @@ func (proxy *ProxyServer) handleDiscoveryPing(client net.Addr, ping []byte) erro
438459 return fmt .Errorf ("proxy not running" )
439460 }
440461
462+ // Already offline: reply immediately. Waiting on a recovery probe made
463+ // OfflinePong arrive after console discovery timeouts — blackholed remotes
464+ // take the full deadline, so the LAN entry never appeared even though logs
465+ // later showed a pong was sent.
466+ if proxy .isServerOffline () {
467+ if _ , err := server .WriteTo (proxy .buildOfflinePong (ping ), client ); err != nil {
468+ return err
469+ }
470+ log .Info ().Msgf ("Sent server offline pong to client: %v" , client .String ())
471+ return nil
472+ }
473+
441474 pong , err := proxy .probeRemoteUnconnectedPong (ping )
442475 if err != nil {
443476 proxy .markServerOffline ()
@@ -458,6 +491,49 @@ func (proxy *ProxyServer) handleDiscoveryPing(client net.Addr, ping []byte) erro
458491 return nil
459492}
460493
494+ // startUpstreamHealthCheck probes the remote on a timer so OfflinePong can be
495+ // advertised before the first LAN ping, and so recovery does not require a
496+ // client to wait out a synchronous probe.
497+ func (proxy * ProxyServer ) startUpstreamHealthCheck () {
498+ go func () {
499+ proxy .probeUpstreamHealth ()
500+ ticker := time .NewTicker (discoveryHealthInterval ())
501+ defer ticker .Stop ()
502+ for {
503+ select {
504+ case <- ticker .C :
505+ if proxy .dead .IsSet () {
506+ return
507+ }
508+ proxy .probeUpstreamHealth ()
509+ }
510+ }
511+ }()
512+ }
513+
514+ func (proxy * ProxyServer ) probeUpstreamHealth () {
515+ if proxy .dead .IsSet () || proxy .dataConn () == nil {
516+ return
517+ }
518+ _ , err := proxy .probeRemoteUnconnectedPong (healthCheckPing ())
519+ if err != nil {
520+ proxy .markServerOffline ()
521+ return
522+ }
523+ proxy .noteUpstreamReachable ()
524+ }
525+
526+ // healthCheckPing is a minimal valid Unconnected Ping for background probes.
527+ func healthCheckPing () []byte {
528+ magic := []byte {0x00 , 0xff , 0xff , 0x00 , 0xfe , 0xfe , 0xfe , 0xfe , 0xfd , 0xfd , 0xfd , 0xfd , 0x12 , 0x34 , 0x56 , 0x78 }
529+ out := make ([]byte , 0 , 1 + 8 + 8 + 16 )
530+ out = append (out , proto .UnconnectedPingID )
531+ out = append (out , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ) // ping time
532+ out = append (out , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ) // client GUID
533+ out = append (out , magic ... )
534+ return out
535+ }
536+
461537// probeRemoteUnconnectedPong dials a one-shot UDP socket to the remote and
462538// waits for an Unconnected Pong. A fresh local port avoids stale RakNet state.
463539func (proxy * ProxyServer ) probeRemoteUnconnectedPong (ping []byte ) ([]byte , error ) {
@@ -467,9 +543,9 @@ func (proxy *ProxyServer) probeRemoteUnconnectedPong(ping []byte) ([]byte, error
467543 }
468544 defer conn .Close ()
469545
470- timeout := discoveryPingTimeout
546+ timeout := discoveryPingTimeout ()
471547 if proxy .isServerOffline () {
472- timeout = discoveryRecoveryProbeTimeout
548+ timeout = discoveryRecoveryProbeTimeout ()
473549 }
474550 _ = conn .SetDeadline (time .Now ().Add (timeout ))
475551 if _ , err := conn .Write (ping ); err != nil {
0 commit comments