@@ -90,6 +90,16 @@ const (
9090 ZeroWinProbeMax = 30 * time .Second // max zero-window probe backoff
9191)
9292
93+ // EndpointCacheTTL is how long a cached endpoint is considered fresh.
94+ // After this, the entry is stale but still usable as a fallback.
95+ const EndpointCacheTTL = 5 * time .Minute
96+
97+ // endpointEntry caches a resolved endpoint for a peer node.
98+ type endpointEntry struct {
99+ addr string // "host:port"
100+ cachedAt time.Time // when the entry was stored
101+ }
102+
93103type Daemon struct {
94104 config Config
95105 addrMu sync.RWMutex // protects nodeID and addr (H6 fix)
@@ -107,6 +117,10 @@ type Daemon struct {
107117 stopCh chan struct {} // closed on Stop() to signal goroutines
108118 lanAddrs []string // LAN addresses for same-network peer detection
109119
120+ // Endpoint cache: nodeID -> last-known endpoint (peer resilience)
121+ epCacheMu sync.RWMutex
122+ epCache map [uint32 ]* endpointEntry
123+
110124 // SYN rate limiter (token bucket)
111125 synMu sync.Mutex
112126 synTokens int
@@ -177,6 +191,7 @@ func New(cfg Config) *Daemon {
177191 synTokens : cfg .synRateLimit (),
178192 synLastFill : time .Now (),
179193 perSrcSYN : make (map [uint32 ]* srcSYNBucket ),
194+ epCache : make (map [uint32 ]* endpointEntry ),
180195 }
181196 d .ipc = NewIPCServer (cfg .SocketPath , d )
182197 d .handshakes = NewHandshakeManager (d )
@@ -744,12 +759,15 @@ type DaemonInfo struct {
744759 Identity bool // true if identity is persisted
745760 PublicKey string // base64 Ed25519 public key (empty if no identity)
746761 Email string // email address for account identification and key recovery
747- BytesSent uint64
748- BytesRecv uint64
749- PktsSent uint64
750- PktsRecv uint64
751- PeerList []PeerInfo
752- ConnList []ConnectionInfo
762+ BytesSent uint64
763+ BytesRecv uint64
764+ PktsSent uint64
765+ PktsRecv uint64
766+ EncryptOK uint64
767+ EncryptFail uint64
768+ HandshakePendingCount int
769+ PeerList []PeerInfo
770+ ConnList []ConnectionInfo
753771}
754772
755773// Info returns current daemon status.
@@ -805,12 +823,15 @@ func (d *Daemon) Info() *DaemonInfo {
805823 Identity : hasIdentity ,
806824 PublicKey : pubKeyStr ,
807825 Email : d .config .Email ,
808- BytesSent : atomic .LoadUint64 (& d .tunnels .BytesSent ),
809- BytesRecv : atomic .LoadUint64 (& d .tunnels .BytesRecv ),
810- PktsSent : atomic .LoadUint64 (& d .tunnels .PktsSent ),
811- PktsRecv : atomic .LoadUint64 (& d .tunnels .PktsRecv ),
812- PeerList : peerList ,
813- ConnList : d .ports .ConnectionList (),
826+ BytesSent : atomic .LoadUint64 (& d .tunnels .BytesSent ),
827+ BytesRecv : atomic .LoadUint64 (& d .tunnels .BytesRecv ),
828+ PktsSent : atomic .LoadUint64 (& d .tunnels .PktsSent ),
829+ PktsRecv : atomic .LoadUint64 (& d .tunnels .PktsRecv ),
830+ EncryptOK : atomic .LoadUint64 (& d .tunnels .EncryptOK ),
831+ EncryptFail : atomic .LoadUint64 (& d .tunnels .EncryptFail ),
832+ HandshakePendingCount : d .handshakes .PendingCount (),
833+ PeerList : peerList ,
834+ ConnList : d .ports .ConnectionList (),
814835 }
815836}
816837
@@ -1822,8 +1843,45 @@ func (d *Daemon) broadcastDatagram(netID uint16, srcPort, dstPort uint16, data [
18221843 return nil
18231844}
18241845
1846+ // cacheEndpoint stores a resolved endpoint in the cache.
1847+ func (d * Daemon ) cacheEndpoint (nodeID uint32 , addr string ) {
1848+ d .epCacheMu .Lock ()
1849+ d .epCache [nodeID ] = & endpointEntry {addr : addr , cachedAt : time .Now ()}
1850+ d .epCacheMu .Unlock ()
1851+ }
1852+
1853+ // cachedEndpoint returns a previously cached endpoint, or empty string if none.
1854+ // The second return value is true if the entry exists (even if stale).
1855+ func (d * Daemon ) cachedEndpoint (nodeID uint32 ) (string , bool ) {
1856+ d .epCacheMu .RLock ()
1857+ e , ok := d .epCache [nodeID ]
1858+ d .epCacheMu .RUnlock ()
1859+ if ! ok {
1860+ return "" , false
1861+ }
1862+ return e .addr , true
1863+ }
1864+
1865+ // isEndpointStale returns true if the cached entry is older than EndpointCacheTTL.
1866+ // Stale entries are still usable as fallback, but a fresh resolve is preferred.
1867+ func (d * Daemon ) isEndpointStale (nodeID uint32 ) bool {
1868+ d .epCacheMu .RLock ()
1869+ e , ok := d .epCache [nodeID ]
1870+ d .epCacheMu .RUnlock ()
1871+ if ! ok {
1872+ return true
1873+ }
1874+ return time .Since (e .cachedAt ) > EndpointCacheTTL
1875+ }
1876+
1877+ // CachedEndpoint returns a previously cached endpoint for a peer (exported for testing).
1878+ func (d * Daemon ) CachedEndpoint (nodeID uint32 ) (string , bool ) {
1879+ return d .cachedEndpoint (nodeID )
1880+ }
1881+
18251882// ensureTunnel makes sure we have a route to the given node.
18261883// Requests beacon hole-punching for NAT traversal when beacon is configured.
1884+ // Uses an endpoint cache as fallback when the registry is unreachable.
18271885func (d * Daemon ) ensureTunnel (nodeID uint32 ) error {
18281886 if d .tunnels .HasPeer (nodeID ) {
18291887 return nil
@@ -1832,6 +1890,18 @@ func (d *Daemon) ensureTunnel(nodeID uint32) error {
18321890 // Resolve the node's real address from registry (requires our node ID)
18331891 resp , err := d .regConn .Resolve (nodeID , d .NodeID ())
18341892 if err != nil {
1893+ // Registry unreachable — fall back to cached endpoint
1894+ if cached , ok := d .cachedEndpoint (nodeID ); ok {
1895+ stale := d .isEndpointStale (nodeID )
1896+ slog .Warn ("registry resolve failed, using cached endpoint" ,
1897+ "node_id" , nodeID , "cached_addr" , cached , "stale" , stale , "error" , err )
1898+ udpAddr , udpErr := net .ResolveUDPAddr ("udp" , cached )
1899+ if udpErr != nil {
1900+ return fmt .Errorf ("resolve cached %s: %w" , cached , udpErr )
1901+ }
1902+ d .tunnels .AddPeer (nodeID , udpAddr )
1903+ return nil
1904+ }
18351905 return fmt .Errorf ("resolve node %d: %w" , nodeID , err )
18361906 }
18371907
@@ -1849,6 +1919,9 @@ func (d *Daemon) ensureTunnel(nodeID uint32) error {
18491919 }
18501920 }
18511921
1922+ // Cache the resolved endpoint for fallback
1923+ d .cacheEndpoint (nodeID , targetAddr )
1924+
18521925 udpAddr , err := net .ResolveUDPAddr ("udp" , targetAddr )
18531926 if err != nil {
18541927 return fmt .Errorf ("resolve %s: %w" , targetAddr , err )
0 commit comments