Skip to content

Commit 15c01e5

Browse files
authored
ipv6: normalize addrs per RFC-5942 §4 (#25921)
https://datatracker.ietf.org/doc/html/rfc5952#section-4 * copy NormalizeAddr func from vault * PRs hashicorp/vault#29228 & hashicorp/vault#29517 * normalize bind/advertise addrs * normalize consul/vault addrs
1 parent cfe6349 commit 15c01e5

File tree

10 files changed

+384
-29
lines changed

10 files changed

+384
-29
lines changed

command/agent/config.go

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
client "github.com/hashicorp/nomad/client/config"
2727
"github.com/hashicorp/nomad/client/fingerprint"
2828
"github.com/hashicorp/nomad/helper"
29+
"github.com/hashicorp/nomad/helper/ipaddr"
2930
"github.com/hashicorp/nomad/helper/pointer"
3031
"github.com/hashicorp/nomad/helper/users"
3132
"github.com/hashicorp/nomad/nomad"
@@ -1995,6 +1996,7 @@ func (c *Config) normalizeAddrs() error {
19951996
}
19961997
c.BindAddr = ipStr
19971998
}
1999+
c.BindAddr = ipaddr.NormalizeAddr(c.BindAddr)
19982000

19992001
httpAddrs, err := normalizeMultipleBind(c.Addresses.HTTP, c.BindAddr)
20002002
if err != nil {
@@ -2015,9 +2017,12 @@ func (c *Config) normalizeAddrs() error {
20152017
c.Addresses.Serf = addr
20162018

20172019
c.normalizedAddrs = &NormalizedAddrs{
2018-
HTTP: joinHostPorts(httpAddrs, strconv.Itoa(c.Ports.HTTP)),
2019-
RPC: net.JoinHostPort(c.Addresses.RPC, strconv.Itoa(c.Ports.RPC)),
2020-
Serf: net.JoinHostPort(c.Addresses.Serf, strconv.Itoa(c.Ports.Serf)),
2020+
RPC: normalizeAddrWithPort(c.Addresses.RPC, c.Ports.RPC),
2021+
Serf: normalizeAddrWithPort(c.Addresses.Serf, c.Ports.Serf),
2022+
}
2023+
c.normalizedAddrs.HTTP = make([]string, len(httpAddrs))
2024+
for i, addr := range httpAddrs {
2025+
c.normalizedAddrs.HTTP[i] = normalizeAddrWithPort(addr, c.Ports.HTTP)
20212026
}
20222027

20232028
addr, err = normalizeAdvertise(c.AdvertiseAddrs.HTTP, httpAddrs[0], c.Ports.HTTP, c.DevMode)
@@ -2100,14 +2105,21 @@ func parseMultipleIPTemplate(ipTmpl string) ([]string, error) {
21002105
return deduplicateAddrs(ips), nil
21012106
}
21022107

2108+
// normalizeAddrWithPort assumes that addr does not contain a port,
2109+
// noramlizes it per ipv6 RFC-5942 §4, and appends ":{port}".
2110+
func normalizeAddrWithPort(addr string, port int) string {
2111+
return ipaddr.NormalizeAddr(net.JoinHostPort(addr, strconv.Itoa(port)))
2112+
}
2113+
21032114
// normalizeBind returns a normalized bind address.
21042115
//
21052116
// If addr is set it is used, if not the default bind address is used.
21062117
func normalizeBind(addr, bind string) (string, error) {
21072118
if addr == "" {
21082119
return bind, nil
21092120
}
2110-
return listenerutil.ParseSingleIPTemplate(addr)
2121+
addr, err := listenerutil.ParseSingleIPTemplate(addr)
2122+
return ipaddr.NormalizeAddr(addr), err
21112123
}
21122124

21132125
// normalizeMultipleBind returns normalized bind addresses.
@@ -2117,7 +2129,11 @@ func normalizeMultipleBind(addr, bind string) ([]string, error) {
21172129
if addr == "" {
21182130
return []string{bind}, nil
21192131
}
2120-
return parseMultipleIPTemplate(addr)
2132+
addrs, err := parseMultipleIPTemplate(addr)
2133+
for i, addr := range addrs {
2134+
addrs[i] = ipaddr.NormalizeAddr(addr)
2135+
}
2136+
return addrs, err
21212137
}
21222138

21232139
// normalizeAdvertise returns a normalized advertise address.
@@ -2147,10 +2163,10 @@ func normalizeAdvertise(addr string, bind string, defport int, dev bool) (string
21472163
}
21482164

21492165
// missing port, append the default
2150-
return net.JoinHostPort(addr, strconv.Itoa(defport)), nil
2166+
return normalizeAddrWithPort(addr, defport), nil
21512167
}
21522168

2153-
return addr, nil
2169+
return ipaddr.NormalizeAddr(addr), nil
21542170
}
21552171

