Skip to content

Commit

Permalink
fingerprint: add DNS address and port to Consul fingerprint (#19969)
Browse files Browse the repository at this point in the history
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: #10628
  • Loading branch information
tgross authored Feb 14, 2024
1 parent 994a2b1 commit a747758
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .changelog/19969.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
fingerprint: Added a fingerprint for Consul DNS address and port
```
86 changes: 85 additions & 1 deletion client/fingerprint/consul.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package fingerprint

import (
"fmt"
"net/netip"
"strconv"
"strings"
"time"
Expand All @@ -13,6 +14,7 @@ import (
"github.com/hashicorp/go-hclog"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/go-version"
agentconsul "github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper"
Expand Down Expand Up @@ -165,6 +167,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{
Expand All @@ -178,6 +182,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),
}
}

Expand All @@ -191,7 +197,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
Expand Down Expand Up @@ -298,6 +304,84 @@ 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) {

var listenOnEveryIP bool

dnsAddrs, ok := info["DebugConfig"]["DNSAddrs"].([]any)
if !ok {
logger.Warn("Consul returned invalid addresses.dns config",
"value", info["DebugConfig"]["DNSAddrs"])
return "", false
}

for _, d := range dnsAddrs {
dnsAddr, ok := d.(string)
if !ok {
logger.Warn("Consul returned invalid addresses.dns config",
"value", info["DebugConfig"]["DNSAddrs"])
return "", false

}
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, "error", err)
return "", false // response is somehow malformed
}

// only addresses we can use for an iptables rule from a
// container to the host will be fingerprinted
if parsed.Addr().IsUnspecified() {
listenOnEveryIP = true
break
}
if !parsed.Addr().IsLoopback() {
return parsed.Addr().String(), true
}
}

// if Consul DNS is bound on 0.0.0.0, we want to fingerprint the private
// IP (or at worst, the public IP) of the host so that we have a valid
// IP address for the iptables rule
if listenOnEveryIP {

privateIP, err := sockaddr.GetPrivateIP()
if err != nil {
logger.Warn("could not query network interfaces", "error", err)
return "", false // something is very wrong, so bail out
}
if privateIP != "" {
return privateIP, true
}
publicIP, err := sockaddr.GetPublicIP()
if err != nil {
logger.Warn("could not query network interfaces", "error", err)
return "", false // something is very wrong, so bail out
}
if publicIP != "" {
return publicIP, true
}
}

// if we've hit here, Consul is bound on localhost and we won't be able
// to configure container DNS to use it, but we also don't want to have
// the fingerprinter return an error
return "", true
}
}

func (cfs *consulFingerprintState) namespaces(info agentconsul.Self) (string, bool) {
return strconv.FormatBool(agentconsul.Namespaces(info)), true
}
Expand Down
105 changes: 105 additions & 0 deletions client/fingerprint/consul_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,105 @@ 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("get first IP", func(t *testing.T) {
addr, ok := cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{
"DebugConfig": {
"DNSAddrs": []any{"tcp://192.168.1.170:8601", "udp://192.168.1.171:8601"},
},
})
must.True(t, ok)
must.Eq(t, "192.168.1.170", addr)

addr, ok = cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{
"DebugConfig": {"DNSAddrs": []any{"tcp://[2001:0db8:85a3::8a2e:0370:7334]:8601"}},
})
must.True(t, ok)
must.Eq(t, "2001:db8:85a3::8a2e:370:7334", addr)
})

t.Run("loopback address", func(t *testing.T) {
addr, ok := cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{
"DebugConfig": {
"DNSAddrs": []any{"tcp://127.0.0.1:8601", "udp://127.0.0.1:8601"},
},
})
must.True(t, ok)
must.Eq(t, "", addr)

addr, ok = cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{
"DebugConfig": {"DNSAddrs": []any{"tcp://[::1]:8601"}},
})
must.True(t, ok)
must.Eq(t, "", addr)

})

t.Run("fallback to private or public IP", func(t *testing.T) {
addr, ok := cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{
"DebugConfig": {
"DNSAddrs": []any{"tcp://0.0.0.0:8601", "udp://0.0.0.0:8601"},
},
})
must.True(t, ok)
must.NotEq(t, "", addr)
})

t.Run("malformed DNSAddrs", func(t *testing.T) {
addr, ok := cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{
"DebugConfig": {"DNSAddrs": []int{0}}})
must.False(t, ok)
must.Eq(t, "", addr)

addr, ok = cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{
"DebugConfig": {"DNSAddrs": []any{0}}})
must.False(t, ok)
must.Eq(t, "", addr)

addr, ok = cfs.dnsAddr(testlog.HCLogger(t))(agentconsul.Self{
"DebugConfig": {"DNSAddrs": []any{"tcp://XXXXX"}}})
must.False(t, ok)
must.Eq(t, "", addr)
})

}

func TestConsulFingerprint_Fingerprint_oss(t *testing.T) {
ci.Parallel(t)

Expand All @@ -510,6 +609,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",
Expand Down Expand Up @@ -564,6 +664,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)
Expand Down Expand Up @@ -600,6 +701,8 @@ func TestConsulFingerprint_Fingerprint_ent(t *testing.T) {
"consul.ft.namespaces": "true",
"consul.connect": "true",
"consul.grpc": "8502",
"consul.dns.addr": "192.168.1.117",
"consul.dns.port": "8600",
"consul.partition": "default",
"unique.consul.name": "HAL9000",
}, resp.Attributes)
Expand Down Expand Up @@ -649,6 +752,8 @@ func TestConsulFingerprint_Fingerprint_ent(t *testing.T) {
"consul.ft.namespaces": "true",
"consul.connect": "true",
"consul.grpc": "8502",
"consul.dns.addr": "192.168.1.117",
"consul.dns.port": "8600",
"consul.partition": "default",
"unique.consul.name": "HAL9000",
}, resp3.Attributes)
Expand Down
4 changes: 2 additions & 2 deletions client/fingerprint/test_fixtures/consul/agent_self_ent.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@
"ConsulServerHealthInterval": "10ms",
"DNSARecordLimit": 0,
"DNSAddrs": [
"tcp://127.0.0.1:8600",
"udp://127.0.0.1:8600"
"tcp://192.168.1.117:8600",
"udp://192.168.1.117:8600"
],
"DNSAllowStale": true,
"DNSAltDomain": "",
Expand Down

0 comments on commit a747758

Please sign in to comment.