diff --git a/driver/bond_live_test.go b/driver/bond_live_test.go index ffd8d8b..14b48c3 100644 --- a/driver/bond_live_test.go +++ b/driver/bond_live_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/jsimonetti/rtnetlink/v2" + "github.com/jsimonetti/rtnetlink/v2/internal/testutils" "github.com/mdlayher/netlink" ) @@ -34,23 +35,15 @@ func bondSlaveT(d rtnetlink.LinkDriver) *BondSlave { } func TestBond(t *testing.T) { - // establish a netlink connection conn, err := rtnetlink.Dial(nil) if err != nil { t.Fatalf("failed to establish netlink socket: %v", err) } defer conn.Close() - bns, clean, err := createNS("bns1") + connNS, err := rtnetlink.Dial(&netlink.Config{NetNS: testutils.NetNS(t)}) if err != nil { - t.Fatal(err) - } - defer clean() - - // use ns for testing arp ip targets - connNS, err := rtnetlink.Dial(&netlink.Config{NetNS: int(bns.Value())}) - if err != nil { - t.Fatalf("failed to establish netlink socket to ns nkns: %v", err) + t.Fatalf("failed to establish netlink socket to netns: %v", err) } defer connNS.Close() diff --git a/driver/driver_test.go b/driver/driver_test.go index 054571d..63ce4db 100644 --- a/driver/driver_test.go +++ b/driver/driver_test.go @@ -4,43 +4,10 @@ package driver import ( - "bytes" - "fmt" - "os/exec" - "testing" - "github.com/jsimonetti/rtnetlink/v2" "golang.org/x/sys/unix" ) -func getKernelVersion() (kernel, major, minor int, err error) { - var uname unix.Utsname - if err := unix.Uname(&uname); err != nil { - return 0, 0, 0, err - } - - end := bytes.IndexByte(uname.Release[:], 0) - versionStr := uname.Release[:end] - - if count, _ := fmt.Sscanf(string(versionStr), "%d.%d.%d", &kernel, &major, &minor); count < 2 { - err = fmt.Errorf("failed to parse kernel version from: %q", string(versionStr)) - } - return -} - -// kernelMinReq checks if the runtime kernel is sufficient -// for the test -func kernelMinReq(t *testing.T, kernel, major int) { - k, m, _, err := getKernelVersion() - if err != nil { - t.Fatalf("failed to get host kernel version: %v", err) - } - if k < kernel || k == kernel && m < major { - t.Skipf("host kernel (%d.%d) does not meet test's minimum required version: (%d.%d)", - k, m, kernel, major) - } -} - // setupInterface create a interface for testing func setupInterface(conn *rtnetlink.Conn, name string, index, master uint32, driver rtnetlink.LinkDriver) error { attrs := &rtnetlink.LinkAttributes{ @@ -74,30 +41,3 @@ func getInterface(conn *rtnetlink.Conn, index uint32) (*rtnetlink.LinkMessage, e } return &interf, err } - -// creates a network namespace by utilizing ip commandline tool -// returns NetNS and clean function -func createNS(name string) (*rtnetlink.NetNS, func(), error) { - cmdPath, err := exec.LookPath("ip") - if err != nil { - return nil, nil, fmt.Errorf("getting ip command path failed, %w", err) - } - _, err = exec.Command(cmdPath, "netns", "add", name).Output() - if err != nil { - return nil, nil, fmt.Errorf("ip netns add %s, failed: %w", name, err) - } - - ns, err := rtnetlink.NewNetNS(name) - if err != nil { - return nil, nil, fmt.Errorf("reading ns %s, failed: %w", name, err) - } - return ns, func() { - if err := ns.Close(); err != nil { - fmt.Printf("closing ns file failed: %v", err) - } - _, err := exec.Command(cmdPath, "netns", "del", name).Output() - if err != nil { - fmt.Printf("removing netns %s failed, %v", name, err) - } - }, nil -} diff --git a/driver/netkit_live_test.go b/driver/netkit_live_test.go index 9b67a9a..9df03ad 100644 --- a/driver/netkit_live_test.go +++ b/driver/netkit_live_test.go @@ -8,30 +8,23 @@ import ( "github.com/google/go-cmp/cmp" "github.com/jsimonetti/rtnetlink/v2" + "github.com/jsimonetti/rtnetlink/v2/internal/testutils" "github.com/mdlayher/netlink" ) func TestNetkit(t *testing.T) { - kernelMinReq(t, 6, 7) + testutils.SkipOnOldKernel(t, "6.7", "netkit support") - // establish a netlink connection conn, err := rtnetlink.Dial(nil) if err != nil { t.Fatalf("failed to establish netlink socket: %v", err) } defer conn.Close() - // create netns - nkns, clean, err := createNS("nkns1") + ns := testutils.NetNS(t) + connNS, err := rtnetlink.Dial(&netlink.Config{NetNS: ns}) if err != nil { - t.Fatal(err) - } - defer clean() - - // establish a netlink connection with netns - connNS, err := rtnetlink.Dial(&netlink.Config{NetNS: int(nkns.Value())}) - if err != nil { - t.Fatalf("failed to establish netlink socket to ns nkns: %v", err) + t.Fatalf("failed to establish netlink socket to netns: %v", err) } defer connNS.Close() @@ -110,7 +103,7 @@ func TestNetkit(t *testing.T) { Index: ifPeerIndex, Attributes: &rtnetlink.LinkAttributes{ Name: "nke", - NetNS: nkns, + NetNS: rtnetlink.NetNSForFD(uint32(ns)), }, }, }, diff --git a/driver/veth_live_test.go b/driver/veth_live_test.go index 92e00be..d58748f 100644 --- a/driver/veth_live_test.go +++ b/driver/veth_live_test.go @@ -7,28 +7,21 @@ import ( "testing" "github.com/jsimonetti/rtnetlink/v2" + "github.com/jsimonetti/rtnetlink/v2/internal/testutils" "github.com/mdlayher/netlink" ) func TestVeth(t *testing.T) { - // establish a netlink connection conn, err := rtnetlink.Dial(nil) if err != nil { t.Fatalf("failed to establish netlink socket: %v", err) } defer conn.Close() - // create netns - vtns, clean, err := createNS("vtns1") + ns := testutils.NetNS(t) + connNS, err := rtnetlink.Dial(&netlink.Config{NetNS: ns}) if err != nil { - t.Fatal(err) - } - defer clean() - - // establish a netlink connection with netns - connNS, err := rtnetlink.Dial(&netlink.Config{NetNS: int(vtns.Value())}) - if err != nil { - t.Fatalf("failed to establish netlink socket to ns vtns1: %v", err) + t.Fatalf("failed to establish netlink socket to netns: %v", err) } defer connNS.Close() @@ -74,7 +67,7 @@ func TestVeth(t *testing.T) { Index: ifPeerIndex, Attributes: &rtnetlink.LinkAttributes{ Name: "vte", - NetNS: vtns, + NetNS: rtnetlink.NetNSForFD(uint32(ns)), }, }, }, diff --git a/internal/testutils/netns.go b/internal/testutils/netns.go new file mode 100644 index 0000000..4da5cb8 --- /dev/null +++ b/internal/testutils/netns.go @@ -0,0 +1,55 @@ +package testutils + +import ( + "fmt" + "os" + "runtime" + "testing" + + "github.com/jsimonetti/rtnetlink/v2/internal/unix" + "golang.org/x/sync/errgroup" +) + +// NetNS returns a file descriptor to a new network namespace. +// The netns handle is automatically closed as part of test cleanup. +func NetNS(tb testing.TB) int { + tb.Helper() + + var ns *os.File + var eg errgroup.Group + eg.Go(func() error { + // Lock the new goroutine to its OS thread. Never unlock the goroutine so + // the thread dies when the goroutine ends to avoid having to restore the + // thread's netns. + runtime.LockOSThread() + + // Move the current thread to a new network namespace. + if err := unix.Unshare(unix.CLONE_NEWNET); err != nil { + return fmt.Errorf("unsharing netns: %w", err) + } + + f, err := os.OpenFile(fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid()), + unix.O_RDONLY|unix.O_CLOEXEC, 0) + if err != nil { + return fmt.Errorf("opening netns handle: %w", err) + } + + // Store a namespace reference in the outer scope. + ns = f + + return nil + }) + + if err := eg.Wait(); err != nil { + tb.Fatal(err) + } + + tb.Cleanup(func() { + // Maintain a reference to the namespace until the end of the test, where + // the handle will close automatically and the namespace potentially + // disappears if there are no other references (veth/netkit peers, ..) to it. + ns.Close() + }) + + return int(ns.Fd()) +} diff --git a/internal/testutils/version.go b/internal/testutils/version.go new file mode 100644 index 0000000..0eef1b2 --- /dev/null +++ b/internal/testutils/version.go @@ -0,0 +1,41 @@ +package testutils + +import ( + "bytes" + "fmt" + "testing" + + "golang.org/x/sys/unix" +) + +func getKernelVersion(tb testing.TB) (maj, min, patch uint32) { + tb.Helper() + + var uname unix.Utsname + if err := unix.Uname(&uname); err != nil { + tb.Fatalf("getting uname: %s", err) + } + + end := bytes.IndexByte(uname.Release[:], 0) + versionStr := uname.Release[:end] + + if count, _ := fmt.Sscanf(string(versionStr), "%d.%d.%d", &maj, &min, &patch); count < 2 { + tb.Fatalf("failed to parse kernel version from %s", string(versionStr)) + } + return +} + +// SkipOnOldKernel skips the test if the host's kernel is lower than the given +// x.y target version. +func SkipOnOldKernel(tb testing.TB, target, reason string) { + maj, min, _ := getKernelVersion(tb) + + var maj_t, min_t, patch_t uint32 + if count, _ := fmt.Sscanf(target, "%d.%d.%d", &maj_t, &min_t, &patch_t); count < 2 { + tb.Fatalf("failed to parse target version from %s", target) + } + + if maj < maj_t || maj == maj_t && min < min_t { + tb.Skipf("host kernel (%d.%d) too old (minimum %d.%d): %s", maj, min, maj_t, min_t, reason) + } +} diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go index 075b219..008597a 100644 --- a/internal/unix/types_linux.go +++ b/internal/unix/types_linux.go @@ -209,4 +209,10 @@ const ( NETKIT_REDIRECT = linux.NETKIT_REDIRECT NETKIT_L2 = linux.NETKIT_L2 NETKIT_L3 = linux.NETKIT_L3 + CLONE_NEWNET = linux.CLONE_NEWNET + O_RDONLY = linux.O_RDONLY + O_CLOEXEC = linux.O_CLOEXEC ) + +var Gettid = linux.Gettid +var Unshare = linux.Unshare diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go index e0aa11a..8a30d79 100644 --- a/internal/unix/types_other.go +++ b/internal/unix/types_other.go @@ -205,4 +205,15 @@ const ( NETKIT_REDIRECT = 0x7 NETKIT_L2 = 0x0 NETKIT_L3 = 0x1 + CLONE_NEWNET = 0x40000000 + O_RDONLY = 0x0 + O_CLOEXEC = 0x80000 ) + +func Unshare(_ int) error { + return nil +} + +func Gettid() int { + return 0 +} diff --git a/link.go b/link.go index 634d8ef..4400d5a 100644 --- a/link.go +++ b/link.go @@ -403,7 +403,7 @@ func (a *LinkAttributes) encode(ae *netlink.AttributeEncoder) error { } if a.NetNS != nil { - ae.Uint32(a.NetNS.Type(), a.NetNS.Value()) + ae.Uint32(a.NetNS.value()) } return nil @@ -771,8 +771,8 @@ func (xdp *LinkXDP) encode(ae *netlink.AttributeEncoder) error { ae.Int32(unix.IFLA_XDP_FD, xdp.FD) ae.Int32(unix.IFLA_XDP_EXPECTED_FD, xdp.ExpectedFD) ae.Uint32(unix.IFLA_XDP_FLAGS, xdp.Flags) - // XDP_ATtACHED and XDP_PROG_ID are things that only can return from the kernel, - // not be send, so we don't encode them. - // source: https://elixir.bootlin.com/linux/v5.10.15/source/net/core/rtnetlink.c#L2894 + // XDP_ATTACHED and XDP_PROG_ID are things that can only be returned by the + // kernel, so we don't encode them. source: + // https://elixir.bootlin.com/linux/v5.10.15/source/net/core/rtnetlink.c#L2894 return nil } diff --git a/link_live_test.go b/link_live_test.go index 34fb0c7..e9b4f86 100644 --- a/link_live_test.go +++ b/link_live_test.go @@ -4,72 +4,23 @@ package rtnetlink import ( - "bytes" - "fmt" "testing" "github.com/cilium/ebpf" "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/rlimit" + "github.com/jsimonetti/rtnetlink/v2/internal/testutils" + "github.com/mdlayher/netlink" "golang.org/x/sys/unix" ) -func getKernelVersion() (kernel, major, minor int, err error) { - var uname unix.Utsname - if err := unix.Uname(&uname); err != nil { - return 0, 0, 0, err - } - - end := bytes.IndexByte(uname.Release[:], 0) - versionStr := uname.Release[:end] - - if count, _ := fmt.Sscanf(string(versionStr), "%d.%d.%d", &kernel, &major, &minor); count < 2 { - err = fmt.Errorf("failed to parse kernel version from: %q", string(versionStr)) - } - return -} - -// kernelMinReq checks if the runtime kernel is sufficient -// for the test -func kernelMinReq(t *testing.T, kernel, major int) { - k, m, _, err := getKernelVersion() - if err != nil { - t.Fatalf("failed to get host kernel version: %v", err) - } - if k < kernel || k == kernel && m < major { - t.Skipf("host kernel (%d.%d) does not meet test's minimum required version: (%d.%d)", - k, m, kernel, major) - } -} +// lo accesses the loopback interface present in every network namespace. +var lo uint32 = 1 -// SetupDummyInterface create a dummy interface for testing and returns its -// properties -func SetupDummyInterface(conn *Conn, name string) (*LinkMessage, error) { - // construct dummy interface to test XDP program against - if err := conn.Link.New(&LinkMessage{ - Family: unix.AF_UNSPEC, - Index: 1001, - Flags: unix.IFF_UP, - Attributes: &LinkAttributes{ - Name: name, - Info: &LinkInfo{Kind: "dummy"}, - }, - }); err != nil { - return nil, err - } - - // get info for the dummy interface - interf, err := conn.Link.Get(1001) - if err != nil { - conn.Link.Delete(interf.Index) - return nil, err - } - return &interf, err -} +func xdpPrograms(tb testing.TB) (int32, int32) { + tb.Helper() -func GetBPFPrograms() (int32, int32, error) { - // load a BPF test program. If it fails error out of the tests - // and clean up dummy interface. The program loads XDP_PASS - // into the return value register. + // Load XDP_PASS into the return value register. bpfProgram := &ebpf.ProgramSpec{ Type: ebpf.XDP, Instructions: asm.Instructions{ @@ -80,20 +31,26 @@ func GetBPFPrograms() (int32, int32, error) { } prog1, err := ebpf.NewProgram(bpfProgram) if err != nil { - return 0, 0, err + tb.Fatal(err) } prog2, err := ebpf.NewProgram(bpfProgram) if err != nil { - return 0, 0, err + tb.Fatal(err) } + tb.Cleanup(func() { + prog1.Close() + prog2.Close() + }) + // Use the file descriptor of the programs - return int32(prog1.FD()), int32(prog2.FD()), nil + return int32(prog1.FD()), int32(prog2.FD()) } -// SendXDPMsg sends a XDP netlink msg with the specified LinkXDP properties -func SendXPDMsg(conn *Conn, ifIndex uint32, xdp *LinkXDP) error { +func attachXDP(tb testing.TB, conn *Conn, ifIndex uint32, xdp *LinkXDP) { + tb.Helper() + message := LinkMessage{ Family: unix.AF_UNSPEC, Index: ifIndex, @@ -102,46 +59,36 @@ func SendXPDMsg(conn *Conn, ifIndex uint32, xdp *LinkXDP) error { }, } - return conn.Link.Set(&message) + if err := conn.Link.Set(&message); err != nil { + tb.Fatalf("attaching program with fd %d to link at ifindex %d: %s", xdp.FD, ifIndex, err) + } } -// GetXDPProperties returns the XDP attach, XDP prog ID and errors when the +// getXDP returns the XDP attach, XDP prog ID and errors when the // interface could not be fetched -func GetXDPProperties(conn *Conn, ifIndex uint32) (uint8, uint32, error) { +func getXDP(tb testing.TB, conn *Conn, ifIndex uint32) (uint8, uint32) { + tb.Helper() + interf, err := conn.Link.Get(ifIndex) if err != nil { - return 0, 0, err + tb.Fatalf("getting link xdp properties: %s", err) } - return interf.Attributes.XDP.Attached, interf.Attributes.XDP.ProgID, nil + + return interf.Attributes.XDP.Attached, interf.Attributes.XDP.ProgID } func TestLinkXDPAttach(t *testing.T) { - // BPF loading requires a high RLIMIT_MEMLOCK. - n := uint64(1024 * 1024 * 10) - err := unix.Setrlimit(unix.RLIMIT_MEMLOCK, &unix.Rlimit{Cur: n, Max: n}) - if err != nil { - t.Fatalf("failed to increase RLIMIT_MEMLOCK: %v", err) + if err := rlimit.RemoveMemlock(); err != nil { + t.Fatal(err) } - // establish a netlink connections - conn, err := Dial(nil) + conn, err := Dial(&netlink.Config{NetNS: testutils.NetNS(t)}) if err != nil { t.Fatalf("failed to establish netlink socket: %v", err) } defer conn.Close() - // setup dummy interface for the test - interf, err := SetupDummyInterface(conn, "dummyXDPAttach") - if err != nil { - t.Fatalf("failed to setup dummy interface: %v", err) - } - defer conn.Link.Delete(interf.Index) - - // get a BPF program - progFD1, progFD2, err := GetBPFPrograms() - if err != nil { - t.Fatalf("failed to load bpf programs: %v", err) - } + progFD1, progFD2 := xdpPrograms(t) tests := []struct { name string @@ -182,22 +129,12 @@ func TestLinkXDPAttach(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // attach the BPF program to the link - err = SendXPDMsg(conn, interf.Index, tt.xdp) - if err != nil { - t.Fatalf("failed to attach XDP program to link: %v", err) - } - - // validate the XDP properites of the link - attached, progID, err := GetXDPProperties(conn, interf.Index) - if err != nil { - t.Fatalf("failed to get XDP properties from the link: %v", err) - } + attachXDP(t, conn, lo, tt.xdp) + attached, progID := getXDP(t, conn, lo) if attached != unix.XDP_FLAGS_SKB_MODE { t.Fatalf("XDP attached state does not match. Got: %d, wanted: %d", attached, unix.XDP_FLAGS_SKB_MODE) } - if attached == unix.XDP_FLAGS_SKB_MODE && progID == 0 { t.Fatalf("XDP program should be attached but program ID is 0") } @@ -206,60 +143,33 @@ func TestLinkXDPAttach(t *testing.T) { } func TestLinkXDPClear(t *testing.T) { - // BPF loading requires a high RLIMIT_MEMLOCK. - n := uint64(1024 * 1024 * 10) - err := unix.Setrlimit(unix.RLIMIT_MEMLOCK, &unix.Rlimit{Cur: n, Max: n}) - if err != nil { - t.Fatalf("failed to increase RLIMIT_MEMLOCK: %v", err) + if err := rlimit.RemoveMemlock(); err != nil { + t.Fatal(err) } - // establish a netlink connections - conn, err := Dial(nil) + conn, err := Dial(&netlink.Config{NetNS: testutils.NetNS(t)}) if err != nil { t.Fatalf("failed to establish netlink socket: %v", err) } defer conn.Close() - // setup dummy interface for the test - interf, err := SetupDummyInterface(conn, "dummyXDPClear") - if err != nil { - t.Fatalf("failed to setup dummy interface: %v", err) - } - defer conn.Link.Delete(interf.Index) - - // get a BPF program - progFD1, _, err := GetBPFPrograms() - if err != nil { - t.Fatalf("failed to load bpf programs: %v", err) - } + progFD1, _ := xdpPrograms(t) - // attach the BPF program to the link - err = SendXPDMsg(conn, interf.Index, &LinkXDP{ + attachXDP(t, conn, lo, &LinkXDP{ FD: progFD1, Flags: unix.XDP_FLAGS_SKB_MODE, }) - if err != nil { - t.Fatalf("failed to attach XDP program to link: %v", err) - } // clear the BPF program from the link - err = SendXPDMsg(conn, interf.Index, &LinkXDP{ + attachXDP(t, conn, lo, &LinkXDP{ FD: -1, Flags: unix.XDP_FLAGS_SKB_MODE, }) - if err != nil { - t.Fatalf("failed to clear XDP program to link: %v", err) - } - - attached, progID, err := GetXDPProperties(conn, interf.Index) - if err != nil { - t.Fatalf("failed to get XDP program ID 1 from interface: %v", err) - } + attached, progID := getXDP(t, conn, lo) if progID != 0 { t.Fatalf("there is still a program loaded, while we cleared the link") } - if attached != 0 { t.Fatalf( "XDP attached state does not match. Got: %d, wanted: %d\nThere should be no program loaded", @@ -278,78 +188,53 @@ func TestLinkXDPReplace(t *testing.T) { // https://elixir.bootlin.com/linux/v5.6/source/net/core/dev.c#L8662 // source kernel 5.7: // https://elixir.bootlin.com/linux/v5.7/source/net/core/dev.c#L8674 - kernelMinReq(t, 5, 7) + testutils.SkipOnOldKernel(t, "5.7", "XDP_FLAGS_REPLACE") - // BPF loading requires a high RLIMIT_MEMLOCK. - n := uint64(1024 * 1024 * 10) - err := unix.Setrlimit(unix.RLIMIT_MEMLOCK, &unix.Rlimit{Cur: n, Max: n}) - if err != nil { - t.Fatalf("failed to increase RLIMIT_MEMLOCK: %v", err) + if err := rlimit.RemoveMemlock(); err != nil { + t.Fatal(err) } - // establish a netlink connections - conn, err := Dial(nil) + conn, err := Dial(&netlink.Config{NetNS: testutils.NetNS(t)}) if err != nil { t.Fatalf("failed to establish netlink socket: %v", err) } defer conn.Close() - // setup dummy interface for the test - interf, err := SetupDummyInterface(conn, "dummyXDPReplace") - if err != nil { - t.Fatalf("failed to setup dummy interface: %v", err) - } - defer conn.Link.Delete(interf.Index) - - // get BPF programs - progFD1, progFD2, err := GetBPFPrograms() - if err != nil { - t.Fatalf("failed to load bpf programs: %v", err) - } + progFD1, progFD2 := xdpPrograms(t) - err = SendXPDMsg(conn, interf.Index, &LinkXDP{ + attachXDP(t, conn, lo, &LinkXDP{ FD: progFD1, Flags: unix.XDP_FLAGS_SKB_MODE, }) - if err != nil { - t.Fatalf("failed to attach XDP program 1 to link: %v", err) - } - _, progID1, err := GetXDPProperties(conn, interf.Index) - if err != nil { - t.Fatalf("failed to get XDP program ID 1 from interface: %v", err) - } + _, progID1 := getXDP(t, conn, lo) - err = SendXPDMsg(conn, interf.Index, &LinkXDP{ - FD: progFD2, - ExpectedFD: progFD2, - Flags: unix.XDP_FLAGS_SKB_MODE | unix.XDP_FLAGS_REPLACE, - }) - if err == nil { + if err := conn.Link.Set(&LinkMessage{ + Family: unix.AF_UNSPEC, + Index: lo, + Attributes: &LinkAttributes{ + XDP: &LinkXDP{ + FD: progFD2, + ExpectedFD: progFD2, + Flags: unix.XDP_FLAGS_SKB_MODE | unix.XDP_FLAGS_REPLACE, + }, + }, + }); err == nil { t.Fatalf("replaced XDP program while expected FD did not match: %v", err) } - _, progID2, err := GetXDPProperties(conn, interf.Index) - if err != nil { - t.Fatalf("failed to get XDP program ID 2 from interface: %v", err) - } + _, progID2 := getXDP(t, conn, lo) if progID2 != progID1 { t.Fatal("XDP prog ID does not match previous program ID, which it should") } - err = SendXPDMsg(conn, interf.Index, &LinkXDP{ + attachXDP(t, conn, lo, &LinkXDP{ FD: progFD2, ExpectedFD: progFD1, Flags: unix.XDP_FLAGS_SKB_MODE | unix.XDP_FLAGS_REPLACE, }) - if err != nil { - t.Fatalf("could not replace XDP program: %v", err) - } - _, progID2, err = GetXDPProperties(conn, interf.Index) - if err != nil { - t.Fatalf("failed to get XDP program ID 2 from interface: %v", err) - } + _, progID2 = getXDP(t, conn, lo) if progID2 == progID1 { t.Fatal("XDP prog ID does match previous program ID, which it shouldn't") } diff --git a/netns.go b/netns.go index 1e9a8d8..55c48c1 100644 --- a/netns.go +++ b/netns.go @@ -1,54 +1,46 @@ package rtnetlink import ( - "os" - "path/filepath" - "github.com/jsimonetti/rtnetlink/v2/internal/unix" ) -// NetNS represents a Linux network namespace +// NetNS represents a Linux network namespace handle to specify in +// [LinkAttributes]. +// +// Use [NetNSForPID] to create a handle to the network namespace of an existing +// PID, or [NetNSForFD] for a handle to an existing network namespace created by +// another library. type NetNS struct { - file *os.File - pid uint32 + fd *uint32 + pid *uint32 } -// NewNetNS returns a new NetNS from the given type -// When an uint32 is given simply the pid value is set -// When a string is given a namespace file is opened with the name and the file descriptor is set -// The file descriptor should be closed after use with the Close() method -func NewNetNS[T string | uint32](t T) (*NetNS, error) { - if name, ok := any(t).(string); ok { - file, err := os.Open(filepath.Join("/var/run/netns", name)) - if err != nil { - return nil, err - } - - return &NetNS{file: file}, nil - } - return &NetNS{pid: any(t).(uint32)}, nil +// NetNSForPID returns a handle to the network namespace of an existing process +// given its pid. The process must be alive when the NetNS is used in any API +// calls. +// +// The resulting NetNS doesn't hold a hard reference to the netns (it doesn't +// increase its refcount) and becomes invalid when the process it points to +// dies. +func NetNSForPID(pid uint32) *NetNS { + return &NetNS{pid: &pid} } -// Type returns either unix.IFLA_NET_NS_FD or unix.IFLA_NET_NS_PID according ns data type -func (n *NetNS) Type() uint16 { - if n.file != nil { - return unix.IFLA_NET_NS_FD - } - return unix.IFLA_NET_NS_PID +// NetNSForFD returns a handle to an existing network namespace created by +// another library. It does not clone fd or manage its lifecycle in any way. +// The caller is responsible for making sure the underlying fd stays alive +// for the duration of any API calls using the NetNS. +func NetNSForFD(fd uint32) *NetNS { + return &NetNS{fd: &fd} } -// Value returns either a file descriptor value or the pid value of the ns -func (n *NetNS) Value() uint32 { - if n.file != nil { - return uint32(n.file.Fd()) +// value returns the type and value of the NetNS for use in netlink attributes. +func (ns *NetNS) value() (uint16, uint32) { + if ns.fd != nil { + return unix.IFLA_NET_NS_FD, *ns.fd } - return n.pid -} - -// Close closes the file descriptor -func (n *NetNS) Close() error { - if n.file != nil { - return n.file.Close() + if ns.pid != nil { + return unix.IFLA_NET_NS_PID, *ns.pid } - return nil + return 0, 0 } diff --git a/rtnl/neigh.go b/rtnl/neigh.go index 8705bba..058d928 100644 --- a/rtnl/neigh.go +++ b/rtnl/neigh.go @@ -11,6 +11,10 @@ type Neigh struct { HwAddr net.HardwareAddr // Link-layer address IP net.IP // Network-layer address Interface *net.Interface // Network interface + + // Bitfield representing the state of the neighbor entry, + // corresponding to the NUD_ constants. + State uint16 } // Neighbours lists entries from the neighbor table (e.g. the ARP table). @@ -38,7 +42,9 @@ func (c *Conn) Neighbours(ifc *net.Interface, family int) (r []*Neigh, err error if !ok { iface, err = c.LinkByIndex(ifindex) if err != nil { - return nil, err + // Received a neigh entry for a link that no longer exists, so we cannot + // populate the Interface field. Skip the entry. + continue } ifcache[ifindex] = iface } @@ -46,6 +52,7 @@ func (c *Conn) Neighbours(ifc *net.Interface, family int) (r []*Neigh, err error HwAddr: m.Attributes.LLAddress, IP: m.Attributes.Address, Interface: iface, + State: m.State, } r = append(r, p) } diff --git a/rtnl/neigh_live_test.go b/rtnl/neigh_live_test.go index bc97565..47c1c85 100644 --- a/rtnl/neigh_live_test.go +++ b/rtnl/neigh_live_test.go @@ -6,6 +6,8 @@ package rtnl import ( "net" "testing" + + "golang.org/x/sys/unix" ) func TestLiveNeighbours(t *testing.T) { @@ -27,26 +29,32 @@ func TestLiveNeighbours(t *testing.T) { } for i, e := range neigtab { t.Logf("* neighbour table entry [%d]: %v", i, e) + + // Ignore neighbor entries in internal/pseudo state. + if e.State == unix.NUD_NONE { + continue + } + // Loopback and p2p interfaces can have neigh entries with a zero IP address. if e.IP.IsUnspecified() { - // This test doesn't seem to be very reliable - // Disabling for now - // t.Error("zero e.IP, expected non-zero") continue } + if e.Interface == nil { t.Error("nil e.Interface, expected non-nil") - continue } if len(e.Interface.Name) == 0 { t.Error("zero-length e.Interface.Name") } - if e.IP.IsLoopback() { + + // Don't (always) expect hardware address info on entries marked noarp, + // as they include link-local multicast and loopback addresses that are + // valid for all interfaces on the host. + if e.State == unix.NUD_NOARP { continue } + if hardwareAddrIsUnspecified(e.HwAddr) { - // This test doesn't seem to be very reliable - // Disabling for now - // t.Error("zero e.HwAddr, expected non-zero") + t.Error("zero e.HwAddr, expected non-zero") } if hardwareAddrIsUnspecified(e.Interface.HardwareAddr) { t.Error("zero e.Interface.HardwareAddr, expected non-zero")