Skip to content

Commit

Permalink
feat: implement new address sorting algorithm
Browse files Browse the repository at this point in the history
Fixes #9725

See #9749

Signed-off-by: Andrey Smirnov <[email protected]>
  • Loading branch information
smira committed Dec 4, 2024
1 parent 77e9db4 commit 5cc4c60
Show file tree
Hide file tree
Showing 38 changed files with 1,893 additions and 818 deletions.
8 changes: 8 additions & 0 deletions .kres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,14 @@ spec:
VIA_MAINTENANCE_MODE: true
WITH_DISK_ENCRYPTION: true
IMAGE_REGISTRY: registry.dev.siderolabs.io
- name: e2e-node-address-v2
command: e2e-qemu
withSudo: true
environment:
GITHUB_STEP_NAME: ${{ github.job}}-e2e-disk-image
SHORT_INTEGRATION_TEST: yes
WITH_CONFIG_PATCH: "@hack/test/patches/node-address-v2.yaml"
IMAGE_REGISTRY: registry.dev.siderolabs.io
- name: save-talos-logs
conditions:
- always
Expand Down
6 changes: 6 additions & 0 deletions api/resource/definitions/enums/enums.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ enum NethelpersAddressFlag {
ADDRESS_STABLE_PRIVACY = 2048;
}

// NethelpersAddressSortAlgorithm is an internal address sorting algorithm.
enum NethelpersAddressSortAlgorithm {
ADDRESS_SORT_ALGORITHM_V1 = 0;
ADDRESS_SORT_ALGORITHM_V2 = 1;
}

