Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions internal/testutils/userns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
Copyright The Kubernetes Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package testutils
Comment thread
aojea marked this conversation as resolved.

import (
"os"
"os/exec"
"strings"
"sync"
"syscall"
"testing"
)

var (
isSupported bool
checkOnce sync.Once
)

// IsSupported checks if unprivileged user namespaces are enabled on the host system.
// It caches the result after the first check.
func IsSupported() bool {
checkOnce.Do(func() {
cmd := exec.Command("sleep", "1")
Comment thread
aojea marked this conversation as resolved.

// Attempt to map ourselves to root inside the userns.
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUSER,
UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getuid(), Size: 1}},
GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getgid(), Size: 1}},
}

if err := cmd.Start(); err != nil {
return // User namespaces are likely restricted or not supported
}

defer func() {
_ = cmd.Process.Kill()
_ = cmd.Wait()
}()

isSupported = true
})

return isSupported
}

// Run executes the given test function inside a user namespace where the
// current user is mapped to root. This provides capabilities to create network
// namespaces and netfilter rules without running as actual root on the host.
//
// extraCloneflags can be used to request additional namespace types
// (e.g., syscall.CLONE_NEWNET).
func Run(t *testing.T, f func(t *testing.T), extraCloneflags ...uintptr) {
const subprocessEnvKey = "GO_USERNS_SUBPROCESS_KEY"

// 1. If we are already inside the subprocess, run the actual test logic.
if testIDString, ok := os.LookupEnv(subprocessEnvKey); ok && testIDString == "1" {
t.Run("subprocess", f)
return
}

// 2. If we are on the host, verify support before attempting to spawn.
if !IsSupported() {
t.Skip("Unprivileged user namespaces are not supported on this system")
}

// 3. Prepare the command to re-execute the current test binary.
cmd := exec.Command(os.Args[0])
cmd.Args = []string{os.Args[0], "-test.run=" + t.Name() + "$", "-test.v=true"}

for _, arg := range os.Args {
if strings.HasPrefix(arg, "-test.testlogfile=") {
Comment thread
aojea marked this conversation as resolved.
cmd.Args = append(cmd.Args, arg)
}
}

cmd.Env = append(os.Environ(), subprocessEnvKey+"=1")
// Include sbin in PATH, as some networking commands are not found otherwise.
cmd.Env = append(cmd.Env, "PATH=/usr/local/sbin:/usr/sbin:/sbin:"+os.Getenv("PATH"))
cmd.Stdin = os.Stdin

// 4. Configure the namespace clone flags.
cloneflags := uintptr(syscall.CLONE_NEWUSER)
for _, flag := range extraCloneflags {
cloneflags |= flag
}

// Map ourselves to root inside the new user namespace.
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: cloneflags,
UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getuid(), Size: 1}},
GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getgid(), Size: 1}},
}

// 5. Execute and capture output.
out, err := cmd.CombinedOutput()

// Prepending a newline makes the nested test output much easier to read
// in the standard `go test` log format.
t.Logf("\n%s", out)
if err != nil {
t.Fatalf("Subprocess execution failed: %v", err)
}
}
83 changes: 60 additions & 23 deletions pkg/inventory/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package inventory

import (
"math"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/vishvananda/netlink"
Expand All @@ -28,53 +30,88 @@ import (
)

// getDefaultGwInterfaces returns a set of interface names that are configured
// as default gateways in the main routing table. It identifies these by querying
// the main routing table for routes with an unspecified destination (0.0.0.0/0
// for IPv4 or ::/0 for IPv6).
// as active default gateways in the main routing table, respecting route metrics.
Comment thread
aojea marked this conversation as resolved.
// It identifies defaults as routes where Dst is nil (kernel default) or where
// Dst is exactly 0.0.0.0/0 (IPv4) or ::/0 (IPv6).
func getDefaultGwInterfaces() sets.Set[string] {
interfaces := sets.Set[string]{}
interfaces := make(sets.Set[string])

filter := &netlink.Route{
Table: unix.RT_TABLE_MAIN,
}
routes, err := nlwrap.RouteListFiltered(netlink.FAMILY_ALL, filter, netlink.RT_FILTER_TABLE)
if err != nil {
klog.Errorf("Failed to list routes: %v", err)
return interfaces
}

minMetricV4 := math.MaxInt32
minMetricV6 := math.MaxInt32

v4Interfaces := make(sets.Set[string])
v6Interfaces := make(sets.Set[string])

for _, r := range routes {
if r.Family != netlink.FAMILY_V4 && r.Family != netlink.FAMILY_V6 {
continue
}

if r.Dst != nil && !r.Dst.IP.IsUnspecified() {
continue
}

// no multipath
if len(r.MultiPath) == 0 {
if r.Gw == nil {
if r.Dst != nil {
ones, bits := r.Dst.Mask.Size()
if !r.Dst.IP.IsUnspecified() || ones != 0 || (bits != 32 && bits != 128) {
continue
}
intfLink, err := netlink.LinkByIndex(r.LinkIndex)
if err != nil {
klog.Infof("Failed to get interface link for route %v : %v", r, err)
continue
}
interfaces.Insert(intfLink.Attrs().Name)
}

for _, nh := range r.MultiPath {
if nh.Gw == nil {
continue
metric := r.Priority

// 1. Gather all relevant link indices for this route
var linkIndices []int
if len(r.MultiPath) > 0 {
for _, nh := range r.MultiPath {
linkIndices = append(linkIndices, nh.LinkIndex)
}
intfLink, err := netlink.LinkByIndex(r.LinkIndex)
} else {
linkIndices = append(linkIndices, r.LinkIndex)
}

// 2. Evaluate each link index against our metric trackers
for _, linkIndex := range linkIndices {
intfLink, err := netlink.LinkByIndex(linkIndex)
if err != nil {
klog.Infof("Failed to get interface link for route %v : %v", r, err)
klog.Infof("Failed to get interface link for index %d: %v", linkIndex, err)
continue
}
interfaces.Insert(intfLink.Attrs().Name)
name := intfLink.Attrs().Name

if r.Family == netlink.FAMILY_V4 {
if metric < minMetricV4 {
minMetricV4 = metric
v4Interfaces = make(sets.Set[string]) // Clear previous losers
v4Interfaces.Insert(name)
} else if metric == minMetricV4 {
v4Interfaces.Insert(name) // ECMP tie: keep both
}
} else {
if metric < minMetricV6 {
minMetricV6 = metric
v6Interfaces = make(sets.Set[string]) // Clear previous losers
v6Interfaces.Insert(name)
} else if metric == minMetricV6 {
v6Interfaces.Insert(name) // ECMP tie: keep both
}
}
}
}

// Merge the winning IPv4 and IPv6 interfaces into the final set
for k := range v4Interfaces {
interfaces.Insert(k)
}
for k := range v6Interfaces {
interfaces.Insert(k)
}

return interfaces
}

Expand Down
Loading
Loading