From 8f20afc2ba77a56eb80c79897edc00859ebdd152 Mon Sep 17 00:00:00 2001 From: Tim Gross Date: Tue, 13 Feb 2024 14:22:39 -0500 Subject: [PATCH] fingerprint: add DNS address and port to Consul fingerprint In order to provide a DNS address and port to Connect tasks configured for transparent proxy, we need to fingerprint the Consul DNS address and port. The client will pass this address/port to the iptables configuration provided to the `consul-cni` plugin. Ref: https://github.com/hashicorp/nomad/issues/10628 --- .changelog/19969.txt | 3 ++ client/fingerprint/consul.go | 61 ++++++++++++++++++++++++- client/fingerprint/consul_test.go | 75 +++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 .changelog/19969.txt diff --git a/.changelog/19969.txt b/.changelog/19969.txt new file mode 100644 index 000000000000..bc4fe6540601 --- /dev/null +++ b/.changelog/19969.txt @@ -0,0 +1,3 @@ +```release-note:improvement +fingerprint: Added a fingerprint for Consul DNS address and port +``` diff --git a/client/fingerprint/consul.go b/client/fingerprint/consul.go index 987b44c66ec7..59d44b2b384e 100644 --- a/client/fingerprint/consul.go +++ b/client/fingerprint/consul.go @@ -5,6 +5,7 @@ package fingerprint import ( "fmt" + "net/netip" "strconv" "strings" "time" @@ -165,6 +166,8 @@ func (cfs *consulFingerprintState) initialize(cfg *config.ConsulConfig, logger h "consul.grpc": cfs.grpc(consulConfig.Scheme, logger), "consul.ft.namespaces": cfs.namespaces, "consul.partition": cfs.partition, + "consul.dns_port": cfs.dnsPort, + "consul.dns_addr": cfs.dnsAddr(logger), } } else { cfs.extractors = map[string]consulExtractor{ @@ -178,6 +181,8 @@ func (cfs *consulFingerprintState) initialize(cfg *config.ConsulConfig, logger h fmt.Sprintf("consul.%s.grpc", cfg.Name): cfs.grpc(consulConfig.Scheme, logger), fmt.Sprintf("consul.%s.ft.namespaces", cfg.Name): cfs.namespaces, fmt.Sprintf("consul.%s.partition", cfg.Name): cfs.partition, + fmt.Sprintf("consul.%s.dns_port", cfg.Name): cfs.dnsPort, + fmt.Sprintf("consul.%s.dns_addr", cfg.Name): cfs.dnsAddr(logger), } } @@ -191,7 +196,7 @@ func (cfs *consulFingerprintState) query(logger hclog.Logger) agentconsul.Self { if err != nil { // indicate consul no longer available if cfs.isAvailable { - logger.Info("consul agent is unavailable: %v", err) + logger.Info("consul agent is unavailable", "error", err) } cfs.isAvailable = false cfs.nextCheck = time.Time{} // force check on next interval @@ -298,6 +303,60 @@ func (cfs *consulFingerprintState) grpcTLSPort(info agentconsul.Self) (string, b return fmt.Sprintf("%d", int(p)), ok } +func (cfs *consulFingerprintState) dnsPort(info agentconsul.Self) (string, bool) { + p, ok := info["DebugConfig"]["DNSPort"].(float64) + return fmt.Sprintf("%d", int(p)), ok +} + +// dnsAddr fingerprints the Consul DNS address, but only if Nomad can use it +// usefully to provide an iptables rule to a task +func (cfs *consulFingerprintState) dnsAddr(logger hclog.Logger) func(info agentconsul.Self) (string, bool) { + return func(info agentconsul.Self) (string, bool) { + + // only addresses we can use for an iptables rule from a container to the + // host will be fingerprinted + isValidForTaskUse := func(addr netip.Addr) (string, bool) { + if !addr.IsLoopback() && !addr.IsUnspecified() && addr.IsValid() { + return addr.String(), true + } + return "", false + } + + // first try to find an explicitly configured address + dnsAddrs, ok := info["DebugConfig"]["DNSAddrs"].([]string) + if ok { + for _, dnsAddr := range dnsAddrs { + dnsAddr = strings.TrimPrefix(dnsAddr, "tcp://") + dnsAddr = strings.TrimPrefix(dnsAddr, "udp://") + + parsed, err := netip.ParseAddrPort(dnsAddr) + if err != nil { + logger.Warn("could not parse Consul addresses.dns config", "value", dnsAddr) + return "", false + } + if val, ok := isValidForTaskUse(parsed.Addr()); ok { + return val, true + } + } + } + + // fallback to the bind address + bindAddr, ok := info["DebugConfig"]["BindAddr"].(string) + if ok { + parsed, err := netip.ParseAddr(bindAddr) + if err != nil { + logger.Warn("could not parse Consul bind_addr config", "value", bindAddr) + return "", false + } + if val, ok := isValidForTaskUse(parsed); ok { + return val, true + } + } + + return "", true // we can't fingerprint a useful value + } +} + func (cfs *consulFingerprintState) namespaces(info agentconsul.Self) (string, bool) { return strconv.FormatBool(agentconsul.Namespaces(info)), true } diff --git a/client/fingerprint/consul_test.go b/client/fingerprint/consul_test.go index a190d65b3c61..b1fdd11e66af 100644 --- a/client/fingerprint/consul_test.go +++ b/client/fingerprint/consul_test.go @@ -491,6 +491,77 @@ func TestConsulFingerprint_partition(t *testing.T) { }) } +func TestConsulFingerprint_dns(t *testing.T) { + ci.Parallel(t) + + cfs := consulFingerprintState{} + + t.Run("dns port not enabled", func(t *testing.T) { + port, ok := cfs.dnsPort(agentconsul.Self{ + "DebugConfig": {"DNSPort": -1.0}, // JSON numbers are floats + }) + must.True(t, ok) + must.Eq(t, "-1", port) + }) + + t.Run("non-default port value", func(t *testing.T) { + port, ok := cfs.dnsPort(agentconsul.Self{ + "DebugConfig": {"DNSPort": 8601.0}, // JSON numbers are floats + }) + must.True(t, ok) + must.Eq(t, "8601", port) + }) + + t.Run("missing port", func(t *testing.T) { + port, ok := cfs.dnsPort(agentconsul.Self{ + "DebugConfig": {}, + }) + must.False(t, ok) + must.Eq(t, "0", port) + }) + + t.Run("malformed port", func(t *testing.T) { + port, ok := cfs.dnsPort(agentconsul.Self{ + "DebugConfig": {"DNSPort": "A"}, + }) + must.False(t, ok) + must.Eq(t, "0", port) + }) + + t.Run("bind on 0.0.0.0", func(t *testing.T) { + addr, ok := cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{ + "DebugConfig": { + "DNSAddrs": []string{"tcp://0.0.0.0:8601", "udp://0.0.0.0:8601"}, + "BindAddr": "0.0.0.0", + }, + }) + must.True(t, ok) + must.Eq(t, "", addr) + }) + + t.Run("get first IP", func(t *testing.T) { + addr, ok := cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{ + "DebugConfig": { + "DNSAddrs": []string{"tcp://192.168.1.170:8601", "udp://192.168.1.171:8601"}, + "BindAddr": "192.168.1.172", + }, + }) + must.True(t, ok) + must.Eq(t, "192.168.1.170", addr) + }) + + t.Run("fallback to bind_addr", func(t *testing.T) { + addr, ok := cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{ + "DebugConfig": { + "DNSAddrs": []string{"tcp://0.0.0.0:8601", "udp://0.0.0.0:8601"}, + "BindAddr": "192.168.1.172", + }, + }) + must.True(t, ok) + must.Eq(t, "192.168.1.172", addr) + }) +} + func TestConsulFingerprint_Fingerprint_oss(t *testing.T) { ci.Parallel(t) @@ -510,6 +581,7 @@ func TestConsulFingerprint_Fingerprint_oss(t *testing.T) { must.NoError(t, err) must.Eq(t, map[string]string{ "consul.datacenter": "dc1", + "consul.dns_port": "8600", "consul.revision": "3c1c22679", "consul.segment": "seg1", "consul.server": "true", @@ -564,6 +636,7 @@ func TestConsulFingerprint_Fingerprint_oss(t *testing.T) { "consul.version": "1.9.5", "consul.connect": "true", "consul.grpc": "8502", + "consul.dns_port": "8600", "consul.ft.namespaces": "false", "unique.consul.name": "HAL9000", }, resp3.Attributes) @@ -600,6 +673,7 @@ func TestConsulFingerprint_Fingerprint_ent(t *testing.T) { "consul.ft.namespaces": "true", "consul.connect": "true", "consul.grpc": "8502", + "consul.dns_port": "8600", "consul.partition": "default", "unique.consul.name": "HAL9000", }, resp.Attributes) @@ -649,6 +723,7 @@ func TestConsulFingerprint_Fingerprint_ent(t *testing.T) { "consul.ft.namespaces": "true", "consul.connect": "true", "consul.grpc": "8502", + "consul.dns_port": "8600", "consul.partition": "default", "unique.consul.name": "HAL9000", }, resp3.Attributes)