From ec3dc8993f5e0a49a8b9349fa19cec018dad0fff Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 22 Oct 2026 15:01:17 -0400 Subject: [PATCH] Add keepalive messages --- holepunch/holepunch.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/holepunch/holepunch.go b/holepunch/holepunch.go index 99828d7..9432e92 100644 --- a/holepunch/holepunch.go +++ b/holepunch/holepunch.go @@ -56,6 +56,19 @@ type Manager struct { const defaultSendHolepunchIntervalMax = 60 * time.Second const defaultSendHolepunchIntervalMin = 1 * time.Second +// natKeepaliveInterval is how often a keepalive is sent to each exit node +// between hole punches once the hole punch interval has backed off past it. +// It is under the 30s timeout many NATs (e.g. Linux conntrack) give UDP flows +// that never see a reply, so the NAT mapping - and with it the endpoint the +// server recorded from the last hole punch - stays the same between punches. +const natKeepaliveInterval = 20 * time.Second + +// natKeepalivePacket only exists to keep the NAT mapping alive, so it is much +// cheaper for the exit node than a hole punch. Gerbil drops it on sight; older +// gerbils fail to parse the ephemeral key and drop it silently before doing +// any crypto. +var natKeepalivePacket = []byte(`{"ephemeralPublicKey":"keepalive"}`) + // NewManager creates a new hole punch manager func NewManager(sharedBind *bind.SharedBind, ID string, clientType string, publicKey string, publicDNS []string) *Manager { return &Manager{ @@ -514,6 +527,9 @@ func (m *Manager) runMultipleExitNodes() { ticker := time.NewTicker(m.sendHolepunchInterval) defer ticker.Stop() + keepaliveTicker := time.NewTicker(natKeepaliveInterval) + defer keepaliveTicker.Stop() + for { select { case <-m.stopChan: @@ -539,6 +555,22 @@ func (m *Manager) runMultipleExitNodes() { logger.Debug("Failed to send hole punch to %s: %v", node.endpointName, err) } } + case <-keepaliveTicker.C: + // Only needed once hole punches are further apart than the + // keepalive interval. Custom longer intervals (olm low power mode) + // are there to save battery, so let the mapping lapse then. + m.mu.Lock() + needKeepalive := m.sendHolepunchInterval > natKeepaliveInterval && + m.sendHolepunchIntervalMax <= m.defaultIntervalMax + m.mu.Unlock() + if !needKeepalive { + continue + } + for _, node := range resolvedNodes { + if _, err := m.sharedBind.WriteToUDP(natKeepalivePacket, node.remoteAddr); err != nil { + logger.Debug("Failed to send NAT keepalive to %s: %v", node.endpointName, err) + } + } case <-ticker.C: // Send hole punch to all exit nodes (if any are available) if len(resolvedNodes) > 0 {