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 3, 2024
1 parent 0cde08d commit 5d758af
Show file tree
Hide file tree
Showing 7 changed files with 435 additions and 136 deletions.
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,73 @@
// 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"
"net/netip"

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

// 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 {
// first, compare address families
if c := cmp.Compare(family(a), family(b)); c != 0 {
return c
}

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

// if one prefix contains another, the more specific one should come first
aContainsB := a.Contains(b.Addr())
bContainsA := b.Contains(a.Addr())

// if both prefixes contain each other, proceed to compare addresses/prefix lengths
switch {
case aContainsB && !bContainsA:
return 1
case !aContainsB && bContainsA:
return -1
}

// compare addresses, they are not equal
return a.Addr().Compare(b.Addr())
}

// CompareAddressStatuses compares two address statuses with the prefix comparison func.
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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 (
"math/rand/v2"
"net/netip"
"slices"
"testing"

"github.com/siderolabs/gen/xslices"
"github.com/stretchr/testify/assert"

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

func toNetip(prefixes ...string) []netip.Prefix {
return xslices.Map(prefixes, netip.MustParsePrefix)
}

func toString(prefixes []netip.Prefix) []string {
return xslices.Map(prefixes, netip.Prefix.String)
}

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

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

in []netip.Prefix

outLegacy []netip.Prefix
outNew []netip.Prefix
}{
{
name: "ipv4",

in: toNetip("10.3.4.1/24", "10.3.4.5/24", "10.3.4.5/32", "1.2.3.4/26", "192.168.35.11/24", "192.168.36.10/24"),

outLegacy: toNetip("1.2.3.4/26", "10.3.4.1/24", "10.3.4.5/24", "10.3.4.5/32", "192.168.35.11/24", "192.168.36.10/24"),
outNew: toNetip("1.2.3.4/26", "10.3.4.5/32", "10.3.4.1/24", "10.3.4.5/24", "192.168.35.11/24", "192.168.36.10/24"),
},
{
name: "ipv6",

in: toNetip("2001:db8::1/64", "2001:db8::1/128", "2001:db8::2/64", "2001:db8::2/128", "2001:db8::3/64", "2001:db8::3/128"),

outLegacy: toNetip("2001:db8::1/64", "2001:db8::1/128", "2001:db8::2/64", "2001:db8::2/128", "2001:db8::3/64", "2001:db8::3/128"),
outNew: toNetip("2001:db8::1/128", "2001:db8::2/128", "2001:db8::3/128", "2001:db8::1/64", "2001:db8::2/64", "2001:db8::3/64"),
},
{
name: "mixed",

in: toNetip("fd01:cafe::5054:ff:fe1f:c7bd/64", "fd01:cafe::f14c:9fa1:8496:557f/128", "192.168.3.4/24", "10.5.0.0/16"),

outLegacy: toNetip("10.5.0.0/16", "192.168.3.4/24", "fd01:cafe::5054:ff:fe1f:c7bd/64", "fd01:cafe::f14c:9fa1:8496:557f/128"),
outNew: toNetip("10.5.0.0/16", "192.168.3.4/24", "fd01:cafe::f14c:9fa1:8496:557f/128", "fd01:cafe::5054:ff:fe1f:c7bd/64"),
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

// add more randomness to ensure the sorting is stable
in := slices.Clone(test.in)
rand.Shuffle(len(in), func(i, j int) { in[i], in[j] = in[j], in[i] })

legacy := slices.Clone(in)
slices.SortFunc(legacy, addressutil.ComparePrefixesLegacy)

assert.Equal(t, test.outLegacy, legacy, "expected %q but got %q", toString(test.outLegacy), toString(legacy))

newer := slices.Clone(in)
slices.SortFunc(newer, addressutil.ComparePrefixNew)

assert.Equal(t, test.outNew, newer, "expected %q but got %q", toString(test.outNew), toString(newer))
})
}
}
Loading

0 comments on commit 5d758af

Please sign in to comment.