Skip to content

Commit a6ac1b5

Browse files
authored
Merge pull request #153 from aojea/detect_gw
accurately detect default gateways
2 parents f9215fa + 52aa19a commit a6ac1b5

3 files changed

Lines changed: 434 additions & 23 deletions

File tree

internal/testutils/userns.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
Copyright The Kubernetes Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package testutils
18+
19+
import (
20+
"os"
21+
"os/exec"
22+
"strings"
23+
"sync"
24+
"syscall"
25+
"testing"
26+
)
27+
28+
var (
29+
isSupported bool
30+
checkOnce sync.Once
31+
)
32+
33+
// IsSupported checks if unprivileged user namespaces are enabled on the host system.
34+
// It caches the result after the first check.
35+
func IsSupported() bool {
36+
checkOnce.Do(func() {
37+
cmd := exec.Command("sleep", "1")
38+
39+
// Attempt to map ourselves to root inside the userns.
40+
cmd.SysProcAttr = &syscall.SysProcAttr{
41+
Cloneflags: syscall.CLONE_NEWUSER,
42+
UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getuid(), Size: 1}},
43+
GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getgid(), Size: 1}},
44+
}
45+
46+
if err := cmd.Start(); err != nil {
47+
return // User namespaces are likely restricted or not supported
48+
}
49+
50+
defer func() {
51+
_ = cmd.Process.Kill()
52+
_ = cmd.Wait()
53+
}()
54+
55+
isSupported = true
56+
})
57+
58+
return isSupported
59+
}
60+
61+
// Run executes the given test function inside a user namespace where the
62+
// current user is mapped to root. This provides capabilities to create network
63+
// namespaces and netfilter rules without running as actual root on the host.
64+
//
65+
// extraCloneflags can be used to request additional namespace types
66+
// (e.g., syscall.CLONE_NEWNET).
67+
func Run(t *testing.T, f func(t *testing.T), extraCloneflags ...uintptr) {
68+
const subprocessEnvKey = "GO_USERNS_SUBPROCESS_KEY"
69+
70+
// 1. If we are already inside the subprocess, run the actual test logic.
71+
if testIDString, ok := os.LookupEnv(subprocessEnvKey); ok && testIDString == "1" {
72+
t.Run("subprocess", f)
73+
return
74+
}
75+
76+
// 2. If we are on the host, verify support before attempting to spawn.
77+
if !IsSupported() {
78+
t.Skip("Unprivileged user namespaces are not supported on this system")
79+
}
80+
81+
// 3. Prepare the command to re-execute the current test binary.
82+
cmd := exec.Command(os.Args[0])
83+
cmd.Args = []string{os.Args[0], "-test.run=" + t.Name() + "$", "-test.v=true"}
84+
85+
for _, arg := range os.Args {
86+
if strings.HasPrefix(arg, "-test.testlogfile=") {
87+
cmd.Args = append(cmd.Args, arg)
88+
}
89+
}
90+
91+
cmd.Env = append(os.Environ(), subprocessEnvKey+"=1")
92+
// Include sbin in PATH, as some networking commands are not found otherwise.
93+
cmd.Env = append(cmd.Env, "PATH=/usr/local/sbin:/usr/sbin:/sbin:"+os.Getenv("PATH"))
94+
cmd.Stdin = os.Stdin
95+
96+
// 4. Configure the namespace clone flags.
97+
cloneflags := uintptr(syscall.CLONE_NEWUSER)
98+
for _, flag := range extraCloneflags {
99+
cloneflags |= flag
100+
}
101+
102+
// Map ourselves to root inside the new user namespace.
103+
cmd.SysProcAttr = &syscall.SysProcAttr{
104+
Cloneflags: cloneflags,
105+
UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getuid(), Size: 1}},
106+
GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getgid(), Size: 1}},
107+
}
108+
109+
// 5. Execute and capture output.
110+
out, err := cmd.CombinedOutput()
111+
112+
// Prepending a newline makes the nested test output much easier to read
113+
// in the standard `go test` log format.
114+
t.Logf("\n%s", out)
115+
if err != nil {
116+
t.Fatalf("Subprocess execution failed: %v", err)
117+
}
118+
}

pkg/inventory/net.go

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package inventory
1818

