Skip to content

Commit 21eee07

Browse files
authored
Merge pull request #1411 from stgraber/cluster
incusd/cluster: Validate cluster HTTPS address on join too
2 parents ebf13b0 + ad0d260 commit 21eee07

File tree

5 files changed

+100
-232
lines changed

5 files changed

+100
-232
lines changed

cmd/incusd/api_cluster.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,11 +430,17 @@ func clusterPutJoin(d *Daemon, r *http.Request, req api.ClusterPut) response.Res
430430
return response.BadRequest(fmt.Errorf("This server is already clustered"))
431431
}
432432

433-
// The old pre 'clustering_join' join API approach is no longer supported.
433+
// Validate server address.
434434
if req.ServerAddress == "" {
435435
return response.BadRequest(fmt.Errorf("No server address provided for this member"))
436436
}
437437

438+
// Check that the provided address is an IP address or DNS, not wildcard and isn't required to specify a port.
439+
err := validate.IsListenAddress(true, false, false)(req.ServerAddress)
440+
if err != nil {
441+
return response.BadRequest(fmt.Errorf("Invalid server address %q: %w", req.ServerAddress, err))
442+
}
443+
438444
localHTTPSAddress := s.LocalConfig.HTTPSAddress()
439445

440446
var config *node.Config

internal/server/endpoints/endpoints.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111

1212
"github.com/lxc/incus/v6/internal/linux"
1313
"github.com/lxc/incus/v6/internal/server/endpoints/listeners"
14-
"github.com/lxc/incus/v6/internal/server/util"
14+
"github.com/lxc/incus/v6/internal/util"
1515
"github.com/lxc/incus/v6/shared/logger"
1616
localtls "github.com/lxc/incus/v6/shared/tls"
1717
)

internal/server/util/net.go

Lines changed: 0 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import (
77
"net"
88
"os"
99

10-
"github.com/lxc/incus/v6/internal/ports"
11-
internalUtil "github.com/lxc/incus/v6/internal/util"
1210
"github.com/lxc/incus/v6/shared/logger"
1311
localtls "github.com/lxc/incus/v6/shared/tls"
1412
)
@@ -92,152 +90,6 @@ func ServerTLSConfig(cert *localtls.CertInfo) *tls.Config {
9290
return config
9391
}
9492

