Files
nomad/client/rpc_test.go
Tim Gross 30e57c39b0 discovery: correctly handle IPv6 addresses from go-discover (#24649)
Nomad sets a default port when resolving server addresses that don't have
one. When we get a "bare" IPv6 address without a port, we end up with an
unexpected error "too many colons in address" when we try to split the address
and host, because the standard library function expects IPv6 addresses to be
wrapped in brackets as recommended by RFC5952. User-configured addresses avoid
this problem by accepting IP address and port as separate configuration values,
but go-discover emits "bare" IPv6 addresses without a port in IPv6 environments.

Fix this by adding brackets to IPv6 addresses when we get the "too many colons"
error from the stdlib. This will still give erroneous results if the address
includes the port but is missing brackets, but there's no way to unambiguously
parse that address.

Ref: https://www.rfc-editor.org/rfc/rfc5952
Fixes: https://github.com/hashicorp/nomad/issues/24608
2024-12-17 15:49:40 -05:00

194 lines
4.6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package client
import (
"errors"
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/nomad"
"github.com/hashicorp/nomad/nomad/structs"
sconfig "github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/testutil"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/require"
)
func TestRpc_streamingRpcConn_badEndpoint(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
s1, cleanupS1 := nomad.TestServer(t, nil)
defer cleanupS1()
testutil.WaitForLeader(t, s1.RPC)
c, cleanupC := TestClient(t, func(c *config.Config) {
c.Servers = []string{s1.GetConfig().RPCAddr.String()}
})
defer cleanupC()
// Wait for the client to connect
testutil.WaitForResult(func() (bool, error) {
node, err := s1.State().NodeByID(nil, c.NodeID())
if err != nil {
return false, err
}
if node == nil {
return false, errors.New("no node")
}
return node.Status == structs.NodeStatusReady, errors.New("wrong status")
}, func(err error) {
t.Fatalf("should have a clients")
})
// Get the server
server := c.servers.FindServer()
require.NotNil(server)
conn, err := c.streamingRpcConn(server, "Bogus")
require.Nil(conn)
require.NotNil(err)
require.Contains(err.Error(), "Unknown rpc method: \"Bogus\"")
}
func TestRpc_streamingRpcConn_badEndpoint_TLS(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
const (
cafile = "../helper/tlsutil/testdata/nomad-agent-ca.pem"
fooservercert = "../helper/tlsutil/testdata/regionFoo-server-nomad.pem"
fooserverkey = "../helper/tlsutil/testdata/regionFoo-server-nomad-key.pem"
)
s1, cleanupS1 := nomad.TestServer(t, func(c *nomad.Config) {
c.Region = "regionFoo"
c.BootstrapExpect = 1
c.TLSConfig = &sconfig.TLSConfig{
EnableHTTP: true,
EnableRPC: true,
VerifyServerHostname: true,
CAFile: cafile,
CertFile: fooservercert,
KeyFile: fooserverkey,
}
})
defer cleanupS1()
testutil.WaitForLeader(t, s1.RPC)
c, cleanupC := TestClient(t, func(c *config.Config) {
c.Region = "regionFoo"
c.Servers = []string{s1.GetConfig().RPCAddr.String()}
c.TLSConfig = &sconfig.TLSConfig{
EnableHTTP: true,
EnableRPC: true,
VerifyServerHostname: true,
CAFile: cafile,
CertFile: fooservercert,
KeyFile: fooserverkey,
}
})
defer cleanupC()
// Wait for the client to connect
testutil.WaitForResult(func() (bool, error) {
node, err := s1.State().NodeByID(nil, c.NodeID())
if err != nil {
return false, err
}
if node == nil {
return false, errors.New("no node")
}
return node.Status == structs.NodeStatusReady, errors.New("wrong status")
}, func(err error) {
t.Fatalf("should have a clients")
})
// Get the server
server := c.servers.FindServer()
require.NotNil(server)
conn, err := c.streamingRpcConn(server, "Bogus")
require.Nil(conn)
require.NotNil(err)
require.Contains(err.Error(), "Unknown rpc method: \"Bogus\"")
}
func Test_resolveServer(t *testing.T) {
// note: we can't test a DNS name here without making an external DNS query,
// which we don't want to do from CI
testCases := []struct {
name string
addr string
expect string
expectErr string
}{
{
name: "ipv6 no brackets",
addr: "2001:db8::1",
expect: "[2001:db8::1]:4647",
},
{
// expected bad result
name: "ambiguous ipv6 no brackets with port",
addr: "2001:db8::1:4647",
expect: "[2001:db8::1:4647]:4647",
},
{
name: "ipv6 no port",
addr: "[2001:db8::1]",
expect: "[2001:db8::1]:4647",
},
{
name: "ipv6 trailing port colon",
addr: "[2001:db8::1]:",
expect: "[2001:db8::1]:4647",
},
{
name: "ipv6 malformed",
addr: "[2001:db8::1]:]",
expectErr: "address [2001:db8::1]:]: unexpected ']' in address",
},
{
name: "ipv6 with port",
addr: "[2001:db8::1]:6647",
expect: "[2001:db8::1]:6647",
},
{
name: "ipv4 no port",
addr: "192.168.1.117",
expect: "192.168.1.117:4647",
},
{
name: "ipv4 trailing port colon",
addr: "192.168.1.117:",
expect: "192.168.1.117:4647",
},
{
name: "ipv4 with port",
addr: "192.168.1.117:6647",
expect: "192.168.1.117:6647",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
addr, err := resolveServer(tc.addr)
if tc.expectErr != "" {
must.Nil(t, addr)
must.EqError(t, err, tc.expectErr)
} else {
must.NoError(t, err)
must.Eq(t, tc.expect, addr.String())
}
})
}
}