diff --git a/agent/agent.go b/agent/agent.go index 32ba092fce..13c55b1567 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -2139,13 +2139,17 @@ func (a *Agent) AddProxy(proxy *structs.ConnectManagedProxy, persist bool, if err != nil { return err } - chkTypes := []*structs.CheckType{ - &structs.CheckType{ - Name: "Connect Proxy Listening", - TCP: fmt.Sprintf("%s:%d", proxyCfg["bind_address"], - proxyCfg["bind_port"]), - Interval: 10 * time.Second, - }, + chkAddr := a.resolveProxyCheckAddress(proxyCfg) + chkTypes := []*structs.CheckType{} + if chkAddr != "" { + chkTypes = []*structs.CheckType{ + &structs.CheckType{ + Name: "Connect Proxy Listening", + TCP: fmt.Sprintf("%s:%d", chkAddr, + proxyCfg["bind_port"]), + Interval: 10 * time.Second, + }, + } } err = a.AddService(proxyService, chkTypes, persist, token) @@ -2162,6 +2166,44 @@ func (a *Agent) AddProxy(proxy *structs.ConnectManagedProxy, persist bool, return nil } +// resolveProxyCheckAddress returns the best address to use for a TCP check of +// the proxy's public listener. It expects the input to already have default +// values populated by applyProxyConfigDefaults. It may return an empty string +// indicating that the TCP check should not be created at all. +// +// By default this uses the proxy's bind address which in turn defaults to the +// agent's bind address. If the proxy bind address ends up being 0.0.0.0 we have +// to assume the agent can dial it over loopback which is usually true. +// +// In some topologies such as proxy being in a different container, the IP the +// agent used to dial proxy over a local bridge might not be the same as the +// container's public routable IP address so we allow a manual override of the +// check address in config "tcp_check_address" too. +// +// Finally the TCP check can be disabled by another manual override +// "disable_tcp_check" in cases where the agent will never be able to dial the +// proxy directly for some reason. +func (a *Agent) resolveProxyCheckAddress(proxyCfg map[string]interface{}) string { + // If user disabled the check return empty string + if disable, ok := proxyCfg["disable_tcp_check"].(bool); ok && disable { + return "" + } + + // If user specified a custom one, use that + if chkAddr, ok := proxyCfg["tcp_check_address"].(string); ok && chkAddr != "" { + return chkAddr + } + + // If we have a bind address and its diallable, use that + if bindAddr, ok := proxyCfg["bind_address"].(string); ok && + bindAddr != "" && bindAddr != "0.0.0.0" && bindAddr != "[::]" { + return bindAddr + } + + // Default to localhost + return "127.0.0.1" +} + // applyProxyConfigDefaults takes a *structs.ConnectManagedProxy and returns // it's Config map merged with any defaults from the Agent's config. It would be // nicer if this were defined as a method on structs.ConnectManagedProxy but we diff --git a/agent/agent_test.go b/agent/agent_test.go index 43478ef780..f22fc88ff7 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -2567,30 +2567,6 @@ func TestAgent_reloadWatchesHTTPS(t *testing.T) { func TestAgent_AddProxy(t *testing.T) { t.Parallel() - a := NewTestAgent(t.Name(), ` - node_name = "node1" - - connect { - proxy_defaults { - exec_mode = "script" - daemon_command = ["foo", "bar"] - script_command = ["bar", "foo"] - } - } - - ports { - proxy_min_port = 20000 - proxy_max_port = 20000 - } - `) - defer a.Shutdown() - - // Register a target service we can use - reg := &structs.NodeService{ - Service: "web", - Port: 8080, - } - require.NoError(t, a.AddService(reg, nil, false, "")) tests := []struct { desc string @@ -2621,7 +2597,10 @@ func TestAgent_AddProxy(t *testing.T) { }, TargetServiceID: "web", }, - wantErr: false, + // Proxy will inherit agent's 0.0.0.0 bind address but we can't check that + // so we should default to localhost in that case. + wantTCPCheck: "127.0.0.1:20000", + wantErr: false, }, { desc: "default global exec mode", @@ -2634,7 +2613,8 @@ func TestAgent_AddProxy(t *testing.T) { Command: []string{"consul", "connect", "proxy"}, TargetServiceID: "web", }, - wantErr: false, + wantTCPCheck: "127.0.0.1:20000", + wantErr: false, }, { desc: "default daemon command", @@ -2647,7 +2627,8 @@ func TestAgent_AddProxy(t *testing.T) { Command: []string{"foo", "bar"}, TargetServiceID: "web", }, - wantErr: false, + wantTCPCheck: "127.0.0.1:20000", + wantErr: false, }, { desc: "default script command", @@ -2660,7 +2641,8 @@ func TestAgent_AddProxy(t *testing.T) { Command: []string{"bar", "foo"}, TargetServiceID: "web", }, - wantErr: false, + wantTCPCheck: "127.0.0.1:20000", + wantErr: false, }, { desc: "managed proxy with custom bind port", @@ -2677,7 +2659,6 @@ func TestAgent_AddProxy(t *testing.T) { wantTCPCheck: "127.10.10.10:1234", wantErr: false, }, - { // This test is necessary since JSON and HCL both will parse // numbers as a float64. @@ -2695,12 +2676,83 @@ func TestAgent_AddProxy(t *testing.T) { wantTCPCheck: "127.10.10.10:1234", wantErr: false, }, + { + desc: "managed proxy with overridden but unspecified ipv6 bind address", + proxy: &structs.ConnectManagedProxy{ + ExecMode: structs.ProxyExecModeDaemon, + Command: []string{"consul", "connect", "proxy"}, + Config: map[string]interface{}{ + "foo": "bar", + "bind_address": "[::]", + }, + TargetServiceID: "web", + }, + wantTCPCheck: "127.0.0.1:20000", + wantErr: false, + }, + { + desc: "managed proxy with overridden check address", + proxy: &structs.ConnectManagedProxy{ + ExecMode: structs.ProxyExecModeDaemon, + Command: []string{"consul", "connect", "proxy"}, + Config: map[string]interface{}{ + "foo": "bar", + "tcp_check_address": "127.20.20.20", + }, + TargetServiceID: "web", + }, + wantTCPCheck: "127.20.20.20:20000", + wantErr: false, + }, + { + desc: "managed proxy with disabled check", + proxy: &structs.ConnectManagedProxy{ + ExecMode: structs.ProxyExecModeDaemon, + Command: []string{"consul", "connect", "proxy"}, + Config: map[string]interface{}{ + "foo": "bar", + "disable_tcp_check": true, + }, + TargetServiceID: "web", + }, + wantTCPCheck: "", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { require := require.New(t) + a := NewTestAgent(t.Name(), ` + node_name = "node1" + + # Explicit test because proxies inheriting this value must have a health + # check on a different IP. + bind_addr = "0.0.0.0" + + connect { + proxy_defaults { + exec_mode = "script" + daemon_command = ["foo", "bar"] + script_command = ["bar", "foo"] + } + } + + ports { + proxy_min_port = 20000 + proxy_max_port = 20000 + } + `) + defer a.Shutdown() + + // Register a target service we can use + reg := &structs.NodeService{ + Service: "web", + Port: 8080, + } + require.NoError(a.AddService(reg, nil, false, "")) + err := a.AddProxy(tt.proxy, false, "") if tt.wantErr { require.Error(err) @@ -2719,23 +2771,23 @@ func TestAgent_AddProxy(t *testing.T) { // Ensure a TCP check was created for the service. gotCheck := a.State.Check("service:web-proxy") - require.NotNil(gotCheck) - require.Equal("Connect Proxy Listening", gotCheck.Name) - - // Confusingly, a.State.Check("service:web-proxy") will return the state - // but it's Definition field will be empty. This appears to be expected - // when adding Checks as part of `AddService`. Notice how `AddService` - // tests in this file don't assert on that state but instead look at the - // agent's check state directly to ensure the right thing was registered. - // We'll do the same for now. - gotTCP, ok := a.checkTCPs["service:web-proxy"] - require.True(ok) - wantTCPCheck := tt.wantTCPCheck - if wantTCPCheck == "" { - wantTCPCheck = "127.0.0.1:20000" + if tt.wantTCPCheck == "" { + require.Nil(gotCheck) + } else { + require.NotNil(gotCheck) + require.Equal("Connect Proxy Listening", gotCheck.Name) + + // Confusingly, a.State.Check("service:web-proxy") will return the state + // but it's Definition field will be empty. This appears to be expected + // when adding Checks as part of `AddService`. Notice how `AddService` + // tests in this file don't assert on that state but instead look at the + // agent's check state directly to ensure the right thing was registered. + // We'll do the same for now. + gotTCP, ok := a.checkTCPs["service:web-proxy"] + require.True(ok) + require.Equal(tt.wantTCPCheck, gotTCP.TCP) + require.Equal(10*time.Second, gotTCP.Interval) } - require.Equal(wantTCPCheck, gotTCP.TCP) - require.Equal(10*time.Second, gotTCP.Interval) }) } } diff --git a/website/source/docs/connect/configuration.html.md b/website/source/docs/connect/configuration.html.md index 3a08348810..fdb92413be 100644 --- a/website/source/docs/connect/configuration.html.md +++ b/website/source/docs/connect/configuration.html.md @@ -62,6 +62,8 @@ described here, the rest of the service definition is shown for context and is "config": { "bind_address": "0.0.0.0", "bind_port": 20000, + "tcp_check_address": "192.168.0.1", + "disable_tcp_check": false, "local_service_address": "127.0.0.1:1234", "local_connect_timeout_ms": 1000, "handshake_timeout_ms": 10000, @@ -84,6 +86,8 @@ described here, the rest of the service definition is shown for context and is #### Configuration Key Reference +All fields are optional with a sane default. + * `bind_address` - The address the proxy will bind it's _public_ mTLS listener to. It defaults to the same address the agent binds to. @@ -94,6 +98,22 @@ described here, the rest of the service definition is shown for context and is range](/docs/agent/options.html#proxy_min_port) if available. By default the range is [20000, 20255] and the port is selected at random from that range. +* `tcp_check_address` - The address the agent will + run a [TCP health check](/docs/agent/checks.html) against. By default this is + the same as the proxy's [bind address](#bind_address) except if the + bind_address is `0.0.0.0` or `[::]` in which case this defaults to `127.0.0.1` + and assumes the agent can dial the proxy over loopback. For more complex + configurations where agent and proxy communicate over a bridge for example, + this configuration can be used to specify a different _address_ (but not port) + for the agent to use for health checks if it can't talk to the proxy over + localhost or it's publicly advertised port. The check always uses the same + port that the proxy is bound to. + +* `disable_tcp_check` - If true, this disables a + TCP check being setup for the proxy. Default is false. + * `local_service_address` - The `[address]:port` that the proxy should use to connect to the local application instance. By default it assumes `127.0.0.1` as the address and takes the port