Skip to content

Commit 6d66090

Browse files
authored
Added Link Settings (GLINKSETTINGS/SLINKSETTINGS) Support (#102)
* add support ETHTOOL_GLINKSETTINGS ETHTOOL_SLINKSETTINGS * fix review
1 parent f1c48ce commit 6d66090

File tree

5 files changed

+571
-33
lines changed

5 files changed

+571
-33
lines changed

ethtool.go

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@ const (
5454
ETH_SS_FEATURES = 4
5555

5656
// CMD supported
57-
ETHTOOL_GSET = 0x00000001 /* Get settings. */
58-
ETHTOOL_SSET = 0x00000002 /* Set settings. */
59-
ETHTOOL_GWOL = 0x00000005 /* Get wake-on-lan options. */
60-
ETHTOOL_SWOL = 0x00000006 /* Set wake-on-lan options. */
61-
ETHTOOL_GDRVINFO = 0x00000003 /* Get driver info. */
62-
ETHTOOL_GMSGLVL = 0x00000007 /* Get driver message level */
63-
ETHTOOL_SMSGLVL = 0x00000008 /* Set driver msg level. */
57+
ETHTOOL_GSET = 0x00000001 /* Get settings. */
58+
ETHTOOL_SSET = 0x00000002 /* Set settings. */
59+
ETHTOOL_GWOL = 0x00000005 /* Get wake-on-lan options. */
60+
ETHTOOL_SWOL = 0x00000006 /* Set wake-on-lan options. */
61+
ETHTOOL_GDRVINFO = 0x00000003 /* Get driver info. */
62+
ETHTOOL_GMSGLVL = 0x00000007 /* Get driver message level */
63+
ETHTOOL_SMSGLVL = 0x00000008 /* Set driver msg level. */
64+
ETHTOOL_GLINKSETTINGS = unix.ETHTOOL_GLINKSETTINGS // 0x4c
65+
ETHTOOL_SLINKSETTINGS = unix.ETHTOOL_SLINKSETTINGS // 0x4d
6466

6567
// Get link status for host, i.e. whether the interface *and* the
6668
// physical port (if there is one) are up (ethtool_value).
@@ -89,6 +91,38 @@ const (
8991
ETHTOOL_GRXFHINDIR = 0x00000038 /* Get RX flow hash indir'n table */
9092
ETHTOOL_SRXFHINDIR = 0x00000039 /* Set RX flow hash indir'n table */
9193
ETH_RXFH_INDIR_NO_CHANGE = 0xFFFFFFFF
94+
95+
// Speed and Duplex unknowns/constants (Manually defined based on <linux/ethtool.h>)
96+
SPEED_UNKNOWN = 0xffffffff // ((__u32)-1) SPEED_UNKNOWN
97+
DUPLEX_HALF = 0x00 // DUPLEX_HALF
98+
DUPLEX_FULL = 0x01 // DUPLEX_FULL
99+
DUPLEX_UNKNOWN = 0xff // DUPLEX_UNKNOWN
100+
101+
// Port types (Manually defined based on <linux/ethtool.h>)
102+
PORT_TP = 0x00 // PORT_TP
103+
PORT_AUI = 0x01 // PORT_AUI
104+
PORT_MII = 0x02 // PORT_MII
105+
PORT_FIBRE = 0x03 // PORT_FIBRE
106+
PORT_BNC = 0x04 // PORT_BNC
107+
PORT_DA = 0x05 // PORT_DA
108+
PORT_NONE = 0xef // PORT_NONE
109+
PORT_OTHER = 0xff // PORT_OTHER
110+
111+
// Autoneg settings (Manually defined based on <linux/ethtool.h>)
112+
AUTONEG_DISABLE = 0x00 // AUTONEG_DISABLE
113+
AUTONEG_ENABLE = 0x01 // AUTONEG_ENABLE
114+
115+
// MDIX states (Manually defined based on <linux/ethtool.h>)
116+
ETH_TP_MDI_INVALID = 0x00 // ETH_TP_MDI_INVALID
117+
ETH_TP_MDI = 0x01 // ETH_TP_MDI
118+
ETH_TP_MDI_X = 0x02 // ETH_TP_MDI_X
119+
ETH_TP_MDI_AUTO = 0x03 // Control value ETH_TP_MDI_AUTO
120+
121+
// Link mode mask bits count (Manually defined based on ethtool.h)
122+
ETHTOOL_LINK_MODE_MASK_NBITS = 92 // __ETHTOOL_LINK_MODE_MASK_NBITS
123+
124+
// Calculate max nwords based on NBITS using the manually defined constant
125+
MAX_LINK_MODE_MASK_NWORDS = (ETHTOOL_LINK_MODE_MASK_NBITS + 31) / 32 // = 3
92126
)
93127

94128
// MAX_GSTRINGS maximum number of stats entries that ethtool can
@@ -1253,7 +1287,7 @@ func supportedSpeeds(mask uint64) (ret []struct {
12531287
speed uint64
12541288
}) {
12551289
for _, mode := range supportedCapabilities {
1256-
if ((1 << mode.mask) & mask) != 0 {
1290+
if mode.speed > 0 && ((1<<mode.mask)&mask) != 0 {
12571291
ret = append(ret, mode)
12581292
}
12591293
}

ethtool_link_settings.go

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
package ethtool
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"math"
7+
"strings"
8+
"syscall"
9+
"unsafe"
10+
11+
"golang.org/x/sys/unix"
12+
)
13+
14+
type LinkSettingSource string
15+
16+
// Constants defining the source of the LinkSettings data
17+
const (
18+
SourceGLinkSettings LinkSettingSource = "GLINKSETTINGS"
19+
SourceGSet LinkSettingSource = "GSET"
20+
)
21+
22+
// EthtoolLinkSettingsFixed corresponds to struct ethtool_link_settings fixed part
23+
type EthtoolLinkSettingsFixed struct {
24+
Cmd uint32
25+
Speed uint32
26+
Duplex uint8
27+
Port uint8
28+
PhyAddress uint8
29+
Autoneg uint8
30+
MdixSupport uint8 // Renamed from mdio_support
31+
EthTpMdix uint8
32+
EthTpMdixCtrl uint8
33+
LinkModeMasksNwords int8
34+
Transceiver uint8
35+
MasterSlaveCfg uint8
36+
MasterSlaveState uint8
37+
Reserved1 [1]byte
38+
Reserved [7]uint32
39+
// Flexible array member link_mode_masks[0] starts here implicitly
40+
}
41+
42+
// ethtoolLinkSettingsRequest includes space for the flexible array members
43+
type ethtoolLinkSettingsRequest struct {
44+
Settings EthtoolLinkSettingsFixed
45+
Masks [3 * MAX_LINK_MODE_MASK_NWORDS]uint32 // Uses MAX_LINK_MODE_MASK_NWORDS constant from ethtool.go
46+
}
47+
48+
// LinkSettings is the user-friendly representation returned by GetLinkSettings
49+
type LinkSettings struct {
50+
Speed uint32
51+
Duplex uint8
52+
Port uint8
53+
PhyAddress uint8
54+
Autoneg uint8
55+
MdixSupport uint8
56+
EthTpMdix uint8
57+
EthTpMdixCtrl uint8
58+
Transceiver uint8
59+
MasterSlaveCfg uint8
60+
MasterSlaveState uint8
61+
SupportedLinkModes []string
62+
AdvertisingLinkModes []string
63+
LpAdvertisingModes []string
64+
Source LinkSettingSource // "GSET" or "GLINKSETTINGS"
65+
}
66+
67+
// GetLinkSettings retrieves link settings, preferring ETHTOOL_GLINKSETTINGS and falling back to ETHTOOL_GSET.
68+
// Uses a single ioctl call with the maximum expected buffer size.
69+
func (e *Ethtool) GetLinkSettings(intf string) (*LinkSettings, error) {
70+
// 1. Attempt ETHTOOL_GLINKSETTINGS with max buffer size
71+
var req ethtoolLinkSettingsRequest
72+
req.Settings.Cmd = ETHTOOL_GLINKSETTINGS
73+
// Provide the maximum expected nwords based on our constant
74+
req.Settings.LinkModeMasksNwords = int8(MAX_LINK_MODE_MASK_NWORDS)
75+
76+
err := e.ioctl(intf, uintptr(unsafe.Pointer(&req)))
77+
fallbackReason := ""
78+
79+
var errno syscall.Errno
80+
switch {
81+
case errors.As(err, &errno) && errors.Is(errno, unix.EOPNOTSUPP):
82+
// Condition 1: ioctl returned EOPNOTSUPP
83+
fallbackReason = "EOPNOTSUPP"
84+
case err == nil:
85+
// Condition 2: ioctl succeeded, but nwords might be invalid or buffer too small
86+
nwords := int(req.Settings.LinkModeMasksNwords)
87+
switch {
88+
case nwords <= 0 || nwords > MAX_LINK_MODE_MASK_NWORDS:
89+
// Sub-case 2a: Invalid nwords -> fallback
90+
fmt.Printf("Warning: GLINKSETTINGS succeeded but returned invalid nwords (%d), attempting fallback to GSET\n", nwords)
91+
fallbackReason = "invalid nwords from GLINKSETTINGS"
92+
case 3*nwords > len(req.Masks):
93+
// Sub-case 2b: Buffer too small -> error
94+
return nil, fmt.Errorf("kernel requires %d words for GLINKSETTINGS, buffer only has space for %d (max %d)", nwords, len(req.Masks)/3, MAX_LINK_MODE_MASK_NWORDS)
95+
default:
96+
// Sub-case 2c: Success (nwords valid and buffer sufficient)
97+
results := &LinkSettings{
98+
Speed: req.Settings.Speed,
99+
Duplex: req.Settings.Duplex,
100+
Port: req.Settings.Port,
101+
PhyAddress: req.Settings.PhyAddress,
102+
Autoneg: req.Settings.Autoneg,
103+
MdixSupport: req.Settings.MdixSupport,
104+
EthTpMdix: req.Settings.EthTpMdix,
105+
EthTpMdixCtrl: req.Settings.EthTpMdixCtrl,
106+
Transceiver: req.Settings.Transceiver,
107+
MasterSlaveCfg: req.Settings.MasterSlaveCfg,
108+
MasterSlaveState: req.Settings.MasterSlaveState,
109+
SupportedLinkModes: parseLinkModeMasks(req.Masks[0*nwords : 1*nwords]),
110+
AdvertisingLinkModes: parseLinkModeMasks(req.Masks[1*nwords : 2*nwords]),
111+
LpAdvertisingModes: parseLinkModeMasks(req.Masks[2*nwords : 3*nwords]),
112+
Source: SourceGLinkSettings,
113+
}
114+
return results, nil
115+
}
116+
default:
117+
// Condition 3: ioctl failed with an error other than EOPNOTSUPP
118+
// No fallback in this case.
119+
return nil, fmt.Errorf("ETHTOOL_GLINKSETTINGS ioctl failed: %w", err)
120+
}
121+
122+
// Fallback to ETHTOOL_GSET using e.CmdGet
123+
var cmd EthtoolCmd
124+
_, errGet := e.CmdGet(&cmd, intf)
125+
if errGet != nil {
126+
return nil, fmt.Errorf("ETHTOOL_GLINKSETTINGS failed (%s), fallback ETHTOOL_GSET (CmdGet) also failed: %w", fallbackReason, errGet)
127+
}
128+
results := convertCmdToLinkSettings(&cmd)
129+
results.Source = SourceGSet
130+
return results, nil
131+
}
132+
133+
// SetLinkSettings applies link settings, determining whether to use ETHTOOL_SLINKSETTINGS or ETHTOOL_SSET.
134+
func (e *Ethtool) SetLinkSettings(intf string, settings *LinkSettings) error {
135+
var checkReq ethtoolLinkSettingsRequest
136+
checkReq.Settings.Cmd = ETHTOOL_GLINKSETTINGS
137+
checkReq.Settings.LinkModeMasksNwords = int8(MAX_LINK_MODE_MASK_NWORDS)
138+
139+
errGLinkSettings := e.ioctl(intf, uintptr(unsafe.Pointer(&checkReq)))
140+
canUseGLinkSettings := false
141+
nwords := 0
142+
143+
if errGLinkSettings == nil {
144+
nwords = int(checkReq.Settings.LinkModeMasksNwords)
145+
if nwords <= 0 || nwords > MAX_LINK_MODE_MASK_NWORDS {
146+
return fmt.Errorf("ETHTOOL_GLINKSETTINGS check succeeded but returned invalid nwords: %d", nwords)
147+
}
148+
canUseGLinkSettings = true
149+
} else {
150+
var errno syscall.Errno
151+
if !errors.As(errGLinkSettings, &errno) || !errors.Is(errno, unix.EOPNOTSUPP) {
152+
return fmt.Errorf("checking support via ETHTOOL_GLINKSETTINGS failed: %w", errGLinkSettings)
153+
}
154+
}
155+
156+
if canUseGLinkSettings {
157+
var setReq ethtoolLinkSettingsRequest
158+
if 3*nwords > len(setReq.Masks) {
159+
return fmt.Errorf("internal error: required nwords (%d) exceeds allocated buffer (%d)", nwords, MAX_LINK_MODE_MASK_NWORDS)
160+
}
161+
setReq.Settings.Cmd = ETHTOOL_SLINKSETTINGS
162+
setReq.Settings.Speed = settings.Speed
163+
setReq.Settings.Duplex = settings.Duplex
164+
setReq.Settings.Port = settings.Port
165+
setReq.Settings.PhyAddress = settings.PhyAddress
166+
setReq.Settings.Autoneg = settings.Autoneg
167+
setReq.Settings.EthTpMdixCtrl = settings.EthTpMdixCtrl
168+
setReq.Settings.MasterSlaveCfg = settings.MasterSlaveCfg
169+
setReq.Settings.LinkModeMasksNwords = int8(nwords)
170+
171+
advertisingMask := buildLinkModeMask(settings.AdvertisingLinkModes, nwords)
172+
if len(advertisingMask) != nwords {
173+
return fmt.Errorf("failed to build advertising mask with correct size (%d != %d)", len(advertisingMask), nwords)
174+
}
175+
copy(setReq.Masks[nwords:2*nwords], advertisingMask)
176+
zeroMaskSupported := make([]uint32, nwords)
177+
zeroMaskLp := make([]uint32, nwords)
178+
copy(setReq.Masks[0*nwords:1*nwords], zeroMaskSupported)
179+
copy(setReq.Masks[2*nwords:3*nwords], zeroMaskLp)
180+
181+
if err := e.ioctl(intf, uintptr(unsafe.Pointer(&setReq))); err != nil {
182+
return fmt.Errorf("ETHTOOL_SLINKSETTINGS ioctl failed: %w", err)
183+
}
184+
return nil
185+
186+
}
187+
// Check if trying to set high bits when only SSET is available
188+
advertisingMaskCheck := buildLinkModeMask(settings.AdvertisingLinkModes, MAX_LINK_MODE_MASK_NWORDS)
189+
for i := 1; i < len(advertisingMaskCheck); i++ {
190+
if advertisingMaskCheck[i] != 0 {
191+
return fmt.Errorf("cannot set link modes beyond 32 bits using legacy ETHTOOL_SSET; device does not support ETHTOOL_SLINKSETTINGS")
192+
}
193+
}
194+
195+
// Fallback to SSET
196+
cmd := convertLinkSettingsToCmd(settings)
197+
_, errSet := e.CmdSet(cmd, intf)
198+
if errSet != nil {
199+
return fmt.Errorf("ETHTOOL_SLINKSETTINGS not supported, fallback ETHTOOL_SSET (CmdSet) failed: %w", errSet)
200+
}
201+
return nil
202+
}
203+
204+
// parseLinkModeMasks converts a slice of uint32 bitmasks to a list of mode names.
205+
// It filters out non-speed/duplex modes (like TP, Autoneg, Pause).
206+
func parseLinkModeMasks(mask []uint32) []string {
207+
modes := make([]string, 0, 8)
208+
for _, capability := range supportedCapabilities {
209+
// Only include capabilities that represent a speed/duplex mode
210+
if capability.speed > 0 {
211+
bitIndex := int(capability.mask)
212+
wordIndex := bitIndex / 32
213+
bitInWord := uint(bitIndex % 32)
214+
if wordIndex < len(mask) && (mask[wordIndex]>>(bitInWord))&1 != 0 {
215+
modes = append(modes, capability.name)
216+
}
217+
}
218+
}
219+
return modes
220+
}
221+
222+
// buildLinkModeMask converts a list of mode names back into a uint32 bitmask slice.
223+
// It filters out non-speed/duplex modes.
224+
func buildLinkModeMask(modes []string, nwords int) []uint32 {
225+
if nwords <= 0 || nwords > MAX_LINK_MODE_MASK_NWORDS {
226+
return make([]uint32, 0)
227+
}
228+
mask := make([]uint32, nwords)
229+
modeMap := make(map[string]struct {
230+
bitIndex int
231+
speed uint64
232+
})
233+
for _, capability := range supportedCapabilities {
234+
// Only consider capabilities that represent a speed/duplex mode
235+
if capability.speed > 0 {
236+
modeMap[capability.name] = struct {
237+
bitIndex int
238+
speed uint64
239+
}{bitIndex: int(capability.mask), speed: capability.speed}
240+
}
241+
}
242+
for _, modeName := range modes {
243+
if info, ok := modeMap[strings.TrimSpace(modeName)]; ok {
244+
wordIndex := info.bitIndex / 32
245+
bitInWord := uint(info.bitIndex % 32)
246+
if wordIndex < nwords {
247+
mask[wordIndex] |= 1 << bitInWord
248+
} else {
249+
fmt.Printf("Warning: Link mode '%s' (bit %d) exceeds device's mask size (%d words)\n", modeName, info.bitIndex, nwords)
250+
}
251+
} else {
252+
// Check if the user provided a non-speed mode name - ignore it for the mask, maybe warn?
253+
isKnownNonSpeed := false
254+
for _, capability := range supportedCapabilities {
255+
if capability.speed == 0 && capability.name == strings.TrimSpace(modeName) {
256+
isKnownNonSpeed = true
257+
break
258+
}
259+
}
260+
if !isKnownNonSpeed {
261+
fmt.Printf("Warning: Unknown link mode '%s' specified for mask building\n", modeName)
262+
} // Silently ignore known non-speed modes like Autoneg, TP, Pause for the mask
263+
}
264+
}
265+
return mask
266+
}
267+
268+
// convertCmdToLinkSettings converts data from the legacy EthtoolCmd to the new LinkSettings format.
269+
func convertCmdToLinkSettings(cmd *EthtoolCmd) *LinkSettings {
270+
ls := &LinkSettings{
271+
Speed: (uint32(cmd.Speed_hi) << 16) | uint32(cmd.Speed),
272+
Duplex: cmd.Duplex,
273+
Port: cmd.Port,
274+
PhyAddress: cmd.Phy_address,
275+
Autoneg: cmd.Autoneg,
276+
MdixSupport: cmd.Mdio_support,
277+
EthTpMdix: cmd.Eth_tp_mdix,
278+
EthTpMdixCtrl: ETH_TP_MDI_INVALID,
279+
Transceiver: cmd.Transceiver,
280+
MasterSlaveCfg: 0, // No equivalent in EthtoolCmd
281+
MasterSlaveState: 0, // No equivalent in EthtoolCmd
282+
SupportedLinkModes: parseLegacyLinkModeMask(cmd.Supported),
283+
AdvertisingLinkModes: parseLegacyLinkModeMask(cmd.Advertising),
284+
LpAdvertisingModes: parseLegacyLinkModeMask(cmd.Lp_advertising),
285+
}
286+
if cmd.Speed == math.MaxUint16 && cmd.Speed_hi == math.MaxUint16 {
287+
ls.Speed = SPEED_UNKNOWN // GSET uses 0xFFFF/0xFFFF for unknown/auto
288+
}
289+
return ls
290+
}
291+
292+
// parseLegacyLinkModeMask helper for converting single uint32 mask.
293+
func parseLegacyLinkModeMask(mask uint32) []string {
294+
return parseLinkModeMasks([]uint32{mask})
295+
}
296+
297+
// convertLinkSettingsToCmd converts new LinkSettings data back to the legacy EthtoolCmd format for SSET fallback.
298+
func convertLinkSettingsToCmd(ls *LinkSettings) *EthtoolCmd {
299+
cmd := &EthtoolCmd{}
300+
if ls.Speed == 0 || ls.Speed == SPEED_UNKNOWN {
301+
cmd.Speed = math.MaxUint16
302+
cmd.Speed_hi = math.MaxUint16
303+
} else {
304+
cmd.Speed = uint16(ls.Speed & 0xFFFF)
305+
cmd.Speed_hi = uint16((ls.Speed >> 16) & 0xFFFF)
306+
}
307+
cmd.Duplex = ls.Duplex
308+
cmd.Port = ls.Port
309+
cmd.Phy_address = ls.PhyAddress
310+
cmd.Autoneg = ls.Autoneg
311+
// Cannot set EthTpMdixCtrl via EthtoolCmd
312+
cmd.Transceiver = ls.Transceiver
313+
cmd.Advertising = buildLegacyLinkModeMask(ls.AdvertisingLinkModes)
314+
return cmd
315+
}
316+
317+
// buildLegacyLinkModeMask helper for building single uint32 mask from names.
318+
func buildLegacyLinkModeMask(modes []string) uint32 {
319+
maskSlice := buildLinkModeMask(modes, 1)
320+
if len(maskSlice) > 0 {
321+
return maskSlice[0]
322+
}
323+
return 0
324+
}

0 commit comments

Comments
 (0)