package nebula import ( "bytes" "encoding/binary" "errors" "fmt" "net/netip" "sync" "testing" "time" "github.com/gaissmai/bart" "github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/config" "github.com/slackhq/nebula/header" "github.com/slackhq/nebula/test" "github.com/slackhq/nebula/udp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v3" ) func TestOldIPv4Only(t *testing.T) { // This test ensures our new ipv6 enabled LH protobuf IpAndPorts works with the old style to enable backwards compatibility b := []byte{8, 129, 130, 132, 80, 16, 10} var m V4AddrPort err := m.Unmarshal(b) require.NoError(t, err) ip := netip.MustParseAddr("10.1.1.1") bp := ip.As4() assert.Equal(t, binary.BigEndian.Uint32(bp[:]), m.GetAddr()) } func testCertState(networks ...netip.Prefix) *CertState { cs := &CertState{ myVpnNetworks: networks, myVpnNetworksTable: new(bart.Lite), myVpnAddrs: make([]netip.Addr, 0, len(networks)), myVpnAddrsTable: new(bart.Lite), } for _, n := range networks { cs.myVpnNetworksTable.Insert(n) cs.myVpnAddrs = append(cs.myVpnAddrs, n.Addr()) cs.myVpnAddrsTable.Insert(netip.PrefixFrom(n.Addr(), n.Addr().BitLen())) } return cs } func Test_lhStaticMapping(t *testing.T) { l := test.NewLogger() myVpnNet := netip.MustParsePrefix("10.128.0.1/16") cs := testCertState(myVpnNet) lh1 := "10.128.0.2" c := config.NewC(l) c.Settings["lighthouse"] = map[string]any{"hosts": []any{lh1}} c.Settings["static_host_map"] = map[string]any{lh1: []any{"1.1.1.1:4242"}} _, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil) require.NoError(t, err) lh2 := "10.128.0.3" c = config.NewC(l) c.Settings["lighthouse"] = map[string]any{"hosts": []any{lh1, lh2}} c.Settings["static_host_map"] = map[string]any{lh1: []any{"100.1.1.1:4242"}} _, err = NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil) require.EqualError(t, err, "lighthouse 10.128.0.3 does not have a static_host_map entry") } func TestReloadLighthouseInterval(t *testing.T) { l := test.NewLogger() myVpnNet := netip.MustParsePrefix("10.128.0.1/16") cs := testCertState(myVpnNet) lh1 := "10.128.0.2" c := config.NewC(l) c.Settings["lighthouse"] = map[string]any{ "hosts": []any{lh1}, "interval": "1s", } c.Settings["static_host_map"] = map[string]any{lh1: []any{"1.1.1.1:4242"}} lh, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil) require.NoError(t, err) lh.ifce = &mockEncWriter{} // The first one routine is kicked off by main.go currently, lets make sure that one dies require.NoError(t, c.ReloadConfigString("lighthouse:\n interval: 5").Err) assert.Equal(t, int64(5), lh.interval.Load()) // Subsequent calls are killed off by the LightHouse.Reload function require.NoError(t, c.ReloadConfigString("lighthouse:\n interval: 10").Err) assert.Equal(t, int64(10), lh.interval.Load()) // If this completes then nothing is stealing our reload routine require.NoError(t, c.ReloadConfigString("lighthouse:\n interval: 11").Err) assert.Equal(t, int64(11), lh.interval.Load()) } func BenchmarkLighthouseHandleRequest(b *testing.B) { l := test.NewLogger() myVpnNet := netip.MustParsePrefix("10.128.0.1/0") cs := testCertState(myVpnNet) c := config.NewC(l) lh, err := NewLightHouseFromConfig(b.Context(), l, c, cs, nil, nil) require.NoError(b, err) hAddr := netip.MustParseAddrPort("4.5.6.7:12345") hAddr2 := netip.MustParseAddrPort("4.5.6.7:12346") vpnIp3 := netip.MustParseAddr("0.0.0.3") lh.addrMap[vpnIp3] = NewRemoteList([]netip.Addr{vpnIp3}, nil) lh.addrMap[vpnIp3].unlockedSetV4( vpnIp3, vpnIp3, []*V4AddrPort{ netAddrToProtoV4AddrPort(hAddr.Addr(), hAddr.Port()), netAddrToProtoV4AddrPort(hAddr2.Addr(), hAddr2.Port()), }, func(netip.Addr, *V4AddrPort) bool { return true }, ) rAddr := netip.MustParseAddrPort("1.2.2.3:12345") rAddr2 := netip.MustParseAddrPort("1.2.2.3:12346") vpnIp2 := netip.MustParseAddr("0.0.0.3") lh.addrMap[vpnIp2] = NewRemoteList([]netip.Addr{vpnIp2}, nil) lh.addrMap[vpnIp2].unlockedSetV4( vpnIp3, vpnIp3, []*V4AddrPort{ netAddrToProtoV4AddrPort(rAddr.Addr(), rAddr.Port()), netAddrToProtoV4AddrPort(rAddr2.Addr(), rAddr2.Port()), }, func(netip.Addr, *V4AddrPort) bool { return true }, ) mw := &mockEncWriter{} hi := []netip.Addr{vpnIp2} b.Run("notfound", func(b *testing.B) { lhh := lh.NewRequestHandler() req := &NebulaMeta{ Type: NebulaMeta_HostQuery, Details: &NebulaMetaDetails{ OldVpnAddr: 4, V4AddrPorts: nil, }, } p, err := req.Marshal() require.NoError(b, err) for n := 0; n < b.N; n++ { lhh.HandleRequest(rAddr, hi, p, mw) } }) b.Run("found", func(b *testing.B) { lhh := lh.NewRequestHandler() req := &NebulaMeta{ Type: NebulaMeta_HostQuery, Details: &NebulaMetaDetails{ OldVpnAddr: 3, V4AddrPorts: nil, }, } p, err := req.Marshal() require.NoError(b, err) for n := 0; n < b.N; n++ { lhh.HandleRequest(rAddr, hi, p, mw) } }) } func TestLighthouse_Memory(t *testing.T) { l := test.NewLogger() myUdpAddr0 := netip.MustParseAddrPort("10.0.0.2:4242") myUdpAddr1 := netip.MustParseAddrPort("192.168.0.2:4242") myUdpAddr2 := netip.MustParseAddrPort("172.16.0.2:4242") myUdpAddr3 := netip.MustParseAddrPort("100.152.0.2:4242") myUdpAddr4 := netip.MustParseAddrPort("24.15.0.2:4242") myUdpAddr5 := netip.MustParseAddrPort("192.168.0.2:4243") myUdpAddr6 := netip.MustParseAddrPort("192.168.0.2:4244") myUdpAddr7 := netip.MustParseAddrPort("192.168.0.2:4245") myUdpAddr8 := netip.MustParseAddrPort("192.168.0.2:4246") myUdpAddr9 := netip.MustParseAddrPort("192.168.0.2:4247") myUdpAddr10 := netip.MustParseAddrPort("192.168.0.2:4248") myUdpAddr11 := netip.MustParseAddrPort("192.168.0.2:4249") myVpnIp := netip.MustParseAddr("10.128.0.2") theirUdpAddr0 := netip.MustParseAddrPort("10.0.0.3:4242") theirUdpAddr1 := netip.MustParseAddrPort("192.168.0.3:4242") theirUdpAddr2 := netip.MustParseAddrPort("172.16.0.3:4242") theirUdpAddr3 := netip.MustParseAddrPort("100.152.0.3:4242") theirUdpAddr4 := netip.MustParseAddrPort("24.15.0.3:4242") theirVpnIp := netip.MustParseAddr("10.128.0.3") c := config.NewC(l) c.Settings["lighthouse"] = map[string]any{"am_lighthouse": true} c.Settings["listen"] = map[string]any{"port": 4242} myVpnNet := netip.MustParsePrefix("10.128.0.1/24") cs := testCertState(myVpnNet) lh, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil) lh.ifce = &mockEncWriter{} require.NoError(t, err) lhh := lh.NewRequestHandler() // Test that my first update responds with just that newLHHostUpdate(myUdpAddr0, myVpnIp, []netip.AddrPort{myUdpAddr1, myUdpAddr2}, lhh) r := newLHHostRequest(myUdpAddr0, myVpnIp, myVpnIp, lhh) assertIp4InArray(t, r.msg.Details.V4AddrPorts, myUdpAddr1, myUdpAddr2) // Ensure we don't accumulate addresses newLHHostUpdate(myUdpAddr0, myVpnIp, []netip.AddrPort{myUdpAddr3}, lhh) r = newLHHostRequest(myUdpAddr0, myVpnIp, myVpnIp, lhh) assertIp4InArray(t, r.msg.Details.V4AddrPorts, myUdpAddr3) // Grow it back to 2 newLHHostUpdate(myUdpAddr0, myVpnIp, []netip.AddrPort{myUdpAddr1, myUdpAddr4}, lhh) r = newLHHostRequest(myUdpAddr0, myVpnIp, myVpnIp, lhh) assertIp4InArray(t, r.msg.Details.V4AddrPorts, myUdpAddr1, myUdpAddr4) // Update a different host and ask about it newLHHostUpdate(theirUdpAddr0, theirVpnIp, []netip.AddrPort{theirUdpAddr1, theirUdpAddr2, theirUdpAddr3, theirUdpAddr4}, lhh) r = newLHHostRequest(theirUdpAddr0, theirVpnIp, theirVpnIp, lhh) assertIp4InArray(t, r.msg.Details.V4AddrPorts, theirUdpAddr1, theirUdpAddr2, theirUdpAddr3, theirUdpAddr4) // Have both hosts ask about the other r = newLHHostRequest(theirUdpAddr0, theirVpnIp, myVpnIp, lhh) assertIp4InArray(t, r.msg.Details.V4AddrPorts, myUdpAddr1, myUdpAddr4) r = newLHHostRequest(myUdpAddr0, myVpnIp, theirVpnIp, lhh) assertIp4InArray(t, r.msg.Details.V4AddrPorts, theirUdpAddr1, theirUdpAddr2, theirUdpAddr3, theirUdpAddr4) // Make sure we didn't get changed r = newLHHostRequest(myUdpAddr0, myVpnIp, myVpnIp, lhh) assertIp4InArray(t, r.msg.Details.V4AddrPorts, myUdpAddr1, myUdpAddr4) // Ensure proper ordering and limiting // Send 12 addrs, get 10 back, the last 2 removed, allowing the duplicate to remain (clients dedupe) newLHHostUpdate( myUdpAddr0, myVpnIp, []netip.AddrPort{ myUdpAddr1, myUdpAddr2, myUdpAddr3, myUdpAddr4, myUdpAddr5, myUdpAddr5, //Duplicated on purpose myUdpAddr6, myUdpAddr7, myUdpAddr8, myUdpAddr9, myUdpAddr10, myUdpAddr11, // This should get cut }, lhh) r = newLHHostRequest(myUdpAddr0, myVpnIp, myVpnIp, lhh) assertIp4InArray( t, r.msg.Details.V4AddrPorts, myUdpAddr1, myUdpAddr2, myUdpAddr3, myUdpAddr4, myUdpAddr5, myUdpAddr5, myUdpAddr6, myUdpAddr7, myUdpAddr8, myUdpAddr9, ) // Make sure we won't add ips in our vpn network bad1 := netip.MustParseAddrPort("10.128.0.99:4242") bad2 := netip.MustParseAddrPort("10.128.0.100:4242") good := netip.MustParseAddrPort("1.128.0.99:4242") newLHHostUpdate(myUdpAddr0, myVpnIp, []netip.AddrPort{bad1, bad2, good}, lhh) r = newLHHostRequest(myUdpAddr0, myVpnIp, myVpnIp, lhh) assertIp4InArray(t, r.msg.Details.V4AddrPorts, good) } func TestLighthouse_reload(t *testing.T) { l := test.NewLogger() c := config.NewC(l) c.Settings["lighthouse"] = map[string]any{"am_lighthouse": true} c.Settings["listen"] = map[string]any{"port": 4242} myVpnNet := netip.MustParsePrefix("10.128.0.1/24") cs := testCertState(myVpnNet) lh, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil) require.NoError(t, err) nc := map[string]any{ "static_host_map": map[string]any{ "10.128.0.2": []any{"1.1.1.1:4242"}, }, } rc, err := yaml.Marshal(nc) require.NoError(t, err) c.ReloadConfigString(string(rc)) err = lh.reload(c, false, nil) require.NoError(t, err) } // TestLighthouse_reloadStaticHostMap verifies that reloading static_host_map applies the new // config rather than appending to it. See issue #718. func TestLighthouse_reloadStaticHostMap(t *testing.T) { l := test.NewLogger() c := config.NewC(l) c.Settings["lighthouse"] = map[string]any{"am_lighthouse": true} c.Settings["listen"] = map[string]any{"port": 4242} c.Settings["static_host_map"] = map[string]any{ "10.128.0.2": []any{"1.1.1.1:4242"}, } myVpnNet := netip.MustParsePrefix("10.128.0.1/24") cs := testCertState(myVpnNet) lh, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil) require.NoError(t, err) staticHost := netip.MustParseAddr("10.128.0.2") otherHost := netip.MustParseAddr("10.128.0.3") // Capture the RemoteList pointer up front; an in-flight handshake would hold the same one // on hostinfo.remotes, so it must reflect every reload below. pinned := lh.Query(staticHost) require.NotNil(t, pinned) assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("1.1.1.1:4242")}, pinned.CopyAddrs([]netip.Prefix{})) // Replace the remote address. The new address should be the only entry. nc := map[string]any{ "static_host_map": map[string]any{ "10.128.0.2": []any{"2.2.2.2:4242"}, }, } rc, err := yaml.Marshal(nc) require.NoError(t, err) require.NoError(t, c.ReloadConfigString(string(rc)).Err) rl := lh.Query(staticHost) require.NotNil(t, rl) assert.Same(t, pinned, rl, "RemoteList pointer must stay stable so in-flight handshakes pick up the change") assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("2.2.2.2:4242")}, rl.CopyAddrs([]netip.Prefix{})) // Reload back to the original IP. Mirrors the round-trip in issue #718 step 6-8 where // the buggy reload produced [1.1.1.1, 2.2.2.2, 1.1.1.1] instead of [1.1.1.1]. nc = map[string]any{ "static_host_map": map[string]any{ "10.128.0.2": []any{"1.1.1.1:4242"}, }, } rc, err = yaml.Marshal(nc) require.NoError(t, err) require.NoError(t, c.ReloadConfigString(string(rc)).Err) rl = lh.Query(staticHost) require.NotNil(t, rl) assert.Same(t, pinned, rl) assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("1.1.1.1:4242")}, rl.CopyAddrs([]netip.Prefix{})) // Reload with the same config. An unchanged entry must not duplicate. require.NoError(t, c.ReloadConfigString(string(rc)).Err) rl = lh.Query(staticHost) require.NotNil(t, rl) assert.Same(t, pinned, rl) assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("1.1.1.1:4242")}, rl.CopyAddrs([]netip.Prefix{})) // Switch back to 2.2.2.2 so the rest of the test continues against a known address. nc = map[string]any{ "static_host_map": map[string]any{ "10.128.0.2": []any{"2.2.2.2:4242"}, }, } rc, err = yaml.Marshal(nc) require.NoError(t, err) require.NoError(t, c.ReloadConfigString(string(rc)).Err) // Add a second host alongside the first. Both should be present, neither duplicated. nc = map[string]any{ "static_host_map": map[string]any{ "10.128.0.2": []any{"2.2.2.2:4242"}, "10.128.0.3": []any{"3.3.3.3:4242"}, }, } rc, err = yaml.Marshal(nc) require.NoError(t, err) require.NoError(t, c.ReloadConfigString(string(rc)).Err) rl = lh.Query(staticHost) require.NotNil(t, rl) assert.Same(t, pinned, rl, "adding a sibling entry must not displace the existing RemoteList") assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("2.2.2.2:4242")}, rl.CopyAddrs([]netip.Prefix{})) rl = lh.Query(otherHost) require.NotNil(t, rl) assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("3.3.3.3:4242")}, rl.CopyAddrs([]netip.Prefix{})) // Drop the first host entirely. The vpnAddr is no longer marked static, our owner // contribution is cleared, but the addrMap entry stays in place so non-static cache // data (from lighthouse queries) on the same RemoteList isn't lost. In-flight handshakes // that already had the pointer see an empty address list rather than retrying stale ones. nc = map[string]any{ "static_host_map": map[string]any{ "10.128.0.3": []any{"3.3.3.3:4242"}, }, } rc, err = yaml.Marshal(nc) require.NoError(t, err) require.NoError(t, c.ReloadConfigString(string(rc)).Err) _, isStatic := lh.GetStaticHostList()[staticHost] assert.False(t, isStatic) rl = lh.Query(staticHost) require.NotNil(t, rl) assert.Same(t, pinned, rl) assert.Empty(t, rl.CopyAddrs([]netip.Prefix{})) rl = lh.Query(otherHost) require.NotNil(t, rl) assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("3.3.3.3:4242")}, rl.CopyAddrs([]netip.Prefix{})) } // sendLHHostRequest delivers a HostQuery to lhh and hands back the writer that // captured what it emitted. Pass a nil filter to see every message. func sendLHHostRequest(fromAddr netip.AddrPort, myVpnIp, queryVpnIp netip.Addr, lhh *LightHouseHandler, filter *NebulaMeta_MessageType) *testEncWriter { req := &NebulaMeta{ Type: NebulaMeta_HostQuery, Details: &NebulaMetaDetails{}, } if queryVpnIp.Is4() { bip := queryVpnIp.As4() req.Details.OldVpnAddr = binary.BigEndian.Uint32(bip[:]) } else { req.Details.VpnAddr = netAddrToProtoAddr(queryVpnIp) } b, err := req.Marshal() if err != nil { panic(err) } w := &testEncWriter{metaFilter: filter} lhh.HandleRequest(fromAddr, []netip.Addr{myVpnIp}, b, w) return w } func newLHHostRequest(fromAddr netip.AddrPort, myVpnIp, queryVpnIp netip.Addr, lhh *LightHouseHandler) testLhReply { filter := NebulaMeta_HostQueryReply return sendLHHostRequest(fromAddr, myVpnIp, queryVpnIp, lhh, &filter).lastReply } func TestLighthouse_IgnoresHostQueryForItself(t *testing.T) { // Validate that we don't answer host queries for our own address. l := test.NewLogger() myVpnNet := netip.MustParsePrefix("10.128.0.1/24") myVpnIp := myVpnNet.Addr() c := config.NewC(l) c.Settings["lighthouse"] = map[string]any{"am_lighthouse": true} c.Settings["listen"] = map[string]any{"port": 4242} // Add a static_host_map entry for ourselves, so our address // is in the addrMap. c.Settings["static_host_map"] = map[string]any{ myVpnIp.String(): []any{"192.168.100.1:4242"}, } lh, err := NewLightHouseFromConfig(t.Context(), l, c, testCertState(myVpnNet), nil, nil) require.NoError(t, err) lh.ifce = &mockEncWriter{} lhh := lh.NewRequestHandler() peerVpnIp := netip.MustParseAddr("10.128.0.2") peerUdpAddr := netip.MustParseAddrPort("10.0.0.2:4242") otherVpnIp := netip.MustParseAddr("10.128.0.3") otherUdpAddr := netip.MustParseAddrPort("10.0.0.3:4242") newLHHostUpdate(peerUdpAddr, peerVpnIp, []netip.AddrPort{peerUdpAddr}, lhh) newLHHostUpdate(otherUdpAddr, otherVpnIp, []netip.AddrPort{otherUdpAddr}, lhh) // Control: a query about a real peer is still answered, and still ends with // the punch notification aimed at the host that was asked about. w := sendLHHostRequest(peerUdpAddr, peerVpnIp, otherVpnIp, lhh, nil) require.NotNil(t, w.lastReply.msg) assert.Equal(t, NebulaMeta_HostPunchNotification, w.lastReply.msg.Type) assert.Equal(t, otherVpnIp, w.lastReply.vpnIp) // Now validate that we don't send to ourselves. found, _, err := lh.queryAndPrepMessage(myVpnIp, func(*cache) (int, error) { return 0, nil }) require.NoError(t, err) require.True(t, found, "the lighthouse should hold a cache entry for its own address") w = sendLHHostRequest(peerUdpAddr, peerVpnIp, myVpnIp, lhh, nil) assert.Nil(t, w.lastReply.msg, "a query about our own address must produce no reply and no punch notification") } func newLHHostUpdate(fromAddr netip.AddrPort, vpnIp netip.Addr, addrs []netip.AddrPort, lhh *LightHouseHandler) { req := &NebulaMeta{ Type: NebulaMeta_HostUpdateNotification, Details: &NebulaMetaDetails{}, } if vpnIp.Is4() { bip := vpnIp.As4() req.Details.OldVpnAddr = binary.BigEndian.Uint32(bip[:]) } else { req.Details.VpnAddr = netAddrToProtoAddr(vpnIp) } for _, v := range addrs { if v.Addr().Is4() { req.Details.V4AddrPorts = append(req.Details.V4AddrPorts, netAddrToProtoV4AddrPort(v.Addr(), v.Port())) } else { req.Details.V6AddrPorts = append(req.Details.V6AddrPorts, netAddrToProtoV6AddrPort(v.Addr(), v.Port())) } } b, err := req.Marshal() if err != nil { panic(err) } w := &testEncWriter{} lhh.HandleRequest(fromAddr, []netip.Addr{vpnIp}, b, w) } type testLhReply struct { nebType header.MessageType nebSubType header.MessageSubType vpnIp netip.Addr msg *NebulaMeta } type testEncWriter struct { lastReply testLhReply metaFilter *NebulaMeta_MessageType protocolVersion cert.Version } func (tw *testEncWriter) SendVia(via *HostInfo, relay *Relay, ad, nb, out []byte, nocopy bool, q int) { } func (tw *testEncWriter) Handshake(vpnIp netip.Addr) { } func (tw *testEncWriter) SendMessageToHostInfo(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p, _, _ []byte) { msg := &NebulaMeta{} err := msg.Unmarshal(p) if tw.metaFilter == nil || msg.Type == *tw.metaFilter { tw.lastReply = testLhReply{ nebType: t, nebSubType: st, vpnIp: hostinfo.vpnAddrs[0], msg: msg, } } if err != nil { panic(err) } } func (tw *testEncWriter) SendMessageToVpnAddr(t header.MessageType, st header.MessageSubType, vpnIp netip.Addr, p, _, _ []byte) { msg := &NebulaMeta{} err := msg.Unmarshal(p) if tw.metaFilter == nil || msg.Type == *tw.metaFilter { tw.lastReply = testLhReply{ nebType: t, nebSubType: st, vpnIp: vpnIp, msg: msg, } } if err != nil { panic(err) } } func (tw *testEncWriter) GetHostInfo(vpnIp netip.Addr) *HostInfo { return nil } func (tw *testEncWriter) GetCertState() *CertState { return &CertState{initiatingVersion: tw.protocolVersion} } // assertIp4InArray asserts every address in want is at the same position in have and that the lengths match func assertIp4InArray(t *testing.T, have []*V4AddrPort, want ...netip.AddrPort) { if !assert.Len(t, have, len(want)) { return } for k, w := range want { h := protoV4AddrPortToNetAddrPort(have[k]) if !(h == w) { assert.Fail(t, fmt.Sprintf("Response did not contain: %v at %v, found %v", w, k, h)) } } } func Test_findNetworkUnion(t *testing.T) { var out netip.Addr var ok bool tenDot := netip.MustParsePrefix("10.0.0.0/8") oneSevenTwo := netip.MustParsePrefix("172.16.0.0/16") fe80 := netip.MustParsePrefix("fe80::/8") fc00 := netip.MustParsePrefix("fc00::/7") a1 := netip.MustParseAddr("10.0.0.1") afe81 := netip.MustParseAddr("fe80::1") //simple out, ok = findNetworkUnion([]netip.Prefix{tenDot}, []netip.Addr{a1}) assert.True(t, ok) assert.Equal(t, out, a1) //mixed lengths out, ok = findNetworkUnion([]netip.Prefix{tenDot}, []netip.Addr{a1, afe81}) assert.True(t, ok) assert.Equal(t, out, a1) out, ok = findNetworkUnion([]netip.Prefix{tenDot, oneSevenTwo}, []netip.Addr{a1}) assert.True(t, ok) assert.Equal(t, out, a1) //mixed family out, ok = findNetworkUnion([]netip.Prefix{tenDot, oneSevenTwo, fe80}, []netip.Addr{a1}) assert.True(t, ok) assert.Equal(t, out, a1) out, ok = findNetworkUnion([]netip.Prefix{tenDot, oneSevenTwo, fe80}, []netip.Addr{a1, afe81}) assert.True(t, ok) assert.Equal(t, out, a1) //ordering out, ok = findNetworkUnion([]netip.Prefix{tenDot, oneSevenTwo, fe80}, []netip.Addr{afe81, a1}) assert.True(t, ok) assert.Equal(t, out, a1) out, ok = findNetworkUnion([]netip.Prefix{fe80, tenDot, oneSevenTwo}, []netip.Addr{afe81, a1}) assert.True(t, ok) assert.Equal(t, out, afe81) //some mismatches out, ok = findNetworkUnion([]netip.Prefix{tenDot, oneSevenTwo, fe80}, []netip.Addr{afe81}) assert.True(t, ok) assert.Equal(t, out, afe81) out, ok = findNetworkUnion([]netip.Prefix{oneSevenTwo, fe80}, []netip.Addr{a1, afe81}) assert.True(t, ok) assert.Equal(t, out, afe81) //falsey cases out, ok = findNetworkUnion([]netip.Prefix{oneSevenTwo, fe80}, []netip.Addr{a1}) assert.False(t, ok) out, ok = findNetworkUnion([]netip.Prefix{fc00, fe80}, []netip.Addr{a1}) assert.False(t, ok) out, ok = findNetworkUnion([]netip.Prefix{oneSevenTwo, fc00}, []netip.Addr{a1, afe81}) assert.False(t, ok) out, ok = findNetworkUnion([]netip.Prefix{fc00}, []netip.Addr{a1, afe81}) assert.False(t, ok) } func TestLighthouse_Dont_Delete_Static_Hosts(t *testing.T) { l := test.NewLogger() myUdpAddr2 := netip.MustParseAddrPort("1.2.3.4:4242") testSameHostNotStatic := netip.MustParseAddr("10.128.0.41") testStaticHost := netip.MustParseAddr("10.128.0.42") //myVpnIp := netip.MustParseAddr("10.128.0.2") c := config.NewC(l) lh1 := "10.128.0.2" c.Settings["lighthouse"] = map[string]any{ "hosts": []any{lh1}, "interval": "1s", } c.Settings["listen"] = map[string]any{"port": 4242} c.Settings["static_host_map"] = map[string]any{ lh1: []any{"1.1.1.1:4242"}, "10.128.0.42": []any{"1.2.3.4:4242"}, } myVpnNet := netip.MustParsePrefix("10.128.0.1/24") cs := testCertState(myVpnNet) lh, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil) require.NoError(t, err) lh.ifce = &mockEncWriter{} //test that we actually have the static entry: out := lh.Query(testStaticHost) assert.NotNil(t, out) assert.Equal(t, out.vpnAddrs[0], testStaticHost) out.Rebuild([]netip.Prefix{}) //why tho assert.Equal(t, out.addrs[0], myUdpAddr2) //bolt on a lower numbered primary IP am := lh.unlockedGetRemoteList([]netip.Addr{testStaticHost}) am.vpnAddrs = []netip.Addr{testSameHostNotStatic, testStaticHost} lh.addrMap[testSameHostNotStatic] = am out.Rebuild([]netip.Prefix{}) //??? //test that we actually have the static entry: out = lh.Query(testStaticHost) assert.NotNil(t, out) assert.Equal(t, out.vpnAddrs[0], testSameHostNotStatic) assert.Equal(t, out.vpnAddrs[1], testStaticHost) assert.Equal(t, out.addrs[0], myUdpAddr2) //test that we actually have the static entry for BOTH: out2 := lh.Query(testSameHostNotStatic) assert.Same(t, out2, out) //now do the delete lh.DeleteVpnAddrs([]netip.Addr{testSameHostNotStatic, testStaticHost}) //verify out = lh.Query(testSameHostNotStatic) assert.NotNil(t, out) if out == nil { t.Fatal("expected non-nil query for the static host") } assert.Equal(t, out.vpnAddrs[0], testSameHostNotStatic) assert.Equal(t, out.vpnAddrs[1], testStaticHost) assert.Equal(t, out.addrs[0], myUdpAddr2) } func TestLighthouse_DeletesWork(t *testing.T) { l := test.NewLogger() myUdpAddr2 := netip.MustParseAddrPort("1.2.3.4:4242") testHost := netip.MustParseAddr("10.128.0.42") c := config.NewC(l) lh1 := "10.128.0.2" c.Settings["lighthouse"] = map[string]any{ "hosts": []any{lh1}, "interval": "1s", } c.Settings["listen"] = map[string]any{"port": 4242} c.Settings["static_host_map"] = map[string]any{ lh1: []any{"1.1.1.1:4242"}, } myVpnNet := netip.MustParsePrefix("10.128.0.1/24") cs := testCertState(myVpnNet) lh, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil) require.NoError(t, err) lh.ifce = &mockEncWriter{} //insert the host am := lh.unlockedGetRemoteList([]netip.Addr{testHost}) am.vpnAddrs = []netip.Addr{testHost} am.addrs = []netip.AddrPort{myUdpAddr2} lh.addrMap[testHost] = am am.Rebuild([]netip.Prefix{}) //??? //test that we actually have the entry: out := lh.Query(testHost) assert.NotNil(t, out) assert.Equal(t, out.vpnAddrs[0], testHost) out.Rebuild([]netip.Prefix{}) //why tho assert.Equal(t, out.addrs[0], myUdpAddr2) //now do the delete lh.DeleteVpnAddrs([]netip.Addr{testHost}) //verify out = lh.Query(testHost) assert.Nil(t, out) } func TestLightHouse_logLocalAddrsErr(t *testing.T) { out := &bytes.Buffer{} lh := &LightHouse{l: test.NewLoggerWithOutput(out)} // The first sighting of a failure warns. lh.logLocalAddrsErr(errors.New("permission denied")) assert.Contains(t, out.String(), "level=WARN") assert.Contains(t, out.String(), "permission denied") // Repeating unchanged does not warn again, which is what keeps a permanent failure from warning // on every lighthouse.interval for the life of the process. out.Reset() lh.logLocalAddrsErr(errors.New("permission denied")) assert.NotContains(t, out.String(), "level=WARN") // A different failure is a new event and warns. out.Reset() lh.logLocalAddrsErr(errors.New("something else")) assert.Contains(t, out.String(), "level=WARN") assert.Contains(t, out.String(), "something else") // Recovering resets, so the same failure returning later warns again. out.Reset() lh.logLocalAddrsErr(nil) assert.Empty(t, out.String()) lh.logLocalAddrsErr(errors.New("something else")) assert.Contains(t, out.String(), "level=WARN") } // heldEncWriter holds the query worker in its first GetHostInfo until release closes. type heldEncWriter struct { mockEncWriter once sync.Once held chan struct{} release chan struct{} } func (w *heldEncWriter) GetHostInfo(netip.Addr) *HostInfo { w.once.Do(func() { close(w.held) }) <-w.release return nil } // QueryServer never waits on the query worker, since the packet readers call it and nothing drains the channel at // shutdown; a query that finds the channel full is dropped and counted. The worker is held on its first query so the // channel fills. func TestLighthouse_QueryServerDoesNotBlock(t *testing.T) { l := test.NewLogger() cs := testCertState(netip.MustParsePrefix("10.128.0.1/16")) lh1 := "10.128.0.2" c := config.NewC(l) c.Settings["lighthouse"] = map[string]any{"hosts": []any{lh1}} c.Settings["static_host_map"] = map[string]any{lh1: []any{"1.1.1.1:4242"}} c.Settings["handshakes"] = map[string]any{"query_buffer": 1} lh, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil) require.NoError(t, err) w := &heldEncWriter{held: make(chan struct{}), release: make(chan struct{})} lh.ifce = w t.Cleanup(func() { close(w.release) }) lh.QueryServer(netip.MustParseAddr("10.128.0.10")) select { case <-w.held: case <-time.After(5 * time.Second): t.Fatal("the query worker never took the first query") } dropped := lh.queryDropped.Count() done := make(chan struct{}) go func() { lh.QueryServer(netip.MustParseAddr("10.128.0.11")) lh.QueryServer(netip.MustParseAddr("10.128.0.12")) close(done) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("QueryServer blocked on a full query channel") } assert.EqualValues(t, 1, lh.queryDropped.Count()-dropped) } // hostmapEncWriter answers GetHostInfo from a real hostmap, the way Interface does. type hostmapEncWriter struct { mockEncWriter hm *HostMap } func (w *hostmapEncWriter) GetHostInfo(a netip.Addr) *HostInfo { return w.hm.QueryVpnAddr(a) } func (w *hostmapEncWriter) GetCertState() *CertState { return &CertState{initiatingVersion: cert.Version1} } // A lighthouse runs four paths that each hold one lock while waiting for the next: CheckAndComplete // holds the hostmap and takes the handshake manager, StartHandshake holds the handshake manager and // takes the lighthouse, a host update holds the lighthouse and takes the sender's RemoteList, and a // punch notification holds the querier's RemoteList while it resolves the target. If that last step // reaches the hostmap under the RemoteList lock, the four deadlock. Drive them all at once. func TestLighthouse_PunchNotificationDoesNotCloseLockCycle(t *testing.T) { l := test.NewLogger() myVpnNet := netip.MustParsePrefix("10.128.0.1/24") c := config.NewC(l) c.Settings["lighthouse"] = map[string]any{"am_lighthouse": true} c.Settings["listen"] = map[string]any{"port": 4242} lh, err := NewLightHouseFromConfig(t.Context(), l, c, testCertState(myVpnNet), nil, nil) require.NoError(t, err) mainHM := newHostMap(l) pr := []netip.Prefix{} mainHM.preferredRanges.Store(&pr) lh.ifce = &hostmapEncWriter{hm: mainHM} hm := NewHandshakeManager(l, mainHM, lh, &udp.NoopConn{}, defaultHandshakeConfig) f := &Interface{hostMap: mainHM, lightHouse: lh, handshakeManager: hm, l: l} hm.f = f peer := netip.MustParseAddr("10.128.0.2") peerUdp := netip.MustParseAddrPort("10.0.0.2:4242") target := netip.MustParseAddr("10.128.0.3") targetUdp := netip.MustParseAddrPort("10.0.0.3:4242") outbound := netip.MustParseAddr("10.128.0.4") inbound := netip.MustParseAddr("10.128.0.5") seed := lh.NewRequestHandler() newLHHostUpdate(peerUdp, peer, []netip.AddrPort{peerUdp}, seed) newLHHostUpdate(targetUdp, target, []netip.AddrPort{targetUdp}, seed) stop := make(chan struct{}) var wg sync.WaitGroup loop := func(body func()) { defer wg.Done() for { select { case <-stop: return default: body() } } } wg.Add(4) // Outbound handshakes keep starting and being dropped. go loop(func() { hm.DeleteHostInfo(hm.StartHandshake(outbound, nil)) }) // Inbound handshakes keep completing and being torn down. var idx uint32 go loop(func() { idx++ hi := &HostInfo{ vpnAddrs: []netip.Addr{inbound}, localIndexId: idx, remoteIndexId: idx, remotes: lh.QueryCache([]netip.Addr{inbound}), HandshakePacket: map[uint8][]byte{handshakePacketStage0: {byte(idx)}}, } if _, err := hm.CheckAndComplete(hi, handshakePacketStage0, f); err == nil { mainHM.DeleteHostInfo(hi) } }) // The peer keeps sending host updates. updates := lh.NewRequestHandler() go loop(func() { newLHHostUpdate(peerUdp, peer, []netip.AddrPort{peerUdp}, updates) }) // The same peer keeps querying for the target, which ends in a punch notification. queries := lh.NewRequestHandler() go loop(func() { sendLHHostRequest(peerUdp, peer, target, queries, nil) }) time.Sleep(time.Second) close(stop) done := make(chan struct{}) go func() { wg.Wait(); close(done) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("deadlock: the lighthouse paths never finished after stop was signalled") } } func TestLighthouse_reloadRelaysTriggersUpdate(t *testing.T) { l := test.NewLogger() c := config.NewC(l) base := "lighthouse:\n am_lighthouse: true\nlisten:\n port: 4242\n" require.NoError(t, c.LoadString(base+"relay:\n relays: [10.128.0.5]\n")) lh, err := NewLightHouseFromConfig(t.Context(), l, c, testCertState(netip.MustParsePrefix("10.128.0.1/24")), nil, nil) require.NoError(t, err) assert.Empty(t, lh.updateTrigger) require.NoError(t, c.ReloadConfigString(base+"relay:\n relays: [10.128.0.6]\n").Err) assert.Equal(t, []netip.Addr{netip.MustParseAddr("10.128.0.6")}, lh.GetRelaysForMe()) require.Len(t, lh.updateTrigger, 1) <-lh.updateTrigger // The config changed but what we advertise did not, a bad entry is skipped require.NoError(t, c.ReloadConfigString(base+"relay:\n relays: [10.128.0.6, nope]\n").Err) assert.Equal(t, []netip.Addr{netip.MustParseAddr("10.128.0.6")}, lh.GetRelaysForMe()) assert.Empty(t, lh.updateTrigger) } func TestLighthouse_reloadAmRelayAlone(t *testing.T) { l := test.NewLogger() c := config.NewC(l) base := "lighthouse:\n am_lighthouse: true\nlisten:\n port: 4242\nrelay:\n relays: [10.128.0.5]\n" require.NoError(t, c.LoadString(base+" am_relay: false\n")) lh, err := NewLightHouseFromConfig(t.Context(), l, c, testCertState(netip.MustParsePrefix("10.128.0.1/24")), nil, nil) require.NoError(t, err) assert.Equal(t, []netip.Addr{netip.MustParseAddr("10.128.0.5")}, lh.GetRelaysForMe()) // Relays aren't allowed to have relays, flipping am_relay alone must drop them and tell the lighthouses require.NoError(t, c.ReloadConfigString(base+" am_relay: true\n").Err) assert.Empty(t, lh.GetRelaysForMe()) require.Len(t, lh.updateTrigger, 1) <-lh.updateTrigger require.NoError(t, c.ReloadConfigString(base+" am_relay: false\n").Err) assert.Equal(t, []netip.Addr{netip.MustParseAddr("10.128.0.5")}, lh.GetRelaysForMe()) require.Len(t, lh.updateTrigger, 1) } func TestLighthouse_reloadRestartRequired(t *testing.T) { l := test.NewLogger() c := config.NewC(l) base := "lighthouse:\n am_lighthouse: true\n" require.NoError(t, c.LoadString(base+"listen:\n port: 4242\n")) _, err := NewLightHouseFromConfig(t.Context(), l, c, testCertState(netip.MustParsePrefix("10.128.0.1/24")), nil, nil) require.NoError(t, err) res := c.ReloadConfigString(base + "listen:\n port: 4242\n") require.NoError(t, res.Err) assert.False(t, res.RestartRequired) res = c.ReloadConfigString(base + "listen:\n port: 4243\n") require.NoError(t, res.Err) assert.True(t, res.RestartRequired) } // A relay that doesn't parse is skipped and the rest are used, the reload says one was skipped func TestLighthouse_reloadBadRelayFails(t *testing.T) { l := test.NewLogger() c := config.NewC(l) require.NoError(t, c.LoadString("listen:\n port: 4242\n")) lh, err := NewLightHouseFromConfig(t.Context(), l, c, testCertState(netip.MustParsePrefix("10.128.0.1/24")), nil, nil) require.NoError(t, err) res := c.ReloadConfigString("listen:\n port: 4242\nrelay:\n relays: [10.128.0.2, nope]\n") require.NoError(t, res.Err) assert.True(t, res.Failed) assert.Equal(t, []netip.Addr{netip.MustParseAddr("10.128.0.2")}, lh.GetRelaysForMe()) } // An advertise_addrs entry inside our own network is skipped and the rest are used, the reload says one was skipped func TestLighthouse_reloadAdvertiseAddrInNetworkFails(t *testing.T) { l := test.NewLogger() c := config.NewC(l) require.NoError(t, c.LoadString("listen:\n port: 4242\n")) lh, err := NewLightHouseFromConfig(t.Context(), l, c, testCertState(netip.MustParsePrefix("10.128.0.1/24")), nil, nil) require.NoError(t, err) res := c.ReloadConfigString("listen:\n port: 4242\nlighthouse:\n advertise_addrs: [\"1.2.3.4:4242\", \"10.128.0.5:4242\"]\n") require.NoError(t, res.Err) assert.True(t, res.Failed) assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("1.2.3.4:4242")}, lh.GetAdvertiseAddrs()) }