From de5a37d46f8232731b7f14a97cb39c4d271abe06 Mon Sep 17 00:00:00 2001 From: Timo Beckers Date: Wed, 18 Mar 2026 15:27:07 +0100 Subject: [PATCH 1/3] handle: move lookupByDump from HandleOptions to Handle This is not really a configuration option, but rather an internal cache for remembering the lookup fallback behaviour. Signed-off-by: Timo Beckers --- handle_linux.go | 4 +++- link_linux.go | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/handle_linux.go b/handle_linux.go index 0be42b8f8..eb2de66fe 100644 --- a/handle_linux.go +++ b/handle_linux.go @@ -2,6 +2,7 @@ package netlink import ( "fmt" + "sync/atomic" "time" "github.com/vishvananda/netlink/nl" @@ -13,7 +14,6 @@ import ( var pkgHandle = &Handle{} type HandleOptions struct { - lookupByDump bool collectVFInfo bool retryInterrupted bool } @@ -25,6 +25,8 @@ type HandleOptions struct { type Handle struct { sockets map[int]*nl.SocketHandle options HandleOptions + + lookupByDump atomic.Bool } // DisableVFInfoCollection configures the handle to skip VF information fetching diff --git a/link_linux.go b/link_linux.go index 119b06470..dbde22573 100644 --- a/link_linux.go +++ b/link_linux.go @@ -2020,7 +2020,7 @@ func LinkByName(name string) (Link, error) { // filtering a dump of all link names. In this case, if the returned error is // [ErrDumpInterrupted] the result may be missing or outdated. func (h *Handle) LinkByName(name string) (Link, error) { - if h.options.lookupByDump { + if h.lookupByDump.Load() { return h.linkByNameDump(name) } @@ -2044,7 +2044,7 @@ func (h *Handle) LinkByName(name string) (Link, error) { if err == unix.EINVAL { // older kernels don't support looking up via IFLA_IFNAME // so fall back to dumping all links - h.options.lookupByDump = true + h.lookupByDump.Store(true) return h.linkByNameDump(name) } @@ -2068,7 +2068,7 @@ func LinkByAlias(alias string) (Link, error) { // filtering a dump of all link names. In this case, if the returned error is // [ErrDumpInterrupted] the result may be missing or outdated. func (h *Handle) LinkByAlias(alias string) (Link, error) { - if h.options.lookupByDump { + if h.lookupByDump.Load() { return h.linkByAliasDump(alias) } @@ -2089,7 +2089,7 @@ func (h *Handle) LinkByAlias(alias string) (Link, error) { if err == unix.EINVAL { // older kernels don't support looking up via IFLA_IFALIAS // so fall back to dumping all links - h.options.lookupByDump = true + h.lookupByDump.Store(true) return h.linkByAliasDump(alias) } From 378a48ced8cbf5de660a568a0340eeacef629b10 Mon Sep 17 00:00:00 2001 From: Timo Beckers Date: Wed, 18 Mar 2026 15:30:04 +0100 Subject: [PATCH 2/3] handle: add NewHandleWithOptions(), deprecate DisableVFInfoCollection() This commit repurposes HandleOptions as a declarative configuration mechanism for Handle. It can be passed to NewHandleWithOptions. It also removes the unreleased Handle.RetryInterrupted and deprecates DisableVFInfoCollection in favor of its HandleOptions counterpart. Signed-off-by: Timo Beckers --- handle_linux.go | 54 +++++++++++++++++++++++++++++++++---------------- link_linux.go | 22 ++++++++++---------- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/handle_linux.go b/handle_linux.go index eb2de66fe..3b145665d 100644 --- a/handle_linux.go +++ b/handle_linux.go @@ -13,9 +13,22 @@ import ( // Empty handle used by the netlink package methods var pkgHandle = &Handle{} +// HandleOptions defines the options for creating a netlink Handle, allowing the +// caller to customize its behaviour. type HandleOptions struct { - collectVFInfo bool - retryInterrupted bool + // DisableVFInfoCollection controls whether to fetch VF information for each + // link. This is an expensive operation and should be disabled if the caller + // does not need the VF information. + DisableVFInfoCollection bool + + // RetryInterrupted controls whether to automatically retry dump operations a + // number of times if they fail with EINTR before finally returning + // [ErrDumpInterrupted]. + RetryInterrupted bool + + // NetNS specifies the network namespace to operate on. If not set, the + // current network namespace will be used. + NetNS *netns.NsHandle } // Handle is a handle for the netlink requests on a @@ -30,16 +43,11 @@ type Handle struct { } // DisableVFInfoCollection configures the handle to skip VF information fetching +// +// Deprecated: Use [NewHandleWithOptions] and set +// [HandleOptions.DisableVFInfoCollection] instead. func (h *Handle) DisableVFInfoCollection() *Handle { - h.options.collectVFInfo = false - return h -} - -// RetryInterrupted configures the Handle to automatically retry dump operations -// a number of times if they fail with EINTR before finally returning -// [ErrDumpInterrupted]. -func (h *Handle) RetryInterrupted() *Handle { - h.options.retryInterrupted = true + h.options.DisableVFInfoCollection = true return h } @@ -70,7 +78,8 @@ func (h *Handle) SupportsNetlinkFamily(nlFamily int) bool { // If no families are specified, all the families the netlink package // supports will be automatically added. func NewHandle(nlFamilies ...int) (*Handle, error) { - return newHandle(netns.None(), netns.None(), nlFamilies...) + none := netns.None() + return newHandle(none, HandleOptions{NetNS: &none}, nlFamilies...) } // SetSocketTimeout sets the send and receive timeout for each socket in the @@ -148,24 +157,35 @@ func (h *Handle) SetStrictCheck(state bool) error { // specified by ns. If ns=netns.None(), current network namespace // will be assumed func NewHandleAt(ns netns.NsHandle, nlFamilies ...int) (*Handle, error) { - return newHandle(ns, netns.None(), nlFamilies...) + return newHandle(netns.None(), HandleOptions{NetNS: &ns}, nlFamilies...) } // NewHandleAtFrom works as NewHandle but allows client to specify the // new and the origin netns Handle. func NewHandleAtFrom(newNs, curNs netns.NsHandle) (*Handle, error) { - return newHandle(newNs, curNs) + return newHandle(curNs, HandleOptions{NetNS: &newNs}) } -func newHandle(newNs, curNs netns.NsHandle, nlFamilies ...int) (*Handle, error) { +// NewHandleWithOptions returns a Handle created using the specified options. +func NewHandleWithOptions(opts HandleOptions, nlFamilies ...int) (*Handle, error) { + return newHandle(netns.None(), opts, nlFamilies...) +} + +func newHandle(curNs netns.NsHandle, opts HandleOptions, nlFamilies ...int) (*Handle, error) { h := &Handle{ sockets: map[int]*nl.SocketHandle{}, - options: HandleOptions{collectVFInfo: true}, + options: opts, } fams := nl.SupportedNlFamilies if len(nlFamilies) != 0 { fams = nlFamilies } + + newNs := netns.None() + if opts.NetNS != nil { + newNs = *opts.NetNS + } + for _, f := range fams { s, err := nl.GetNetlinkSocketAt(newNs, curNs, f) if err != nil { @@ -209,6 +229,6 @@ func (h *Handle) newNetlinkRequest(proto, flags int) *nl.NetlinkRequest { }, Sockets: h.sockets, - RetryInterrupted: h.options.retryInterrupted, + RetryInterrupted: h.options.RetryInterrupted, } } diff --git a/link_linux.go b/link_linux.go index dbde22573..d57d6e7ba 100644 --- a/link_linux.go +++ b/link_linux.go @@ -2029,9 +2029,8 @@ func (h *Handle) LinkByName(name string) (Link, error) { msg := nl.NewIfInfomsg(unix.AF_UNSPEC) req.AddData(msg) - if h.options.collectVFInfo { - attr := nl.NewRtAttr(unix.IFLA_EXT_MASK, nl.Uint32Attr(nl.RTEXT_FILTER_VF)) - req.AddData(attr) + if !h.options.DisableVFInfoCollection { + req.AddData(nl.NewRtAttr(unix.IFLA_EXT_MASK, nl.Uint32Attr(nl.RTEXT_FILTER_VF))) } nameData := nl.NewRtAttr(unix.IFLA_IFNAME, nl.ZeroTerminated(name)) @@ -2077,9 +2076,8 @@ func (h *Handle) LinkByAlias(alias string) (Link, error) { msg := nl.NewIfInfomsg(unix.AF_UNSPEC) req.AddData(msg) - if h.options.collectVFInfo { - attr := nl.NewRtAttr(unix.IFLA_EXT_MASK, nl.Uint32Attr(nl.RTEXT_FILTER_VF)) - req.AddData(attr) + if !h.options.DisableVFInfoCollection { + req.AddData(nl.NewRtAttr(unix.IFLA_EXT_MASK, nl.Uint32Attr(nl.RTEXT_FILTER_VF))) } nameData := nl.NewRtAttr(unix.IFLA_IFALIAS, nl.ZeroTerminated(alias)) @@ -2108,9 +2106,9 @@ func (h *Handle) LinkByIndex(index int) (Link, error) { msg := nl.NewIfInfomsg(unix.AF_UNSPEC) msg.Index = int32(index) req.AddData(msg) - if h.options.collectVFInfo { - attr := nl.NewRtAttr(unix.IFLA_EXT_MASK, nl.Uint32Attr(nl.RTEXT_FILTER_VF)) - req.AddData(attr) + + if !h.options.DisableVFInfoCollection { + req.AddData(nl.NewRtAttr(unix.IFLA_EXT_MASK, nl.Uint32Attr(nl.RTEXT_FILTER_VF))) } return execGetLink(req) @@ -2529,8 +2527,10 @@ func (h *Handle) LinkList() ([]Link, error) { msg := nl.NewIfInfomsg(unix.AF_UNSPEC) req.AddData(msg) - attr := nl.NewRtAttr(unix.IFLA_EXT_MASK, nl.Uint32Attr(nl.RTEXT_FILTER_VF)) - req.AddData(attr) + + if !h.options.DisableVFInfoCollection { + req.AddData(nl.NewRtAttr(unix.IFLA_EXT_MASK, nl.Uint32Attr(nl.RTEXT_FILTER_VF))) + } msgs, executeErr := req.Execute(unix.NETLINK_ROUTE, unix.RTM_NEWLINK) if executeErr != nil && !errors.Is(executeErr, ErrDumpInterrupted) { From 6b21e75ca3b7c8fbe787c0cf3cb86514f1715b2a Mon Sep 17 00:00:00 2001 From: Timo Beckers Date: Mon, 23 Mar 2026 14:56:38 +0100 Subject: [PATCH 3/3] handle: configure package-global handle using ConfigureHandle This commit introduces the funcion ConfigureHandle to replace the global package handle with one configured using the given HandleOptions. It can only be called once per process. Signed-off-by: Timo Beckers --- handle_linux.go | 32 ++++++++++++++++++++++++++++++++ handle_linux_test.go | 19 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/handle_linux.go b/handle_linux.go index 3b145665d..a6e8346d2 100644 --- a/handle_linux.go +++ b/handle_linux.go @@ -2,6 +2,7 @@ package netlink import ( "fmt" + "sync" "sync/atomic" "time" @@ -13,6 +14,37 @@ import ( // Empty handle used by the netlink package methods var pkgHandle = &Handle{} +var configMu sync.Mutex +var configDone bool + +// ConfigureHandle configures the default, package-wide netlink handle used by +// the netlink package's global functions like [LinkList] and [AddrList] with +// the given opts. It does not affect any existing or future [Handle] returned +// by the library. +// +// This function is not safe to call concurrently with any netlink operations +// using the global package handle. Invoke it from init() functions only. +// +// Returns an error if called more than once per process. +func ConfigureHandle(opts HandleOptions) error { + configMu.Lock() + defer configMu.Unlock() + + if configDone { + return fmt.Errorf("netlink package handle already configured") + } + + h, err := NewHandleWithOptions(opts) + if err != nil { + return fmt.Errorf("creating handle: %w", err) + } + + configDone = true + pkgHandle = h + + return nil +} + // HandleOptions defines the options for creating a netlink Handle, allowing the // caller to customize its behaviour. type HandleOptions struct { diff --git a/handle_linux_test.go b/handle_linux_test.go index 3b549af38..29e728d03 100644 --- a/handle_linux_test.go +++ b/handle_linux_test.go @@ -3,6 +3,8 @@ package netlink import ( "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestSetGetSocketTimeout(t *testing.T) { @@ -15,3 +17,20 @@ func TestSetGetSocketTimeout(t *testing.T) { t.Fatalf("Unexpected socket timeout value: got=%v, expected=%v", val, timeout) } } + +func TestConfigureHandle(t *testing.T) { + orig := pkgHandle + origDone := configDone + t.Cleanup(func() { + configMu.Lock() + defer configMu.Unlock() + + pkgHandle = orig + configDone = origDone + }) + + assert.NoError(t, ConfigureHandle(HandleOptions{DisableVFInfoCollection: true})) + assert.NotEqual(t, orig, pkgHandle) + assert.NoError(t, pkgHandle.Close()) + assert.Error(t, ConfigureHandle(HandleOptions{})) +}