From e9c21a3a83cb414bfb7f9d19f7f0b894f1ef71e9 Mon Sep 17 00:00:00 2001 From: Lorenz Brun Date: Wed, 22 Nov 2023 04:24:47 +0100 Subject: [PATCH] ethtool: add support for private flags This adds support for geting and setting private flags which are driver-specific (and sometimes even model-specific) controls not part of any generic network settings API. They are used to toggle low-level features in various NICs. --- client.go | 56 ++++++++++++++++++ client_linux.go | 132 ++++++++++++++++++++++++++++++++++++++++++- client_linux_test.go | 55 ++++++++++++++++++ client_others.go | 29 +++++----- 4 files changed, 258 insertions(+), 14 deletions(-) diff --git a/client.go b/client.go index 6fceee9..b4272f6 100644 --- a/client.go +++ b/client.go @@ -284,5 +284,61 @@ func (c *Client) SetWakeOnLAN(wol WakeOnLAN) error { return c.c.SetWakeOnLAN(wol) } +// PrivateFlags is a list of driver-specific flags which are either on or off. +// These are used to control behavior specific to a specific driver or device +// for which no generic API exists. +// +// The flags which go in here are mostly undocumented other than in kernel +// source code, you can get the list of supported flags by calling +// PrivateFlags() and then searching for the returned names in Linux kernel +// sources. +// +// This is technically a bitset but as the bit positions are not stable across +// kernel versions there is no reason to use that functionality, thus it is not +// exposed. +// +// Note that these flags are in practice not fully covered by Linux's userspace +// ABI guarantees, it should be expected that a flag can go away. +type PrivateFlags struct { + Interface Interface + // Flags is a map of flag names to their active state, i.e. if the flag + // is on or off. + Flags map[string]bool +} + +// AllPrivateFlags returns Private Flags for each ethtool-supported interface +// on this system. +func (c *Client) AllPrivateFlags() ([]*PrivateFlags, error) { + return c.c.AllPrivateFlags() +} + +// PrivateFlags returns Private Flags for a single interface. See the type for +// a more in-depth explanation. +// +// If the requested device does not exist or is not supported by the ethtool +// interface, an error compatible with errors.Is(err, os.ErrNotExist) will be +// returned. +func (c *Client) PrivateFlags(ifi Interface) (*PrivateFlags, error) { + return c.c.PrivateFlags(ifi) +} + +// SetPrivateFlags attempts to set the given private flags on the given +// interface. Flags does not need to contain the all flags, those not +// in it are left as-is. +// +// Setting Private Flags requires elevated privileges and if the caller +// does not have permission, an error compatible with errors.Is(err, +// os.ErrPermission) will be returned. +// +// Note that not all flags can be changed in all interface states, some might +// only be settable if the interface is down or are only settable once. +// +// If the requested device does not exist or is not supported by the ethtool +// interface, an error compatible with errors.Is(err, os.ErrNotExist) will be +// returned. +func (c *Client) SetPrivateFlags(p PrivateFlags) error { + return c.c.SetPrivateFlags(p) +} + // Close cleans up the Client's resources. func (c *Client) Close() error { return c.c.Close() } diff --git a/client_linux.go b/client_linux.go index c786404..0d83dcb 100644 --- a/client_linux.go +++ b/client_linux.go @@ -395,6 +395,134 @@ func (wol WakeOnLAN) encode(ae *netlink.AttributeEncoder) { }) } +// AllPrivateFlags fetches Private Flags for all ethtool-supported links. +func (c *client) AllPrivateFlags() ([]*PrivateFlags, error) { + return c.privateFlags(netlink.Dump, Interface{}) +} + +// PrivateFlags fetches Private Flags for a single interface. +func (c *client) PrivateFlags(ifi Interface) (*PrivateFlags, error) { + fs, err := c.privateFlags(0, ifi) + if err != nil { + return nil, err + } + if f := len(fs); f != 1 { + panicf("ethtool: unexpected number of PrivateFlags messages for request index: %d, name: %q: %d", + ifi.Index, ifi.Name, f) + } + + return fs[0], nil +} + +func (c *client) privateFlags(flags netlink.HeaderFlags, ifi Interface) ([]*PrivateFlags, error) { + msgs, err := c.get( + unix.ETHTOOL_A_PRIVFLAGS_HEADER, + unix.ETHTOOL_MSG_PRIVFLAGS_GET, + flags, + ifi, + nil, + ) + if err != nil { + return nil, err + } + + return parsePrivateFlags(msgs) +} + +func (c *client) SetPrivateFlags(pf PrivateFlags) error { + _, err := c.get( + unix.ETHTOOL_A_WOL_HEADER, + unix.ETHTOOL_MSG_PRIVFLAGS_SET, + netlink.Acknowledge, + pf.Interface, + pf.encode, + ) + return err +} + +// parsePrivateFlags parses PrivateFlag structures from a slice of generic netlink +// messages. +func parsePrivateFlags(msgs []genetlink.Message) ([]*PrivateFlags, error) { + wols := make([]*PrivateFlags, 0, len(msgs)) + for _, m := range msgs { + ad, err := netlink.NewAttributeDecoder(m.Data) + if err != nil { + return nil, err + } + + var privFlags PrivateFlags + for ad.Next() { + switch ad.Type() { + case unix.ETHTOOL_A_PRIVFLAGS_HEADER: + ad.Nested(parseInterface(&privFlags.Interface)) + case unix.ETHTOOL_A_PRIVFLAGS_FLAGS: + ad.Nested(parsePrivateFlagBitset(&privFlags.Flags)) + } + } + + if err := ad.Err(); err != nil { + return nil, err + } + + wols = append(wols, &privFlags) + } + + return wols, nil +} + +func parsePrivateFlagBitset(p *map[string]bool) func(*netlink.AttributeDecoder) error { + return func(ad *netlink.AttributeDecoder) error { + flags := make(map[string]bool) + for ad.Next() { + switch ad.Type() { + case unix.ETHTOOL_A_BITSET_BITS: + ad.Nested(func(nad *netlink.AttributeDecoder) error { + for nad.Next() { + switch nad.Type() { + case unix.ETHTOOL_A_BITSET_BITS_BIT: + nad.Nested(func(nnad *netlink.AttributeDecoder) error { + var name string + var active bool + for nnad.Next() { + switch nnad.Type() { + case unix.ETHTOOL_A_BITSET_BIT_NAME: + name = nnad.String() + case unix.ETHTOOL_A_BITSET_BIT_VALUE: + active = true + } + } + flags[name] = active + return nnad.Err() + }) + } + } + return nad.Err() + }) + } + } + *p = flags + return ad.Err() + } +} + +// encode packs PrivateFlags data into the appropriate netlink attributes for the +// encoder. +func (pf *PrivateFlags) encode(ae *netlink.AttributeEncoder) { + ae.Nested(unix.ETHTOOL_A_PRIVFLAGS_FLAGS, func(nae *netlink.AttributeEncoder) error { + nae.Nested(unix.ETHTOOL_A_BITSET_BITS, func(nnae *netlink.AttributeEncoder) error { + for name, active := range pf.Flags { + nnae.Nested(unix.ETHTOOL_A_BITSET_BITS_BIT, func(nnnae *netlink.AttributeEncoder) error { + nnnae.String(unix.ETHTOOL_A_BITSET_BIT_NAME, name) + nnnae.Flag(unix.ETHTOOL_A_BITSET_BIT_VALUE, active) + return nil + }) + } + return nil + }) + return nil + }) +} + // get performs a request/response interaction with ethtool netlink. func (c *client) get( header uint16, @@ -433,7 +561,9 @@ func (c *client) get( // since the ethtool multicast group notifications require the compact // format, so we might as well always use it. if cmd != unix.ETHTOOL_MSG_FEC_SET && - cmd != unix.ETHTOOL_MSG_WOL_SET { + cmd != unix.ETHTOOL_MSG_WOL_SET && + cmd != unix.ETHTOOL_MSG_PRIVFLAGS_GET && + cmd != unix.ETHTOOL_MSG_PRIVFLAGS_SET { nae.Uint32(unix.ETHTOOL_A_HEADER_FLAGS, unix.ETHTOOL_FLAG_COMPACT_BITSETS) } diff --git a/client_linux_test.go b/client_linux_test.go index c7ef66d..5389ffc 100644 --- a/client_linux_test.go +++ b/client_linux_test.go @@ -74,6 +74,13 @@ func TestLinuxClientErrors(t *testing.T) { return err }, }, + { + name: "private flags", + call: func(c *Client, ifi Interface) error { + _, err := c.PrivateFlags(ifi) + return err + }, + }, } for _, tt := range tests { @@ -968,6 +975,54 @@ func TestFEC(t *testing.T) { } } +func TestPrivateFlags(t *testing.T) { + // Reference value captured from ethtool --show-priv-flags eth0 + c := testClient(t, clientTest{ + HeaderFlags: netlink.Request, + Command: unix.ETHTOOL_MSG_PRIVFLAGS_GET, + EncodedAttributes: []byte("\x10\x00\x01\x80\x09\x00\x02\x00\x65\x74\x68\x30\x00\x00\x00\x00"), + Messages: []genetlink.Message{{ + Data: []byte("\x18\x00\x01\x80\x08\x00\x01\x00\x02\x00\x00\x00\x09\x00\x02\x00\x65\x74\x68\x30\x00\x00\x00\x00\xb4\x01\x02\x80\x08\x00\x02\x00\x0d\x00\x00\x00\xa8\x01\x03\x80\x14\x00\x01\x80\x08\x00\x01\x00\x00\x00\x00\x00\x08\x00\x02\x00\x4d\x46\x50\x00\x24\x00\x01\x80\x08\x00\x01\x00\x01\x00\x00\x00\x18\x00\x02\x00\x74\x6f\x74\x61\x6c\x2d\x70\x6f\x72\x74\x2d\x73\x68\x75\x74\x64\x6f\x77\x6e\x00\x1c\x00\x01\x80\x08\x00\x01\x00\x02\x00\x00\x00\x10\x00\x02\x00\x4c\x69\x6e\x6b\x50\x6f\x6c\x6c\x69\x6e\x67\x00\x28\x00\x01\x80\x08\x00\x01\x00\x03\x00\x00\x00\x16\x00\x02\x00\x66\x6c\x6f\x77\x2d\x64\x69\x72\x65\x63\x74\x6f\x72\x2d\x61\x74\x72\x00\x00\x00\x04\x00\x03\x00\x1c\x00\x01\x80\x08\x00\x01\x00\x04\x00\x00\x00\x0e\x00\x02\x00\x76\x65\x62\x2d\x73\x74\x61\x74\x73\x00\x00\x00\x20\x00\x01\x80\x08\x00\x01\x00\x05\x00\x00\x00\x14\x00\x02\x00\x68\x77\x2d\x61\x74\x72\x2d\x65\x76\x69\x63\x74\x69\x6f\x6e\x00\x24\x00\x01\x80\x08\x00\x01\x00\x06\x00\x00\x00\x17\x00\x02\x00\x6c\x69\x6e\x6b\x2d\x64\x6f\x77\x6e\x2d\x6f\x6e\x2d\x63\x6c\x6f\x73\x65\x00\x00\x1c\x00\x01\x80\x08\x00\x01\x00\x07\x00\x00\x00\x0e\x00\x02\x00\x6c\x65\x67\x61\x63\x79\x2d\x72\x78\x00\x00\x00\x28\x00\x01\x80\x08\x00\x01\x00\x08\x00\x00\x00\x1b\x00\x02\x00\x64\x69\x73\x61\x62\x6c\x65\x2d\x73\x6f\x75\x72\x63\x65\x2d\x70\x72\x75\x6e\x69\x6e\x67\x00\x00\x20\x00\x01\x80\x08\x00\x01\x00\x09\x00\x00\x00\x14\x00\x02\x00\x64\x69\x73\x61\x62\x6c\x65\x2d\x66\x77\x2d\x6c\x6c\x64\x70\x00\x1c\x00\x01\x80\x08\x00\x01\x00\x0a\x00\x00\x00\x0b\x00\x02\x00\x72\x73\x2d\x66\x65\x63\x00\x00\x04\x00\x03\x00\x20\x00\x01\x80\x08\x00\x01\x00\x0b\x00\x00\x00\x0f\x00\x02\x00\x62\x61\x73\x65\x2d\x72\x2d\x66\x65\x63\x00\x00\x04\x00\x03\x00\x28\x00\x01\x80\x08\x00\x01\x00\x0c\x00\x00\x00\x1c\x00\x02\x00\x76\x66\x2d\x74\x72\x75\x65\x2d\x70\x72\x6f\x6d\x69\x73\x63\x2d\x73\x75\x70\x70\x6f\x72\x74\x00"), + }}, + Error: nil, + }) + f, err := c.PrivateFlags(Interface{Name: "eth0"}) + if err != nil { + t.Fatalf("failed to get private flags: %v", err) + } + if len(f.Flags) != 13 { + t.Errorf("expected 13 flags, got %d", len(f.Flags)) + } + if _, ok := f.Flags["disable-fw-lldp"]; !ok { + t.Errorf("expected flag disable-fw-lldp to be present, but it is not") + } + if !f.Flags["rs-fec"] { + t.Error("expected rs-fec flag to be active") + } +} + +func TestSetPrivateFlags(t *testing.T) { + // Reference value captured from ethtool --set-priv-flags eth0 disable-fw-lldp on + c := testClient(t, clientTest{ + HeaderFlags: netlink.Request | netlink.Acknowledge, + Command: unix.ETHTOOL_MSG_PRIVFLAGS_SET, + EncodedAttributes: []byte("\x10\x00\x01\x80\x09\x00\x02\x00\x65\x74\x68\x30\x00\x00\x00\x00\x24\x00\x02\x80\x20\x00\x03\x80\x1c\x00\x01\x80\x14\x00\x02\x00\x64\x69\x73\x61\x62\x6c\x65\x2d\x66\x77\x2d\x6c\x6c\x64\x70\x00\x04\x00\x03\x00"), + Messages: []genetlink.Message{{ + Data: nil, + }}, + Error: nil, + }) + err := c.SetPrivateFlags(PrivateFlags{ + Interface: Interface{Name: "eth0"}, + Flags: map[string]bool{ + "disable-fw-lldp": true, + }, + }) + if err != nil { + t.Fatalf("failed to set private flags: %v", err) + } +} + func skipBigEndian(t *testing.T) { t.Helper() diff --git a/client_others.go b/client_others.go index e74eaef..bf3f6a0 100644 --- a/client_others.go +++ b/client_others.go @@ -16,19 +16,22 @@ func (*Error) Is(_ error) bool { return false } type client struct{} -func newClient() (*client, error) { return nil, errUnsupported } -func (c *client) LinkInfos() ([]*LinkInfo, error) { return nil, errUnsupported } -func (c *client) LinkInfo(_ Interface) (*LinkInfo, error) { return nil, errUnsupported } -func (c *client) LinkModes() ([]*LinkMode, error) { return nil, errUnsupported } -func (c *client) LinkMode(_ Interface) (*LinkMode, error) { return nil, errUnsupported } -func (c *client) LinkStates() ([]*LinkState, error) { return nil, errUnsupported } -func (c *client) LinkState(_ Interface) (*LinkState, error) { return nil, errUnsupported } -func (c *client) WakeOnLANs() ([]*WakeOnLAN, error) { return nil, errUnsupported } -func (c *client) WakeOnLAN(_ Interface) (*WakeOnLAN, error) { return nil, errUnsupported } -func (c *client) SetWakeOnLAN(_ WakeOnLAN) error { return errUnsupported } -func (c *client) FEC(_ Interface) (*FEC, error) { return nil, errUnsupported } -func (c *client) SetFEC(_ FEC) error { return errUnsupported } -func (c *client) Close() error { return errUnsupported } +func newClient() (*client, error) { return nil, errUnsupported } +func (c *client) LinkInfos() ([]*LinkInfo, error) { return nil, errUnsupported } +func (c *client) LinkInfo(_ Interface) (*LinkInfo, error) { return nil, errUnsupported } +func (c *client) LinkModes() ([]*LinkMode, error) { return nil, errUnsupported } +func (c *client) LinkMode(_ Interface) (*LinkMode, error) { return nil, errUnsupported } +func (c *client) LinkStates() ([]*LinkState, error) { return nil, errUnsupported } +func (c *client) LinkState(_ Interface) (*LinkState, error) { return nil, errUnsupported } +func (c *client) WakeOnLANs() ([]*WakeOnLAN, error) { return nil, errUnsupported } +func (c *client) WakeOnLAN(_ Interface) (*WakeOnLAN, error) { return nil, errUnsupported } +func (c *client) SetWakeOnLAN(_ WakeOnLAN) error { return errUnsupported } +func (c *client) FEC(_ Interface) (*FEC, error) { return nil, errUnsupported } +func (c *client) SetFEC(_ FEC) error { return errUnsupported } +func (c *client) AllPrivateFlags() ([]*PrivateFlags, error) { return nil, errUnsupported } +func (c *client) PrivateFlags(_ Interface) (*PrivateFlags, error) { return nil, errUnsupported } +func (c *client) SetPrivateFlags(_ PrivateFlags) error { return errUnsupported } +func (c *client) Close() error { return errUnsupported } func (f *FEC) Supported() FECModes { return 0 }