// NethelpersADSelect is ADSelect.
enum NethelpersADSelect {
AD_SELECT_STABLE = 0;
Expand Down
6 changes: 6 additions & 0 deletions api/resource/definitions/network/network.proto
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,15 @@ message NodeAddressFilterSpec {
repeated common.NetIPPrefix exclude_subnets = 2;
}

// NodeAddressSortAlgorithmSpec describes a filter for NodeAddresses.
message NodeAddressSortAlgorithmSpec {
talos.resource.definitions.enums.NethelpersAddressSortAlgorithm algorithm = 1;
}

// NodeAddressSpec describes a set of node addresses.
message NodeAddressSpec {
repeated common.NetIPPrefix addresses = 1;
talos.resource.definitions.enums.NethelpersAddressSortAlgorithm sort_algorithm = 2;
}

// OperatorSpecSpec describes DNS resolvers.
Expand Down
13 changes: 13 additions & 0 deletions hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ options ndots:5
Talos now supports providing a local [Image Cache](https://www.talos.dev/v1.9/talos-guides/configuration/image-cache/) for container images.
"""

[notes.node-address-sort]
title = "Node Address Sort"
description = """\
Talos supports new experimental address sort algorithm for `NodeAddress` which are used to pick up default addresses for kubelet, etcd, etc.
It can be enabled with the following config patch:
```yaml
machine:
features:
nodeAddressSortAlgorithm: v2
"""

[make_deps]

[make_deps.tools]
Expand Down
3 changes: 3 additions & 0 deletions hack/test/patches/node-address-v2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
machine:
features:
nodeAddressSortAlgorithm: v2
22 changes: 22 additions & 0 deletions internal/app/machined/pkg/controllers/k8s/nodeip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,28 @@ func (suite *NodeIPSuite) TestReconcileNoMatch() {
})
}

func (suite *NodeIPSuite) TestReconcileIPv6Denies() {
cfg := k8s.NewNodeIPConfig(k8s.NamespaceName, k8s.KubeletID)
cfg.TypedSpec().ValidSubnets = []string{"::/0", "!fd01:cafe::f14c:9fa1:8496:557f/128"}
suite.Require().NoError(suite.State().Create(suite.Ctx(), cfg))

addresses := network.NewNodeAddress(
network.NamespaceName,
network.FilteredNodeAddressID(network.NodeAddressRoutedID, k8s.NodeAddressFilterNoK8s),
)

addresses.TypedSpec().Addresses = []netip.Prefix{
netip.MustParsePrefix("fd01:cafe::f14c:9fa1:8496:557f/128"),
netip.MustParsePrefix("fd01:cafe::5054:ff:fe1f:c7bd/64"),
}

suite.Require().NoError(suite.State().Create(suite.Ctx(), addresses))

rtestutils.AssertResources(suite.Ctx(), suite.T(), suite.State(), []resource.ID{k8s.KubeletID}, func(nodeIP *k8s.NodeIP, asrt *assert.Assertions) {
asrt.Equal("[fd01:cafe::5054:ff:fe1f:c7bd]", fmt.Sprintf("%s", nodeIP.TypedSpec().Addresses))
})
}

func TestNodeIPSuite(t *testing.T) {
t.Parallel()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Package addressutil contains helpers working with addresses.
package addressutil

import "net/netip"

// DeduplicateIPPrefixes removes duplicates from the given list of prefixes.
//
// The input list must be sorted.
// DeduplicateIPPrefixes performs in-place deduplication.
func DeduplicateIPPrefixes(in []netip.Prefix) []netip.Prefix {
// assumes that current is sorted
n := 0

var prev netip.Prefix

for _, x := range in {
if prev != x {
in[n] = x
n++
}

prev = x
}

return in[:n]
}

// FilterIPs filters the given list of IP prefixes based on the given include and exclude subnets.
//
// If includeSubnets is not empty, only IPs that are in one of the subnets are included.
// If excludeSubnets is not empty, IPs that are in one of the subnets are excluded.
func FilterIPs(addrs []netip.Prefix, includeSubnets, excludeSubnets []netip.Prefix) []netip.Prefix {
result := make([]netip.Prefix, 0, len(addrs))

outer:
for _, ip := range addrs {
if len(includeSubnets) > 0 {
matchesAny := false

for _, subnet := range includeSubnets {
if subnet.Contains(ip.Addr()) {
matchesAny = true

break
}
}

if !matchesAny {
continue outer
}
}

for _, subnet := range excludeSubnets {
if subnet.Contains(ip.Addr()) {
continue outer
}
}

result = append(result, ip)
}

return result
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package addressutil_test

import (
"net/netip"
"testing"

"github.com/stretchr/testify/assert"

"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network/internal/addressutil"
)

func TestDeduplicateIPPrefixes(t *testing.T) {
t.Parallel()

for _, test := range []struct {
name string
in []netip.Prefix

out []netip.Prefix
}{
{
name: "empty",
},
{
name: "single",
in: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32"), netip.MustParsePrefix("1.2.3.4/32")},

out: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32")},
},
{
name: "many",
in: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32"), netip.MustParsePrefix("1.2.3.4/24"), netip.MustParsePrefix("2000::aebc/64"), netip.MustParsePrefix("2000::aebc/64")},

out: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32"), netip.MustParsePrefix("1.2.3.4/24"), netip.MustParsePrefix("2000::aebc/64")},
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

got := addressutil.DeduplicateIPPrefixes(test.in)

assert.Equal(t, test.out, got)
})
}
}

// TestFilterIPs tests the FilterIPs function.
func TestFilterIPs(t *testing.T) {
t.Parallel()

for _, test := range []struct {
name string

in []netip.Prefix
include []netip.Prefix
exclude []netip.Prefix

out []netip.Prefix
}{
{
name: "empty filters",

in: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32"), netip.MustParsePrefix("2000::aebc/64")},

out: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32"), netip.MustParsePrefix("2000::aebc/64")},
},
{
name: "v4 only",

in: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32"), netip.MustParsePrefix("2000::aebc/64")},
include: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},

out: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32")},
},
{
name: "v6 only",

in: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32"), netip.MustParsePrefix("2000::aebc/64")},
exclude: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},

out: []netip.Prefix{netip.MustParsePrefix("2000::aebc/64")},
},
{
name: "include and exclude",

in: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32"), netip.MustParsePrefix("3.4.5.6/24"), netip.MustParsePrefix("2000::aebc/64")},
include: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
exclude: []netip.Prefix{netip.MustParsePrefix("3.0.0.0/8")},

out: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32")},
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

got := addressutil.FilterIPs(test.in, test.include, test.exclude)

assert.Equal(t, test.out, got)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package addressutil

import (
"cmp"
"fmt"
"net/netip"

"github.com/siderolabs/talos/pkg/machinery/nethelpers"
"github.com/siderolabs/talos/pkg/machinery/resources/network"
)

// CompareByAlgorithm returns a comparison function based on the given algorithm.
func CompareByAlgorithm(algorithm nethelpers.AddressSortAlgorithm) func(a, b netip.Prefix) int {
switch algorithm {
case nethelpers.AddressSortAlgorithmV1:
return ComparePrefixesLegacy
case nethelpers.AddressSortAlgorithmV2:
return ComparePrefixNew
}

panic(fmt.Sprintf("unknown address sort algorithm: %s", algorithm))
}

// ComparePrefixesLegacy is the old way to sort prefixes.
//
// It only compares addresses and does not take prefix length into account.
func ComparePrefixesLegacy(a, b netip.Prefix) int {
if c := a.Addr().Compare(b.Addr()); c != 0 {
return c
}

// note: this was missing in the previous implementation, but this makes sorting stable
return cmp.Compare(a.Bits(), b.Bits())
}

func family(a netip.Prefix) int {
if a.Addr().Is4() {
return 4
}

return 6
}

// ComparePrefixNew compares two prefixes by address family, address, and prefix length.
//
// It prefers more specific prefixes.
func ComparePrefixNew(a, b netip.Prefix) int {
// (1): first, compare address families
if c := cmp.Compare(family(a), family(b)); c != 0 {
return c
}

// (2): if addresses are equal, Contains will report that one prefix contains the other, so compare prefix lengths
if a.Addr() == b.Addr() {
return -cmp.Compare(a.Bits(), b.Bits())
}

// (3): if one prefix contains another, the more specific one should come first
// but if both prefixes contain each other, proceed to compare addresses
aContainsB := a.Contains(b.Addr())
bContainsA := b.Contains(a.Addr())

switch {
case aContainsB && !bContainsA:
return 1
case !aContainsB && bContainsA:
return -1
}

// (4): compare addresses, they are not equal at this point (see (2))
return a.Addr().Compare(b.Addr())
}

// CompareAddressStatuses compares two address statuses with the prefix comparison func.
//
// The comparison of AddressStatuses sorts by link name and then by address.
func CompareAddressStatuses(comparePrefixes func(a, b netip.Prefix) int) func(a, b *network.AddressStatus) int {
return func(a, b *network.AddressStatus) int {
if c := cmp.Compare(a.TypedSpec().LinkName, b.TypedSpec().LinkName); c != 0 {
return c
}

return comparePrefixes(a.TypedSpec().Address, b.TypedSpec().Address)
}
}
Loading

0 comments on commit 5cc4c60

Please sign in to comment.