21562172
// Fallback to bind address first, and then try resolving the local hostname
@@ -2162,12 +2178,12 @@ func normalizeAdvertise(addr string, bind string, defport int, dev bool) (string
21622178
// Return the first non-localhost unicast address
21632179
for _, ip := range ips {
21642180
if ip.IsLinkLocalUnicast() || ip.IsGlobalUnicast() {
2165-
return net.JoinHostPort(ip.String(), strconv.Itoa(defport)), nil
2181+
return normalizeAddrWithPort(ip.String(), defport), nil
21662182
}
21672183
if ip.IsLoopback() {
21682184
if dev {
21692185
// loopback is fine for dev mode
2170-
return net.JoinHostPort(ip.String(), strconv.Itoa(defport)), nil
2186+
return normalizeAddrWithPort(ip.String(), defport), nil
21712187
}
21722188
return "", fmt.Errorf("Defaulting advertise to localhost is unsafe, please set advertise manually")
21732189
}
@@ -2178,7 +2194,7 @@ func normalizeAdvertise(addr string, bind string, defport int, dev bool) (string
21782194
if err != nil {
21792195
return "", fmt.Errorf("Unable to parse default advertise address: %v", err)
21802196
}
2181-
return net.JoinHostPort(addr, strconv.Itoa(defport)), nil
2197+
return normalizeAddrWithPort(addr, defport), nil
21822198
}
21832199

21842200
// isMissingPort returns true if an error is a "missing port" error from
@@ -2923,17 +2939,6 @@ func LoadConfigDir(dir string) (*Config, error) {
29232939
return result, nil
29242940
}
29252941

2926-
// joinHostPorts joins every addr in addrs with the specified port
2927-
func joinHostPorts(addrs []string, port string) []string {
2928-
localAddrs := make([]string, len(addrs))
2929-
for i, k := range addrs {
2930-
localAddrs[i] = net.JoinHostPort(k, port)
2931-
2932-
}
2933-
2934-
return localAddrs
2935-
}
2936-
29372942
// isTemporaryFile returns true or false depending on whether the
29382943
// provided file name is a temporary file for the following editors:
29392944
// emacs or vim.

command/agent/config_parse.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/hashicorp/hcl/hcl/ast"
1818
client "github.com/hashicorp/nomad/client/config"
1919
"github.com/hashicorp/nomad/helper"
20+
"github.com/hashicorp/nomad/helper/ipaddr"
2021
"github.com/hashicorp/nomad/nomad/structs"
2122
"github.com/hashicorp/nomad/nomad/structs/config"
2223
"github.com/mitchellh/mapstructure"
@@ -434,6 +435,10 @@ func parseVaults(c *Config, list *ast.ObjectList) error {
434435
c.Vaults = append(c.Vaults, v)
435436
}
436437

438+
for _, conf := range c.Vaults {
439+
conf.Addr = ipaddr.NormalizeAddr(conf.Addr)
440+
}
441+
437442
// Decode the default identity.
438443
var listVal *ast.ObjectList
439444
if ot, ok := obj.Val.(*ast.ObjectType); ok {
@@ -505,6 +510,11 @@ func parseConsuls(c *Config, list *ast.ObjectList) error {
505510
c.Consuls = append(c.Consuls, cc)
506511
}
507512

513+
for _, conf := range c.Consuls {
514+
conf.Addr = ipaddr.NormalizeAddr(conf.Addr)
515+
conf.GRPCAddr = ipaddr.NormalizeAddr(conf.GRPCAddr)
516+
}
517+
508518
// decode service and template identity blocks
509519
var listVal *ast.ObjectList
510520
if ot, ok := obj.Val.(*ast.ObjectType); ok {

command/agent/config_parse_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,7 +1067,7 @@ func TestConfig_MultipleVault(t *testing.T) {
10671067

10681068
must.Eq(t, "alternate", cfg.Vaults[1].Name)
10691069
must.True(t, *cfg.Vaults[1].Enabled)
1070-
must.Eq(t, "127.0.0.1:9501", cfg.Vaults[1].Addr)
1070+
must.Eq(t, "[::1f]:9501", cfg.Vaults[1].Addr)
10711071

10721072
must.Eq(t, "other", cfg.Vaults[2].Name)
10731073
must.Nil(t, cfg.Vaults[2].Enabled)
@@ -1119,7 +1119,7 @@ func TestConfig_MultipleConsul(t *testing.T) {
11191119
must.Eq(t, "abracadabra", defaultConsul.Token)
11201120

11211121
must.Eq(t, "alternate", cfg.Consuls[1].Name)
1122-
must.Eq(t, "127.0.0.2:8501", cfg.Consuls[1].Addr)
1122+
must.Eq(t, "[::1f]:8501", cfg.Consuls[1].Addr)
11231123
must.Eq(t, "xyzzy", cfg.Consuls[1].Token)
11241124

11251125
must.Eq(t, "other", cfg.Consuls[2].Name)

command/agent/config_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/hashicorp/nomad/helper/pointer"
2222
"github.com/hashicorp/nomad/nomad/structs"
2323
"github.com/hashicorp/nomad/nomad/structs/config"
24+
"github.com/shoenig/test"
2425
"github.com/shoenig/test/must"
2526
"github.com/stretchr/testify/require"
2627
)
@@ -956,6 +957,33 @@ func TestConfig_normalizeAddrs_IPv6Loopback(t *testing.T) {
956957
}
957958
}
958959

960+
// TestConfig_normalizeAddrs_IPv6 asserts that bind and advertise addrs conform
961+
// to RFC 5942 §4: https://www.rfc-editor.org/rfc/rfc5942.html#section-4
962+
// Full coverage is provided by tests for ipaddr.NormalizeAddr
963+
func TestConfig_normalizeAddrs_IPv6(t *testing.T) {
964+
c := &Config{
965+
Addresses: &Addresses{},
966+
967+
BindAddr: "0:0::1F",
968+
Ports: &Ports{
969+
HTTP: 4646,
970+
RPC: 4647,
971+
},
972+
AdvertiseAddrs: &AdvertiseAddrs{
973+
HTTP: "[A110::0:0:C8]:8080",
974+
RPC: "0:00FA:0:0:0::CE",
975+
},
976+
DevMode: false,
977+
}
978+
must.NoError(t, c.normalizeAddrs())
979+
test.Eq(t, "::1f", c.Addresses.HTTP, test.Sprint("bind HTTP"))
980+
test.Eq(t, "::1f", c.Addresses.RPC, test.Sprint("bind RPC"))
981+
test.Eq(t, []string{"[::1f]:4646"}, c.normalizedAddrs.HTTP, test.Sprint("normalized HTTP"))
982+
test.Eq(t, "[::1f]:4647", c.normalizedAddrs.RPC, test.Sprint("normalized RPC"))
983+
test.Eq(t, "[a110::c8]:8080", c.AdvertiseAddrs.HTTP, test.Sprint("advertise HTTP"))
984+
test.Eq(t, "[0:fa::ce]:4647", c.AdvertiseAddrs.RPC, test.Sprint("advertise RPC"))
985+
}
986+
959987
// TestConfig_normalizeAddrs_MultipleInterface asserts that normalizeAddrs will
960988
// handle normalizing multiple interfaces in a single protocol.
961989
func TestConfig_normalizeAddrs_MultipleInterfaces(t *testing.T) {

command/agent/testdata/extra-consul.hcl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ consul {
1818
server_rpc_check_name = "nomad-server-rpc-health-check"
1919
client_service_name = "nomad-client"
2020
client_http_check_name = "nomad-client-http-health-check"
21-
address = "127.0.0.2:8501"
21+
address = "[0:0::1F]:8501"
2222
allow_unauthenticated = true
2323
token = "xyzzy"
2424
auth = "username:pass"

command/agent/testdata/extra-consul.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"server_rpc_check_name": "nomad-server-rpc-health-check",
1515
"client_service_name": "nomad-client",
1616
"client_http_check_name": "nomad-client-http-health-check",
17-
"address": "127.0.0.2:8501",
17+
"address": "[0:0::1F]:8501",
1818
"allow_unauthenticated": true,
1919
"token": "xyzzy",
2020
"auth": "username:pass"

command/agent/testdata/extra-vault.hcl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ vault {
1010
# these alternate configs should be added as an extra vault configs
1111
vault {
1212
name = "alternate"
13-
address = "127.0.0.1:9501"
13+
address = "[0:0::1F]:9501"
1414
allow_unauthenticated = true
1515
task_token_ttl = "5s"
1616
enabled = true

command/agent/testdata/extra-vault.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
{
88
"name": "alternate",
9-
"address": "127.0.0.1:9501",
9+
"address": "[0:0::1F]:9501",
1010
"allow_unauthenticated": true,
1111
"task_token_ttl": "5s",
1212
"enabled": true,

helper/ipaddr/ipaddr.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
package ipaddr
55

6+
import (
7+
"net"
8+
"net/url"
9+
"strings"
10+
)
11+
612
// IsAny checks if the given IP address is an IPv4 or IPv6 ANY address.
713
func IsAny(ip string) bool {
814
return isAnyV4(ip) || isAnyV6(ip)
@@ -11,3 +17,104 @@ func IsAny(ip string) bool {
1117
func isAnyV4(ip string) bool { return ip == "0.0.0.0" }
1218

1319
func isAnyV6(ip string) bool { return ip == "::" || ip == "[::]" }
20+
21+
// NormalizeAddr takes a string of a Host, Host:Port, URL, or Destination
22+
// Address and returns a copy where any IP addresses have been normalized to be
23+
// conformant with RFC 5942 §4. If the input string does not match any of the
24+
// supported syntaxes, or the "host" section is not an IP address, the input
25+
// will be returned unchanged. Supported syntaxes are:
26+
//
27+
// Host host or [host]
28+
// Host:Port host:port or [host]:port
29+
// URL scheme://user@host/path?query#frag or scheme://user@[host]/path?query#frag
30+
// Destination Address user@host:port or user@[host]:port
31+
//
32+
// See:
33+
//
34+
// https://rfc-editor.org/rfc/rfc3986.html
35+
// https://rfc-editor.org/rfc/rfc5942.html
36+
// https://rfc-editor.org/rfc/rfc5952.html
37+
//
38+
// Note: This function was copied verbatim from Vault:
39+
// https://github.com/hashicorp/vault/blob/58a49e6/internalshared/configutil/normalize.go
40+
func NormalizeAddr(addr string) string {
41+
if addr == "" {
42+
return ""
43+
}
44+
45+
// Host
46+
ip := net.ParseIP(addr)
47+
if ip != nil {
48+
// net.IP.String() is RFC 5942 §4 compliant
49+
return ip.String()
50+
}
51+
52+
// [Host]
53+
if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") {
54+
if len(addr) < 3 {
55+
return addr
56+
}
57+
58+
// If we've been given a bracketed IP address, return the address
59+
// normalized without brackets.
60+
ip := net.ParseIP(addr[1 : len(addr)-1])
61+
if ip != nil {
62+
return ip.String()
63+
}
64+
65+
// Our input is not a valid schema.
66+
return addr
67+
}
68+
69+
// Host:Port
70+
host, port, err := net.SplitHostPort(addr)
71+
if err == nil {
72+
ip := net.ParseIP(host)
73+
if ip == nil {
74+
// Our host isn't an IP address so we can return it unchanged
75+
return addr
76+
}
77+
78+
// net.JoinHostPort handles bracketing for RFC 5952 §6
79+
return net.JoinHostPort(ip.String(), port)
80+
}
81+
82+
// URL
83+
u, err := url.Parse(addr)
84+
if err == nil {
85+
uhost := u.Hostname()
86+
ip := net.ParseIP(uhost)
87+
if ip == nil {
88+
// Our URL doesn't contain an IP address so we can return our input unchanged.
89+
return addr
90+
} else {
91+
uhost = ip.String()
92+
}
93+
94+
if uport := u.Port(); uport != "" {
95+
uhost = net.JoinHostPort(uhost, uport)
96+
} else if !strings.HasPrefix(uhost, "[") && !strings.HasSuffix(uhost, "]") {
97+
// Ensure the IPv6 URL host is bracketed post-normalization.
98+
// When*url.URL.String() reassembles the URL it will not consider
99+
// whether or not the *url.URL.Host is RFC 5952 §6 and RFC 3986 §3.2.2
100+
// conformant.
101+
uhost = "[" + uhost + "]"
102+
103+
}
104+
u.Host = uhost
105+
106+
return u.String()
107+
}
108+
109+
// Destination Address
110+
if idx := strings.LastIndex(addr, "@"); idx > 0 {
111+
if idx+1 > len(addr) {
112+
return addr
113+
}
114+
115+
return addr[:idx+1] + NormalizeAddr(addr[idx+1:])
116+
}
117+
118+
// Our input did not match our supported schemas. Return it unchanged.
119+
return addr
120+
}

0 commit comments

Comments
 (0)