From f91b2fe9f8d5e77f6308eb0177fdff5cc2888a19 Mon Sep 17 00:00:00 2001 From: Fco Javier Felix Date: Sun, 25 Oct 2026 21:31:13 +0100 Subject: [PATCH] fix(configgen): include peer endpoints for lighthouses Lighthouses do not query each other, so they need static endpoints to establish peer tunnels. Populate their static host map while excluding their own addresses and keeping lighthouse.hosts empty. Add regression coverage for mutual lighthouse reachability with IPv4 and IPv6 addresses. --- internal/configgen/lighthouse_peers_test.go | 50 +++++++++++++++++++++ internal/configgen/marshal.go | 21 +++++---- 2 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 internal/configgen/lighthouse_peers_test.go diff --git a/internal/configgen/lighthouse_peers_test.go b/internal/configgen/lighthouse_peers_test.go new file mode 100644 index 0000000..d7feea8 --- /dev/null +++ b/internal/configgen/lighthouse_peers_test.go @@ -0,0 +1,50 @@ +package configgen + +import ( + "testing" + + "gopkg.in/yaml.v3" +) + +func TestGenerate_LighthousesCanReachEachOther(t *testing.T) { + lighthouses := []LighthouseInfo{ + {NebulaIPs: []string{"192.168.100.1", "fd00::1"}, PublicAddr: "203.0.113.10:4242"}, + {NebulaIPs: []string{"192.168.100.2", "fd00::2"}, PublicAddr: "203.0.113.20:4242"}, + } + for _, own := range lighthouses { + t.Run(own.NebulaIPs[0], func(t *testing.T) { + data, err := Generate(GeneratorInput{ + NebulaIPs: own.NebulaIPs, IsLighthouse: true, Lighthouses: lighthouses, + }) + if err != nil { + t.Fatal(err) + } + var parsed struct { + StaticHostMap map[string][]string `yaml:"static_host_map"` + Lighthouse struct { + AmLighthouse bool `yaml:"am_lighthouse"` + Hosts []string `yaml:"hosts"` + } `yaml:"lighthouse"` + } + if err := yaml.Unmarshal(data, &parsed); err != nil { + t.Fatal(err) + } + if !parsed.Lighthouse.AmLighthouse || len(parsed.Lighthouse.Hosts) != 0 { + t.Fatalf("lighthouses must remain independent: %+v", parsed.Lighthouse) + } + for _, peer := range lighthouses { + for _, ip := range peer.NebulaIPs { + if peer.PublicAddr == own.PublicAddr { + if _, ok := parsed.StaticHostMap[ip]; ok { + t.Errorf("lighthouse advertises itself as a remote: %s", ip) + } + continue + } + if got := parsed.StaticHostMap[ip]; len(got) != 1 || got[0] != peer.PublicAddr { + t.Errorf("cannot locate peer %s: got %v, want [%s]", ip, got, peer.PublicAddr) + } + } + } + }) + } +} diff --git a/internal/configgen/marshal.go b/internal/configgen/marshal.go index 176b297..38678a5 100644 --- a/internal/configgen/marshal.go +++ b/internal/configgen/marshal.go @@ -3,6 +3,7 @@ package configgen import ( "bytes" "fmt" + "slices" "strings" "unicode/utf8" @@ -270,17 +271,19 @@ func buildConfig(input GeneratorInput) nebulaConfig { } cfg.StaticHostMap = map[safeString][]safeString{} - if input.IsLighthouse { - cfg.Lighthouse = lighthouseSection{AmLighthouse: true} - } else { - var hosts []safeString - for _, lh := range input.Lighthouses { - for _, ip := range lh.NebulaIPs { - cfg.StaticHostMap[safeString(ip)] = []safeString{safeString(lh.PublicAddr)} - hosts = append(hosts, safeString(ip)) + cfg.Lighthouse = lighthouseSection{AmLighthouse: input.IsLighthouse} + // Lighthouses do not query one another, but still need static endpoints + // to establish tunnels to their peers, including for relayed traffic. + for _, lh := range input.Lighthouses { + for _, ip := range lh.NebulaIPs { + if input.IsLighthouse && slices.Contains(input.NebulaIPs, ip) { + continue + } + cfg.StaticHostMap[safeString(ip)] = []safeString{safeString(lh.PublicAddr)} + if !input.IsLighthouse { + cfg.Lighthouse.Hosts = append(cfg.Lighthouse.Hosts, safeString(ip)) } } - cfg.Lighthouse = lighthouseSection{AmLighthouse: false, Hosts: hosts} } listenHost := input.ListenHost