Dns static lookerupper (#796)

* Support lighthouse DNS names, and regularly resolve the name in a background goroutine to discover DNS updates.
This commit is contained in:
brad-defined 2023-05-09 11:22:08 -04:00 committed by GitHub
parent d1f786419c
commit bd9cc01d62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 324 additions and 48 deletions

View file

@ -2,10 +2,16 @@ package nebula
import (
"bytes"
"context"
"net"
"net/netip"
"sort"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/iputil"
"github.com/slackhq/nebula/udp"
)
@ -55,6 +61,132 @@ type cacheV6 struct {
reported []*Ip6AndPort
}
type hostnamePort struct {
name string
port uint16
}
type hostnamesResults struct {
hostnames []hostnamePort
network string
lookupTimeout time.Duration
stop chan struct{}
l *logrus.Logger
ips atomic.Pointer[map[netip.AddrPort]struct{}]
}
func NewHostnameResults(ctx context.Context, l *logrus.Logger, d time.Duration, network string, timeout time.Duration, hostPorts []string, onUpdate func()) (*hostnamesResults, error) {
r := &hostnamesResults{
hostnames: make([]hostnamePort, len(hostPorts)),
network: network,
lookupTimeout: timeout,
stop: make(chan (struct{})),
l: l,
}
// Fastrack IP addresses to ensure they're immediately available for use.
// DNS lookups for hostnames that aren't hardcoded IP's will happen in a background goroutine.
performBackgroundLookup := false
ips := map[netip.AddrPort]struct{}{}
for idx, hostPort := range hostPorts {
rIp, sPort, err := net.SplitHostPort(hostPort)
if err != nil {
return nil, err
}
iPort, err := strconv.Atoi(sPort)
if err != nil {
return nil, err
}
r.hostnames[idx] = hostnamePort{name: rIp, port: uint16(iPort)}
addr, err := netip.ParseAddr(rIp)
if err != nil {
// This address is a hostname, not an IP address
performBackgroundLookup = true
continue
}
// Save the IP address immediately
ips[netip.AddrPortFrom(addr, uint16(iPort))] = struct{}{}
}
r.ips.Store(&ips)
// Time for the DNS lookup goroutine
if performBackgroundLookup {
ticker := time.NewTicker(d)
go func() {
defer ticker.Stop()
for {
netipAddrs := map[netip.AddrPort]struct{}{}
for _, hostPort := range r.hostnames {
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, r.lookupTimeout)
addrs, err := net.DefaultResolver.LookupNetIP(timeoutCtx, r.network, hostPort.name)
timeoutCancel()
if err != nil {
l.WithFields(logrus.Fields{"hostname": hostPort.name, "network": r.network}).WithError(err).Error("DNS resolution failed for static_map host")
continue
}
for _, a := range addrs {
netipAddrs[netip.AddrPortFrom(a, hostPort.port)] = struct{}{}
}
}
origSet := r.ips.Load()
different := false
for a := range *origSet {
if _, ok := netipAddrs[a]; !ok {
different = true
break
}
}
if !different {
for a := range netipAddrs {
if _, ok := (*origSet)[a]; !ok {
different = true
break
}
}
}
if different {
l.WithFields(logrus.Fields{"origSet": origSet, "newSet": netipAddrs}).Info("DNS results changed for host list")
r.ips.Store(&netipAddrs)
onUpdate()
}
select {
case <-ctx.Done():
return
case <-r.stop:
return
case <-ticker.C:
continue
}
}
}()
}
return r, nil
}
func (hr *hostnamesResults) Cancel() {
if hr != nil {
hr.stop <- struct{}{}
}
}
func (hr *hostnamesResults) GetIPs() []netip.AddrPort {
var retSlice []netip.AddrPort
if hr != nil {
p := hr.ips.Load()
if p != nil {
for k := range *p {
retSlice = append(retSlice, k)
}
}
}
return retSlice
}
// RemoteList is a unifying concept for lighthouse servers and clients as well as hostinfos.
// It serves as a local cache of query replies, host update notifications, and locally learned addresses
type RemoteList struct {
@ -72,6 +204,9 @@ type RemoteList struct {
// For learned addresses, this is the vpnIp that sent the packet
cache map[iputil.VpnIp]*cache
hr *hostnamesResults
shouldAdd func(netip.Addr) bool
// This is a list of remotes that we have tried to handshake with and have returned from the wrong vpn ip.
// They should not be tried again during a handshake
badRemotes []*udp.Addr
@ -81,14 +216,21 @@ type RemoteList struct {
}
// NewRemoteList creates a new empty RemoteList
func NewRemoteList() *RemoteList {
func NewRemoteList(shouldAdd func(netip.Addr) bool) *RemoteList {
return &RemoteList{
addrs: make([]*udp.Addr, 0),
relays: make([]*iputil.VpnIp, 0),
cache: make(map[iputil.VpnIp]*cache),
addrs: make([]*udp.Addr, 0),
relays: make([]*iputil.VpnIp, 0),
cache: make(map[iputil.VpnIp]*cache),
shouldAdd: shouldAdd,
}
}
func (r *RemoteList) unlockedSetHostnamesResults(hr *hostnamesResults) {
// Cancel any existing hostnamesResults DNS goroutine to release resources
r.hr.Cancel()
r.hr = hr
}
// Len locks and reports the size of the deduplicated address list
// The deduplication work may need to occur here, so you must pass preferredRanges
func (r *RemoteList) Len(preferredRanges []*net.IPNet) int {
@ -437,6 +579,26 @@ func (r *RemoteList) unlockedCollect() {
}
}
dnsAddrs := r.hr.GetIPs()
for _, addr := range dnsAddrs {
if r.shouldAdd == nil || r.shouldAdd(addr.Addr()) {
switch {
case addr.Addr().Is4():
v4 := addr.Addr().As4()
addrs = append(addrs, &udp.Addr{
IP: v4[:],
Port: addr.Port(),
})
case addr.Addr().Is6():
v6 := addr.Addr().As16()
addrs = append(addrs, &udp.Addr{
IP: v6[:],
Port: addr.Port(),
})
}
}
}
r.addrs = addrs
r.relays = relays