Skip to content

Commit

Permalink
ethtool: add support for private flags
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lorenz committed Nov 22, 2023
1 parent 7823329 commit cbdcb31
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 1 deletion.
56 changes: 56 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Check failure on line 322 in client.go

View workflow job for this annotation

GitHub Actions / build (1.19, macos-latest)

too many arguments in call to c.c.PrivateFlags

Check failure on line 322 in client.go

View workflow job for this annotation

GitHub Actions / build (1.20, macos-latest)

too many arguments in call to c.c.PrivateFlags
}

// 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() }
132 changes: 131 additions & 1 deletion client_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}

Expand Down
55 changes: 55 additions & 0 deletions client_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()

Expand Down
3 changes: 3 additions & 0 deletions client_others.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func (c *client) WakeOnLAN(_ Interface) (*WakeOnLAN, error) { return nil, errUns
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() (*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 }
Expand Down

0 comments on commit cbdcb31

Please sign in to comment.