From e753e6e93c215780aef992d0481d894f19f5b9fe Mon Sep 17 00:00:00 2001 From: John Maguire Date: Tue, 21 Apr 2026 16:33:32 -0400 Subject: [PATCH] Immediate Lighthouse update after reconfig/reconnect (#1645) --- e2e/handshakes_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++ handshake_ix.go | 10 ++++++ lighthouse.go | 18 +++++++++- 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/e2e/handshakes_test.go b/e2e/handshakes_test.go index 67b166b1..7729465b 100644 --- a/e2e/handshakes_test.go +++ b/e2e/handshakes_test.go @@ -1369,6 +1369,81 @@ func TestV2NonPrimaryWithOffNetLighthouse(t *testing.T) { theirControl.Stop() } +func TestLighthouseUpdateOnReload(t *testing.T) { + ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{}) + + // Create the lighthouse + lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{"lighthouse": m{"am_lighthouse": true}}) + + // Create a client with NO lighthouse configured and a long update interval. + // The initial SendUpdate at startup will be a no-op since no lighthouses are known. + myControl, myVpnIpNet, _, myConfig := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", m{ + "lighthouse": m{ + "interval": 600, + "local_allow_list": m{ + "10.0.0.0/24": true, + "::/0": false, + }, + }, + }) + + r := router.NewR(t, lhControl, myControl) + defer r.RenderFlow() + + lhControl.Start() + myControl.Start() + + // Drain any startup packets (there should be none meaningful) + r.FlushAll() + + // Verify lighthouse has no knowledge of the client + assert.Nil(t, lhControl.QueryLighthouse(myVpnIpNet[0].Addr())) + + // Build a new config that adds the lighthouse + newSettings := make(m) + for k, v := range myConfig.Settings { + newSettings[k] = v + } + newSettings["static_host_map"] = m{ + lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()}, + } + newSettings["lighthouse"] = m{ + "hosts": []any{lhVpnIpNet[0].Addr().String()}, + "interval": 600, + "local_allow_list": m{ + "10.0.0.0/24": true, + "::/0": false, + }, + } + newCfg, err := yaml.Marshal(newSettings) + require.NoError(t, err) + + // Reload the config. The lighthouse.hosts change triggers TriggerUpdate, + // which wakes the update worker. It calls SendUpdate, initiating a + // handshake to the new lighthouse and caching the HostUpdateNotification. + require.NoError(t, myConfig.ReloadConfigString(string(newCfg))) + + // Route until the lighthouse receives the HostUpdateNotification. + // This covers: handshake stage 1, stage 2, then the cached update. + done := make(chan struct{}) + go func() { + r.RouteForAllUntilAfterMsgTypeTo(lhControl, header.LightHouse, 0) + close(done) + }() + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for lighthouse update after config reload") + } + + // Verify lighthouse now has the client's addresses + assert.NotNil(t, lhControl.QueryLighthouse(myVpnIpNet[0].Addr())) + + r.RenderHostmaps("Final hostmaps", lhControl, myControl) + lhControl.Stop() + myControl.Stop() +} + func TestGoodHandshakeUnsafeDest(t *testing.T) { unsafePrefix := "192.168.6.0/24" ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{}) diff --git a/handshake_ix.go b/handshake_ix.go index 4e04f450..f081eb8c 100644 --- a/handshake_ix.go +++ b/handshake_ix.go @@ -462,6 +462,11 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H) hostinfo.remotes.RefreshFromHandshake(vpnAddrs) + // Don't wait for UpdateWorker + if f.lightHouse.IsAnyLighthouseAddr(vpnAddrs) { + f.lightHouse.TriggerUpdate() + } + return } @@ -674,5 +679,10 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe hostinfo.remotes.RefreshFromHandshake(vpnAddrs) f.metricHandshakes.Update(duration) + // Don't wait for UpdateWorker + if f.lightHouse.IsAnyLighthouseAddr(vpnAddrs) { + f.lightHouse.TriggerUpdate() + } + return false } diff --git a/lighthouse.go b/lighthouse.go index 36eb9aa0..50140e9e 100644 --- a/lighthouse.go +++ b/lighthouse.go @@ -69,7 +69,8 @@ type LightHouse struct { // Addr's of relays that can be used by peers to access me relaysForMe atomic.Pointer[[]netip.Addr] - queryChan chan netip.Addr + updateTrigger chan struct{} + queryChan chan netip.Addr calculatedRemotes atomic.Pointer[bart.Table[[]*calculatedRemote]] // Maps VpnAddr to []*calculatedRemote @@ -105,6 +106,7 @@ func NewLightHouseFromConfig(ctx context.Context, l *logrus.Logger, c *config.C, nebulaPort: nebulaPort, punchConn: pc, punchy: p, + updateTrigger: make(chan struct{}, 1), queryChan: make(chan netip.Addr, c.GetUint32("handshakes.query_buffer", 64)), l: l, } @@ -316,6 +318,7 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error { if !initial { //NOTE: we are not tearing down existing lighthouse connections because they might be used for non lighthouse traffic lh.l.Info("lighthouse.hosts has changed") + lh.TriggerUpdate() } } @@ -841,11 +844,24 @@ func (lh *LightHouse) StartUpdateWorker() { return case <-clockSource.C: continue + case <-lh.updateTrigger: + continue } } }() } +// TriggerUpdate requests an immediate lighthouse update. This is a non-blocking +// operation intended to be called after a handshake completes with a lighthouse, +// so the lighthouse has our current addresses without waiting for the next +// periodic update. +func (lh *LightHouse) TriggerUpdate() { + select { + case lh.updateTrigger <- struct{}{}: + default: + } +} + func (lh *LightHouse) SendUpdate() { var v4 []*V4AddrPort var v6 []*V6AddrPort