diff --git a/handle_linux.go b/handle_linux.go index 0be42b8f8..a6e8346d2 100644 --- a/handle_linux.go +++ b/handle_linux.go @@ -2,6 +2,8 @@ package netlink import ( "fmt" + "sync" + "sync/atomic" "time" "github.com/vishvananda/netlink/nl" @@ -12,10 +14,53 @@ 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 { - lookupByDump bool - 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 @@ -25,19 +70,16 @@ 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 +// +// 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 } @@ -68,7 +110,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 @@ -146,24 +189,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}) +} + +// 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(newNs, curNs netns.NsHandle, nlFamilies ...int) (*Handle, error) { +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 { @@ -207,6 +261,6 @@ func (h *Handle) newNetlinkRequest(proto, flags int) *nl.NetlinkRequest { }, Sockets: h.sockets, - RetryInterrupted: h.options.retryInterrupted, + RetryInterrupted: h.options.RetryInterrupted, } } 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{})) +} diff --git a/link_linux.go b/link_linux.go index 119b06470..d57d6e7ba 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) } @@ -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)) @@ -2044,7 +2043,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 +2067,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) } @@ -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)) @@ -2089,7 +2087,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) } @@ -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) {