95-
// NetworkInterfaceAddress returns the first global unicast address of any of the system network interfaces.
96-
// Return the empty string if none is found.
97-
func NetworkInterfaceAddress() string {
98-
ifaces, err := net.Interfaces()
99-
if err != nil {
100-
return ""
101-
}
102-
103-
for _, iface := range ifaces {
104-
addrs, err := iface.Addrs()
105-
if err != nil {
106-
continue
107-
}
108-
109-
if len(addrs) == 0 {
110-
continue
111-
}
112-
113-
for _, addr := range addrs {
114-
ipNet, ok := addr.(*net.IPNet)
115-
if !ok {
116-
continue
117-
}
118-
119-
if !ipNet.IP.IsGlobalUnicast() {
120-
continue
121-
}
122-
123-
return ipNet.IP.String()
124-
}
125-
}
126-
127-
return ""
128-
}
129-
130-
// IsAddressCovered detects if network address1 is actually covered by
131-
// address2, in the sense that they are either the same address or address2 is
132-
// specified using a wildcard with the same port of address1.
133-
func IsAddressCovered(address1, address2 string) bool {
134-
address1 = internalUtil.CanonicalNetworkAddress(address1, ports.HTTPSDefaultPort)
135-
address2 = internalUtil.CanonicalNetworkAddress(address2, ports.HTTPSDefaultPort)
136-
137-
if address1 == address2 {
138-
return true
139-
}
140-
141-
host1, port1, err := net.SplitHostPort(address1)
142-
if err != nil {
143-
return false
144-
}
145-
146-
host2, port2, err := net.SplitHostPort(address2)
147-
if err != nil {
148-
return false
149-
}
150-
151-
// If the ports are different, then address1 is clearly not covered by
152-
// address2.
153-
if port2 != port1 {
154-
return false
155-
}
156-
157-
// If address1 contains a host name, let's try to resolve it, in order
158-
// to compare the actual IPs.
159-
var addresses1 []net.IP
160-
if host1 != "" {
161-
ip := net.ParseIP(host1)
162-
if ip != nil {
163-
addresses1 = append(addresses1, ip)
164-
} else {
165-
ips, err := net.LookupHost(host1)
166-
if err == nil && len(ips) > 0 {
167-
for _, ipStr := range ips {
168-
ip := net.ParseIP(ipStr)
169-
if ip != nil {
170-
addresses1 = append(addresses1, ip)
171-
}
172-
}
173-
}
174-
}
175-
}
176-
177-
// If address2 contains a host name, let's try to resolve it, in order
178-
// to compare the actual IPs.
179-
var addresses2 []net.IP
180-
if host2 != "" {
181-
ip := net.ParseIP(host2)
182-
if ip != nil {
183-
addresses2 = append(addresses2, ip)
184-
} else {
185-
ips, err := net.LookupHost(host2)
186-
if err == nil && len(ips) > 0 {
187-
for _, ipStr := range ips {
188-
ip := net.ParseIP(ipStr)
189-
if ip != nil {
190-
addresses2 = append(addresses2, ip)
191-
}
192-
}
193-
}
194-
}
195-
}
196-
197-
for _, a1 := range addresses1 {
198-
for _, a2 := range addresses2 {
199-
if a1.Equal(a2) {
200-
return true
201-
}
202-
}
203-
}
204-
205-
// If address2 is using an IPv4 wildcard for the host, then address2 is
206-
// only covered if it's an IPv4 address.
207-
if host2 == "0.0.0.0" {
208-
ip1 := net.ParseIP(host1)
209-
if ip1 != nil && ip1.To4() != nil {
210-
return true
211-
}
212-
213-
return false
214-
}
215-
216-
// If address2 is using an IPv6 wildcard for the host, then address2 is
217-
// always covered.
218-
if host2 == "::" || host2 == "" {
219-
return true
220-
}
221-
222-
return false
223-
}
224-
225-
// IsWildCardAddress returns whether the given address is a wildcard.
226-
func IsWildCardAddress(address string) bool {
227-
address = internalUtil.CanonicalNetworkAddress(address, ports.HTTPSDefaultPort)
228-
229-
host, _, err := net.SplitHostPort(address)
230-
if err != nil {
231-
return false
232-
}
233-
234-
if host == "0.0.0.0" || host == "::" || host == "" {
235-
return true
236-
}
237-
238-
return false
239-
}
240-
24193
// SysctlGet retrieves the value of a sysctl file in /proc/sys.
24294
func SysctlGet(path string) (string, error) {
24395
// Read the current content

internal/server/util/net_test.go

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
package util_test
22

33
import (
4-
"fmt"
54
"io"
6-
"net"
75
"testing"
86

97
"github.com/stretchr/testify/assert"
108
"github.com/stretchr/testify/require"
119

12-
"github.com/lxc/incus/v6/internal/ports"
1310
"github.com/lxc/incus/v6/internal/server/util"
14-
internalUtil "github.com/lxc/incus/v6/internal/util"
1511
)
1612

1713
// The connection returned by the dialer is paired with the one returned by the
@@ -43,81 +39,3 @@ func TestInMemoryNetwork(t *testing.T) {
4339
_, err = client.Write([]byte("hello"))
4440
assert.EqualError(t, err, "io: read/write on closed pipe")
4541
}
46-
47-
func TestCanonicalNetworkAddress(t *testing.T) {
48-
cases := map[string]string{
49-
"127.0.0.1": "127.0.0.1:8443",
50-
"127.0.0.1:": "127.0.0.1:8443",
51-
"foo.bar": "foo.bar:8443",
52-
"foo.bar:": "foo.bar:8443",
53-
"foo.bar:8444": "foo.bar:8444",
54-
"192.168.1.1:443": "192.168.1.1:443",
55-
"f921:7358:4510:3fce:ac2e:844:2a35:54e": "[f921:7358:4510:3fce:ac2e:844:2a35:54e]:8443",
56-
"[f921:7358:4510:3fce:ac2e:844:2a35:54e]": "[f921:7358:4510:3fce:ac2e:844:2a35:54e]:8443",
57-
"[f921:7358:4510:3fce:ac2e:844:2a35:54e]:": "[f921:7358:4510:3fce:ac2e:844:2a35:54e]:8443",
58-
"[f921:7358:4510:3fce:ac2e:844:2a35:54e]:8444": "[f921:7358:4510:3fce:ac2e:844:2a35:54e]:8444",
59-
}
60-
61-
for in, out := range cases {
62-
t.Run(in, func(t *testing.T) {
63-
assert.Equal(t, out, internalUtil.CanonicalNetworkAddress(in, ports.HTTPSDefaultPort))
64-
})
65-
}
66-
}
67-
68-
func TestIsAddressCovered(t *testing.T) {
69-
type testCase struct {
70-
address1 string
71-
address2 string
72-
covered bool
73-
}
74-
75-
cases := []testCase{
76-
{"127.0.0.1:8443", "127.0.0.1:8443", true},
77-
{"garbage", "127.0.0.1:8443", false},
78-
{"127.0.0.1:8444", "garbage", false},
79-
{"127.0.0.1:8444", "127.0.0.1:8443", false},
80-
{"127.0.0.1:8443", "0.0.0.0:8443", true},
81-
{"[::1]:8443", "0.0.0.0:8443", false},
82-
{":8443", "0.0.0.0:8443", false},
83-
{"127.0.0.1:8443", "[::]:8443", true},
84-
{"[::1]:8443", "[::]:8443", true},
85-
{"[::1]:8443", ":8443", true},
86-
{":8443", "[::]:8443", true},
87-
{"0.0.0.0:8443", "[::]:8443", true},
88-
{"10.30.0.8:8443", "[::]", true},
89-
{"localhost:8443", "127.0.0.1:8443", true},
90-
}
91-
92-
// Test some localhost cases too
93-
ips, err := net.LookupHost("localhost")
94-
if err == nil && len(ips) > 0 && ips[0] == "127.0.0.1" {
95-
cases = append(cases, testCase{"127.0.0.1:8443", "localhost:8443", true})
96-
}
97-
98-
ips, err = net.LookupHost("ip6-localhost")
99-
if err == nil && len(ips) > 0 && ips[0] == "::1" {
100-
cases = append(cases, testCase{"[::1]:8443", "ip6-localhost:8443", true})
101-
}
102-
103-
for _, c := range cases {
104-
t.Run(fmt.Sprintf("%s-%s", c.address1, c.address2), func(t *testing.T) {
105-
covered := internalUtil.IsAddressCovered(c.address1, c.address2)
106-
if c.covered {
107-
assert.True(t, covered)
108-
} else {
109-
assert.False(t, covered)
110-
}
111-
})
112-
}
113-
}
114-
115-
// This is a check against Go's stdlib to make sure that when listening to a port without specifying an address,
116-
// then an IPv6 wildcard is assumed.
117-
func TestListenImplicitIPv6Wildcard(t *testing.T) {
118-
listener, err := net.Listen("tcp", ":9999")
119-
require.NoError(t, err)
120-
defer func() { _ = listener.Close() }()
121-
122-
assert.Equal(t, "[::]:9999", listener.Addr().String())
123-
}

internal/util/network_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package util_test
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/lxc/incus/v6/internal/ports"
12+
internalUtil "github.com/lxc/incus/v6/internal/util"
13+
)
14+
15+
func TestCanonicalNetworkAddress(t *testing.T) {
16+
cases := map[string]string{
17+
"127.0.0.1": "127.0.0.1:8443",
18+
"127.0.0.1:": "127.0.0.1:8443",
19+
"foo.bar": "foo.bar:8443",
20+
"foo.bar:": "foo.bar:8443",
21+
"foo.bar:8444": "foo.bar:8444",
22+
"192.168.1.1:443": "192.168.1.1:443",
23+
"f921:7358:4510:3fce:ac2e:844:2a35:54e": "[f921:7358:4510:3fce:ac2e:844:2a35:54e]:8443",
24+
"[f921:7358:4510:3fce:ac2e:844:2a35:54e]": "[f921:7358:4510:3fce:ac2e:844:2a35:54e]:8443",
25+
"[f921:7358:4510:3fce:ac2e:844:2a35:54e]:": "[f921:7358:4510:3fce:ac2e:844:2a35:54e]:8443",
26+
"[f921:7358:4510:3fce:ac2e:844:2a35:54e]:8444": "[f921:7358:4510:3fce:ac2e:844:2a35:54e]:8444",
27+
}
28+
29+
for in, out := range cases {
30+
t.Run(in, func(t *testing.T) {
31+
assert.Equal(t, out, internalUtil.CanonicalNetworkAddress(in, ports.HTTPSDefaultPort))
32+
})
33+
}
34+
}
35+
36+
func TestIsAddressCovered(t *testing.T) {
37+
type testCase struct {
38+
address1 string
39+
address2 string
40+
covered bool
41+
}
42+
43+
cases := []testCase{
44+
{"127.0.0.1:8443", "127.0.0.1:8443", true},
45+
{"garbage", "127.0.0.1:8443", false},
46+
{"127.0.0.1:8444", "garbage", false},
47+
{"127.0.0.1:8444", "127.0.0.1:8443", false},
48+
{"127.0.0.1:8443", "0.0.0.0:8443", true},
49+
{"[::1]:8443", "0.0.0.0:8443", false},
50+
{":8443", "0.0.0.0:8443", false},
51+
{"127.0.0.1:8443", "[::]:8443", true},
52+
{"[::1]:8443", "[::]:8443", true},
53+
{"[::1]:8443", ":8443", true},
54+
{":8443", "[::]:8443", true},
55+
{"0.0.0.0:8443", "[::]:8443", true},
56+
{"10.30.0.8:8443", "[::]", true},
57+
{"localhost:8443", "127.0.0.1:8443", true},
58+
{"localhost:8443", "[::]:8443", true},
59+
}
60+
61+
// Test some localhost cases too
62+
ips, err := net.LookupHost("localhost")
63+
if err == nil && len(ips) > 0 && ips[0] == "127.0.0.1" {
64+
cases = append(cases, testCase{"127.0.0.1:8443", "localhost:8443", true})
65+
}
66+
67+
ips, err = net.LookupHost("ip6-localhost")
68+
if err == nil && len(ips) > 0 && ips[0] == "::1" {
69+
cases = append(cases, testCase{"[::1]:8443", "ip6-localhost:8443", true})
70+
}
71+
72+
for _, c := range cases {
73+
t.Run(fmt.Sprintf("%s-%s", c.address1, c.address2), func(t *testing.T) {
74+
covered := internalUtil.IsAddressCovered(c.address1, c.address2)
75+
if c.covered {
76+
assert.True(t, covered)
77+
} else {
78+
assert.False(t, covered)
79+
}
80+
})
81+
}
82+
}
83+
84+
// This is a check against Go's stdlib to make sure that when listening to a port without specifying an address,
85+
// then an IPv6 wildcard is assumed.
86+
func TestListenImplicitIPv6Wildcard(t *testing.T) {
87+
listener, err := net.Listen("tcp", ":9999")
88+
require.NoError(t, err)
89+
defer func() { _ = listener.Close() }()
90+
91+
assert.Equal(t, "[::]:9999", listener.Addr().String())
92+
}

0 commit comments

Comments
 (0)