1919
import (
20+
"math"
21+
2022
"github.com/cilium/ebpf"
2123
"github.com/cilium/ebpf/link"
2224
"github.com/vishvananda/netlink"
@@ -28,53 +30,88 @@ import (
2830
)
2931

3032
// getDefaultGwInterfaces returns a set of interface names that are configured
31-
// as default gateways in the main routing table. It identifies these by querying
32-
// the main routing table for routes with an unspecified destination (0.0.0.0/0
33-
// for IPv4 or ::/0 for IPv6).
33+
// as active default gateways in the main routing table, respecting route metrics.
34+
// It identifies defaults as routes where Dst is nil (kernel default) or where
35+
// Dst is exactly 0.0.0.0/0 (IPv4) or ::/0 (IPv6).
3436
func getDefaultGwInterfaces() sets.Set[string] {
35-
interfaces := sets.Set[string]{}
37+
interfaces := make(sets.Set[string])
38+
3639
filter := &netlink.Route{
3740
Table: unix.RT_TABLE_MAIN,
3841
}
3942
routes, err := nlwrap.RouteListFiltered(netlink.FAMILY_ALL, filter, netlink.RT_FILTER_TABLE)
4043
if err != nil {
44+
klog.Errorf("Failed to list routes: %v", err)
4145
return interfaces
4246
}
4347

48+
minMetricV4 := math.MaxInt32
49+
minMetricV6 := math.MaxInt32
50+
51+
v4Interfaces := make(sets.Set[string])
52+
v6Interfaces := make(sets.Set[string])
53+
4454
for _, r := range routes {
4555
if r.Family != netlink.FAMILY_V4 && r.Family != netlink.FAMILY_V6 {
4656
continue
4757
}
4858

49-
if r.Dst != nil && !r.Dst.IP.IsUnspecified() {
50-
continue
51-
}
52-
53-
// no multipath
54-
if len(r.MultiPath) == 0 {
55-
if r.Gw == nil {
59+
if r.Dst != nil {
60+
ones, bits := r.Dst.Mask.Size()
61+
if !r.Dst.IP.IsUnspecified() || ones != 0 || (bits != 32 && bits != 128) {
5662
continue
5763
}
58-
intfLink, err := netlink.LinkByIndex(r.LinkIndex)
59-
if err != nil {
60-
klog.Infof("Failed to get interface link for route %v : %v", r, err)
61-
continue
62-
}
63-
interfaces.Insert(intfLink.Attrs().Name)
6464
}
6565

66-
for _, nh := range r.MultiPath {
67-
if nh.Gw == nil {
68-
continue
66+
metric := r.Priority
67+
68+
// 1. Gather all relevant link indices for this route
69+
var linkIndices []int
70+
if len(r.MultiPath) > 0 {
71+
for _, nh := range r.MultiPath {
72+
linkIndices = append(linkIndices, nh.LinkIndex)
6973
}
70-
intfLink, err := netlink.LinkByIndex(r.LinkIndex)
74+
} else {
75+
linkIndices = append(linkIndices, r.LinkIndex)
76+
}
77+
78+
// 2. Evaluate each link index against our metric trackers
79+
for _, linkIndex := range linkIndices {
80+
intfLink, err := netlink.LinkByIndex(linkIndex)
7181
if err != nil {
72-
klog.Infof("Failed to get interface link for route %v : %v", r, err)
82+
klog.Infof("Failed to get interface link for index %d: %v", linkIndex, err)
7383
continue
7484
}
75-
interfaces.Insert(intfLink.Attrs().Name)
85+
name := intfLink.Attrs().Name
86+
87+
if r.Family == netlink.FAMILY_V4 {
88+
if metric < minMetricV4 {
89+
minMetricV4 = metric
90+
v4Interfaces = make(sets.Set[string]) // Clear previous losers
91+
v4Interfaces.Insert(name)
92+
} else if metric == minMetricV4 {
93+
v4Interfaces.Insert(name) // ECMP tie: keep both
94+
}
95+
} else {
96+
if metric < minMetricV6 {
97+
minMetricV6 = metric
98+
v6Interfaces = make(sets.Set[string]) // Clear previous losers
99+
v6Interfaces.Insert(name)
100+
} else if metric == minMetricV6 {
101+
v6Interfaces.Insert(name) // ECMP tie: keep both
102+
}
103+
}
76104
}
77105
}
106+
107+
// Merge the winning IPv4 and IPv6 interfaces into the final set
108+
for k := range v4Interfaces {
109+
interfaces.Insert(k)
110+
}
111+
for k := range v6Interfaces {
112+
interfaces.Insert(k)
113+
}
114+
78115
return interfaces
79116
}
80117

0 commit comments

Comments
 (0)