Skip to content

Commit 1e604cb

Browse files
chris-sanderssmira
authored andcommitted
fix: don't set broadcast for /31 and /32 addresses
RFC 3021 defines /31 as point-to-point links without broadcast. /32 addresses are host routes that don't need broadcast addresses. Setting broadcast=IP for these prefixes confuses kernel routing decisions, causing packets to be incorrectly broadcast instead of routed through the gateway. This particularly affected single-node clusters with VIP addresses where the VIP's broadcast address could interfere with normal routing to the gateway. Signed-off-by: Chris Sanders <sanders.chris@gmail.com> Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
1 parent 65a6609 commit 1e604cb

File tree

3 files changed

+137
-30
lines changed

3 files changed

+137
-30
lines changed

internal/app/machined/pkg/controllers/network/address_spec.go

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import (
2020
"github.com/mdlayher/arp"
2121
"github.com/siderolabs/go-pointer"
2222
"go.uber.org/zap"
23-
"go4.org/netipx"
2423
"golang.org/x/sys/unix"
2524

25+
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network/internal/addressutil"
2626
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network/watch"
2727
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
2828
"github.com/siderolabs/talos/pkg/machinery/resources/network"
@@ -240,7 +240,7 @@ func (ctrl *AddressSpecController) syncAddress(ctx context.Context, r controller
240240
Attributes: &rtnetlink.AddressAttributes{
241241
Address: address.TypedSpec().Address.Addr().AsSlice(),
242242
Local: address.TypedSpec().Address.Addr().AsSlice(),
243-
Broadcast: broadcastAddr(address.TypedSpec().Address),
243+
Broadcast: addressutil.BroadcastAddr(address.TypedSpec().Address),
244244
Flags: uint32(address.TypedSpec().Flags),
245245
Priority: address.TypedSpec().Priority,
246246
},
@@ -300,31 +300,3 @@ func (ctrl *AddressSpecController) gratuitousARP(logger *zap.Logger, linkIndex u
300300

301301
return nil
302302
}
303-
304-
func broadcastAddr(addr netip.Prefix) net.IP {
305-
if !addr.Addr().Is4() {
306-
return nil
307-
}
308-
309-
ipnet := netipx.PrefixIPNet(addr)
310-
311-
ip := ipnet.IP.To4()
312-
if ip == nil {
313-
return nil
314-
}
315-
316-
mask := net.IP(ipnet.Mask).To4()
317-
318-
n := len(ip)
319-
if n != len(mask) {
320-
return nil
321-
}
322-
323-
out := make(net.IP, n)
324-
325-
for i := range n {
326-
out[i] = ip[i] | ^mask[i]
327-
}
328-
329-
return out
330-
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package addressutil
6+
7+
import (
8+
"net"
9+
"net/netip"
10+
11+
"go4.org/netipx"
12+
)
13+
14+
// BroadcastAddr calculates the broadcast address for the given IPv4 prefix.
15+
//
16+
// If the address is not IPv4 or the prefix length is 31 or 32, nil is returned.
17+
func BroadcastAddr(addr netip.Prefix) net.IP {
18+
if !addr.Addr().Is4() {
19+
return nil
20+
}
21+
22+
if addr.Bits() >= 31 {
23+
return nil
24+
}
25+
26+
ipnet := netipx.PrefixIPNet(addr)
27+
28+
ip := ipnet.IP.To4()
29+
if ip == nil {
30+
return nil
31+
}
32+
33+
mask := net.IP(ipnet.Mask).To4()
34+
35+
n := len(ip)
36+
if n != len(mask) {
37+
return nil
38+
}
39+
40+
out := make(net.IP, n)
41+
42+
for i := range n {
43+
out[i] = ip[i] | ^mask[i]
44+
}
45+
46+
return out
47+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package addressutil_test
6+
7+
import (
8+
"net"
9+
"net/netip"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network/internal/addressutil"
16+
)
17+
18+
func TestBroadcastAddr(t *testing.T) {
19+
t.Parallel()
20+
21+
tests := []struct {
22+
name string
23+
prefix string
24+
want string
25+
wantNil bool
26+
}{
27+
{
28+
name: "IPv4 /24 network",
29+
prefix: "10.255.255.231/24",
30+
want: "10.255.255.255",
31+
},
32+
{
33+
name: "IPv4 /16 network",
34+
prefix: "192.168.0.1/16",
35+
want: "192.168.255.255",
36+
},
37+
{
38+
name: "IPv4 /32 host route (VIP case)",
39+
prefix: "10.255.255.230/32",
40+
wantNil: true, // Should return nil, not set broadcast
41+
},
42+
{
43+
name: "Another /32 host route",
44+
prefix: "192.168.1.100/32",
45+
wantNil: true, // Should return nil, not set broadcast
46+
},
47+
{
48+
name: "IPv4 /31 point-to-point",
49+
prefix: "10.0.0.1/31",
50+
wantNil: true, // RFC 3021 - /31 is point-to-point, no broadcast
51+
},
52+
{
53+
name: "IPv4 /8 network",
54+
prefix: "10.0.0.1/8",
55+
want: "10.255.255.255",
56+
},
57+
{
58+
name: "IPv6 address (no broadcast)",
59+
prefix: "2001:db8::1/64",
60+
wantNil: true, // IPv6 doesn't have broadcast
61+
},
62+
{
63+
name: "IPv6 /128 address",
64+
prefix: "2001:db8::1/128",
65+
wantNil: true, // IPv6 doesn't have broadcast
66+
},
67+
}
68+
69+
for _, tt := range tests {
70+
t.Run(tt.name, func(t *testing.T) {
71+
t.Parallel()
72+
73+
prefix, err := netip.ParsePrefix(tt.prefix)
74+
require.NoError(t, err)
75+
76+
got := addressutil.BroadcastAddr(prefix)
77+
78+
if tt.wantNil {
79+
assert.Nil(t, got, "expected nil broadcast for %s", tt.prefix)
80+
} else {
81+
assert.NotNil(t, got, "expected broadcast address for %s", tt.prefix)
82+
83+
want := net.ParseIP(tt.want)
84+
assert.True(t, got.Equal(want), "expected %v, got %v for %s", want, got, tt.prefix)
85+
}
86+
})
87+
}
88+
}

0 commit comments

Comments
 (0)