| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- package nebula
- import (
- "context"
- "fmt"
- "log/slog"
- "net"
- "net/netip"
- "strconv"
- "strings"
- "sync"
- "sync/atomic"
- "github.com/miekg/dns"
- "github.com/slackhq/nebula/config"
- )
- type dnsServer struct {
- sync.RWMutex
- l *slog.Logger
- ctx context.Context
- dnsMap4 map[string]netip.Addr
- dnsMap6 map[string]netip.Addr
- hostMap *HostMap
- pki *PKI
- // selfHost is the cached FQDN we last seeded for ourselves
- selfHost string
- mux *dns.ServeMux
- // enabled mirrors `lighthouse.serve_dns && lighthouse.am_lighthouse`.
- // Start, Add, and reload consult it so callers don't need to know the
- // gating rules. When it toggles off via reload, accumulated records are
- // cleared so a later re-enable starts with a fresh map populated from
- // new handshakes.
- enabled atomic.Bool
- serverMu sync.Mutex
- server *dns.Server
- // started is closed once `server` has finished binding (or after
- // ListenAndServe returns on a bind failure). Stop waits on it before
- // calling Shutdown to avoid the miekg/dns "server not started" race
- // where a Shutdown that arrives before bind completes is silently
- // ignored, leaving the listener running forever.
- started chan struct{}
- addr string
- }
- // newDnsServerFromConfig builds a dnsServer, applies the initial config, and
- // registers a reload callback. The reload callback is registered before the
- // initial config is applied, so a SIGHUP can later enable, fix, or disable
- // DNS even if the initial application failed.
- //
- // The dnsServer internally gates on `lighthouse.serve_dns &&
- // lighthouse.am_lighthouse`. Start and Add are safe to call unconditionally,
- // they no-op when DNS isn't enabled. Each Start invocation owns a ctx-cancel
- // watcher that tears the listener down on nebula shutdown. The returned
- // pointer is always non-nil, even on error.
- func newDnsServerFromConfig(ctx context.Context, l *slog.Logger, pki *PKI, hostMap *HostMap, c *config.C) (*dnsServer, error) {
- ds := &dnsServer{
- l: l,
- ctx: ctx,
- dnsMap4: make(map[string]netip.Addr),
- dnsMap6: make(map[string]netip.Addr),
- hostMap: hostMap,
- pki: pki,
- }
- ds.mux = dns.NewServeMux()
- ds.mux.HandleFunc(".", ds.handleDnsRequest)
- c.RegisterReloadCallback(func(c *config.C, r *config.Reload) {
- if err := ds.reload(c, false); err != nil {
- ds.l.Error("Failed to reload DNS responder from config", "error", err)
- r.Failed()
- }
- })
- if err := ds.reload(c, true); err != nil {
- return ds, err
- }
- ds.seedSelf()
- return ds, nil
- }
- // reload applies the latest config and reconciles the running state with it:
- // - enabled toggled on -> spawn a runner
- // - enabled toggled off -> stop the runner
- // - listen address changed (while running) -> restart on the new address
- // - everything else -> no-op
- //
- // On the initial call it only records configuration; Control.Start is what
- // launches the first runner via dnsStart.
- func (d *dnsServer) reload(c *config.C, initial bool) error {
- wantsDns := c.GetBool("lighthouse.serve_dns", false)
- amLighthouse := c.GetBool("lighthouse.am_lighthouse", false)
- enabled := wantsDns && amLighthouse
- newAddr := getDnsServerAddr(c)
- d.serverMu.Lock()
- running := d.server != nil
- sameAddr := d.addr == newAddr
- d.addr = newAddr
- d.enabled.Store(enabled)
- d.serverMu.Unlock()
- if initial {
- if wantsDns && !amLighthouse {
- d.l.Warn("DNS server refusing to run because this host is not a lighthouse.")
- }
- return nil
- }
- if !enabled {
- if running {
- d.Stop()
- }
- // Drop any records that accumulated while enabled; a later re-enable
- // will repopulate from fresh handshakes and a fresh seedSelf.
- d.clearRecords()
- return nil
- }
- if !running {
- // Was disabled (or never started); bring it up now.
- go d.Start()
- } else if !sameAddr {
- // Stop clears the slot before shutting down, otherwise the Start below can find the dying server and refuse
- d.Stop()
- go d.Start()
- }
- // Refresh the self entry every enabled reload so cert renewals that change our name or VPN addresses are picked up.
- d.seedSelf()
- return nil
- }
- // shutdownServer waits for the server to finish binding (so Shutdown actually
- // stops it rather than no-oping) and then shuts it down.
- func (d *dnsServer) shutdownServer(srv *dns.Server, started chan struct{}, reason string) {
- if srv == nil {
- return
- }
- if started != nil {
- <-started
- }
- if err := srv.Shutdown(); err != nil {
- d.l.Warn("Failed to shut down the DNS responder", "reason", reason, "error", err)
- }
- }
- // Start binds and serves the DNS responder. Blocks until Stop is called or
- // the listener errors. Safe to call when DNS is disabled (returns
- // immediately). This is what Control.dnsStart points at.
- //
- // Must be invoked after the tun device is active so that lighthouse.dns.host
- // may bind to a nebula IP.
- func (d *dnsServer) Start() {
- if !d.enabled.Load() {
- return
- }
- started := make(chan struct{})
- d.serverMu.Lock()
- // Re-check enabled under the lock, a disable that raced our check above snapshots the slot under it too.
- // Two reloads in quick succession can both spawn a Start, the loser would orphan the live listener past Stop
- if d.ctx.Err() != nil || d.server != nil || !d.enabled.Load() {
- d.serverMu.Unlock()
- return
- }
- addr := d.addr
- server := &dns.Server{
- Addr: addr,
- Net: "udp",
- Handler: d.mux,
- NotifyStartedFunc: func() { close(started) },
- }
- d.server = server
- d.started = started
- d.serverMu.Unlock()
- // Per-invocation ctx watcher. Exits when Start does, so we don't leak a
- // watcher per reload-driven restart.
- done := make(chan struct{})
- go func() {
- select {
- case <-d.ctx.Done():
- d.shutdownServer(server, started, "shutdown")
- case <-done:
- }
- }()
- d.l.Info("Starting DNS responder", "dnsListener", addr)
- err := server.ListenAndServe()
- close(done)
- // If the listener never bound (bind error) NotifyStartedFunc never fires,
- // so close started here to release any Stop caller waiting on it.
- select {
- case <-started:
- default:
- close(started)
- }
- // Release our slot, unless a reload already replaced us, so a dead listener can't block a future Start
- d.serverMu.Lock()
- if d.server == server {
- d.server = nil
- d.started = nil
- }
- d.serverMu.Unlock()
- if err != nil {
- d.l.Warn("Failed to run the DNS responder", "error", err)
- }
- }
- // Stop shuts down the active server, if any. Idempotent.
- func (d *dnsServer) Stop() {
- d.serverMu.Lock()
- srv := d.server
- started := d.started
- d.server = nil
- d.started = nil
- d.serverMu.Unlock()
- d.shutdownServer(srv, started, "stop")
- }
- // Query returns the address for the given name and query type. The second
- // return value reports whether the name is known at all (in either A or AAAA),
- // which lets callers distinguish NODATA from NXDOMAIN.
- func (d *dnsServer) Query(q uint16, data string) (netip.Addr, bool) {
- data = strings.ToLower(data)
- d.RLock()
- defer d.RUnlock()
- addr4, haveV4 := d.dnsMap4[data]
- addr6, haveV6 := d.dnsMap6[data]
- nameExists := haveV4 || haveV6
- switch q {
- case dns.TypeA:
- if haveV4 {
- return addr4, nameExists
- }
- case dns.TypeAAAA:
- if haveV6 {
- return addr6, nameExists
- }
- }
- return netip.Addr{}, nameExists
- }
- func (d *dnsServer) QueryCert(data string) string {
- if len(data) < 2 {
- return ""
- }
- ip, err := netip.ParseAddr(data[:len(data)-1])
- if err != nil {
- return ""
- }
- // The hostmap only ever contains peers we have handshaked with, so it never carries an entry for ourselves.
- // Answer self lookups straight from the local cert state.
- if cs := d.certState(); cs != nil && cs.myVpnAddrsTable != nil && cs.myVpnAddrsTable.Contains(ip) {
- c := cs.GetDefaultCertificate()
- if c == nil {
- return ""
- }
- b, err := c.MarshalJSON()
- if err != nil {
- return ""
- }
- return string(b)
- }
- hostinfo := d.hostMap.QueryVpnAddr(ip)
- if hostinfo == nil {
- return ""
- }
- q := hostinfo.GetCert()
- if q == nil {
- return ""
- }
- b, err := q.Certificate.MarshalJSON()
- if err != nil {
- return ""
- }
- return string(b)
- }
- // clearRecords drops all DNS records, including the self entry.
- func (d *dnsServer) clearRecords() {
- d.Lock()
- defer d.Unlock()
- clear(d.dnsMap4)
- clear(d.dnsMap6)
- d.selfHost = ""
- }
- // seedSelf inserts (or refreshes) a record for our own cert name pointing at our VPN addresses,
- // so a single-lighthouse network can resolve the lighthouse's own hostname without the two-process workaround.
- func (d *dnsServer) seedSelf() {
- if !d.enabled.Load() {
- return
- }
- cs := d.certState()
- if cs == nil {
- return
- }
- c := cs.GetDefaultCertificate()
- if c == nil {
- return
- }
- newHost := strings.ToLower(c.Name()) + "."
- d.Lock()
- defer d.Unlock()
- if d.selfHost != "" && d.selfHost != newHost {
- delete(d.dnsMap4, d.selfHost)
- delete(d.dnsMap6, d.selfHost)
- }
- d.selfHost = newHost
- delete(d.dnsMap4, newHost)
- delete(d.dnsMap6, newHost)
- haveV4, haveV6 := false, false
- for _, addr := range cs.myVpnAddrs {
- if addr.Is4() && !haveV4 {
- d.dnsMap4[newHost] = addr
- haveV4 = true
- } else if addr.Is6() && !haveV6 {
- d.dnsMap6[newHost] = addr
- haveV6 = true
- }
- if haveV4 && haveV6 {
- break
- }
- }
- }
- func (d *dnsServer) certState() *CertState {
- if d.pki == nil {
- return nil
- }
- return d.pki.getCertState()
- }
- // Add adds the first IPv4 and IPv6 address that appears in `addresses` as the record for `host`
- func (d *dnsServer) Add(host string, addresses []netip.Addr) {
- if !d.enabled.Load() {
- return
- }
- host = strings.ToLower(host)
- d.Lock()
- defer d.Unlock()
- haveV4 := false
- haveV6 := false
- for _, addr := range addresses {
- if addr.Is4() && !haveV4 {
- d.dnsMap4[host] = addr
- haveV4 = true
- } else if addr.Is6() && !haveV6 {
- d.dnsMap6[host] = addr
- haveV6 = true
- }
- if haveV4 && haveV6 {
- break
- }
- }
- }
- func (d *dnsServer) isSelfNebulaOrLocalhost(addr string) bool {
- a, _, _ := net.SplitHostPort(addr)
- b, err := netip.ParseAddr(a)
- if err != nil {
- return false
- }
- if b.IsLoopback() {
- return true
- }
- cs := d.certState()
- if cs == nil || cs.myVpnAddrsTable == nil {
- return false
- }
- //if we found it in this table, it's good
- return cs.myVpnAddrsTable.Contains(b)
- }
- func (d *dnsServer) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
- debugEnabled := d.l.Enabled(context.Background(), slog.LevelDebug)
- // Per RFC 2308 §2.2, a name that exists but has no record of the requested
- // type must be answered with NOERROR and an empty answer section (NODATA),
- // not NXDOMAIN (RFC 2308 §2.1), which is reserved for names that do not
- // exist at all.
- anyNameExists := false
- for _, q := range m.Question {
- switch q.Qtype {
- case dns.TypeA, dns.TypeAAAA:
- qType := dns.TypeToString[q.Qtype]
- if debugEnabled {
- d.l.Debug("DNS query", "type", qType, "name", q.Name)
- }
- ip, nameExists := d.Query(q.Qtype, q.Name)
- if nameExists {
- anyNameExists = true
- }
- if ip.IsValid() {
- rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, qType, ip))
- if err == nil {
- m.Answer = append(m.Answer, rr)
- }
- }
- case dns.TypeTXT:
- // We only answer these queries from nebula nodes or localhost
- if !d.isSelfNebulaOrLocalhost(w.RemoteAddr().String()) {
- return
- }
- if debugEnabled {
- d.l.Debug("DNS query", "type", "TXT", "name", q.Name)
- }
- ip := d.QueryCert(q.Name)
- if ip != "" {
- rr, err := dns.NewRR(fmt.Sprintf("%s TXT %s", q.Name, ip))
- if err == nil {
- m.Answer = append(m.Answer, rr)
- }
- }
- }
- }
- if len(m.Answer) == 0 && !anyNameExists {
- m.Rcode = dns.RcodeNameError
- }
- }
- func (d *dnsServer) handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) {
- m := new(dns.Msg)
- m.SetReply(r)
- m.Compress = false
- switch r.Opcode {
- case dns.OpcodeQuery:
- d.parseQuery(m, w)
- }
- w.WriteMsg(m)
- }
- func getDnsServerAddr(c *config.C) string {
- dnsHost := strings.TrimSpace(c.GetString("lighthouse.dns.host", ""))
- // Old guidance was to provide the literal `[::]` in `lighthouse.dns.host` but that won't resolve.
- if dnsHost == "[::]" {
- dnsHost = "::"
- }
- return net.JoinHostPort(dnsHost, strconv.Itoa(c.GetInt("lighthouse.dns.port", 53)))
- }
|