File: ethtool_link_settings.go

package info (click to toggle)
golang-github-safchain-ethtool 0.6.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 280 kB
  • sloc: makefile: 12
file content (324 lines) | stat: -rw-r--r-- 12,023 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
package ethtool

import (
	"errors"
	"fmt"
	"math"
	"strings"
	"syscall"
	"unsafe"

	"golang.org/x/sys/unix"
)

type LinkSettingSource string

// Constants defining the source of the LinkSettings data
const (
	SourceGLinkSettings LinkSettingSource = "GLINKSETTINGS"
	SourceGSet          LinkSettingSource = "GSET"
)

// EthtoolLinkSettingsFixed corresponds to struct ethtool_link_settings fixed part
type EthtoolLinkSettingsFixed struct {
	Cmd                 uint32
	Speed               uint32
	Duplex              uint8
	Port                uint8
	PhyAddress          uint8
	Autoneg             uint8
	MdixSupport         uint8 // Renamed from mdio_support
	EthTpMdix           uint8
	EthTpMdixCtrl       uint8
	LinkModeMasksNwords int8
	Transceiver         uint8
	MasterSlaveCfg      uint8
	MasterSlaveState    uint8
	Reserved1           [1]byte
	Reserved            [7]uint32
	// Flexible array member link_mode_masks[0] starts here implicitly
}

// ethtoolLinkSettingsRequest includes space for the flexible array members
type ethtoolLinkSettingsRequest struct {
	Settings EthtoolLinkSettingsFixed
	Masks    [3 * MAX_LINK_MODE_MASK_NWORDS]uint32 // Uses MAX_LINK_MODE_MASK_NWORDS constant from ethtool.go
}

// LinkSettings is the user-friendly representation returned by GetLinkSettings
type LinkSettings struct {
	Speed                uint32
	Duplex               uint8
	Port                 uint8
	PhyAddress           uint8
	Autoneg              uint8
	MdixSupport          uint8
	EthTpMdix            uint8
	EthTpMdixCtrl        uint8
	Transceiver          uint8
	MasterSlaveCfg       uint8
	MasterSlaveState     uint8
	SupportedLinkModes   []string
	AdvertisingLinkModes []string
	LpAdvertisingModes   []string
	Source               LinkSettingSource // "GSET" or "GLINKSETTINGS"
}

// GetLinkSettings retrieves link settings, preferring ETHTOOL_GLINKSETTINGS and falling back to ETHTOOL_GSET.
// Uses a single ioctl call with the maximum expected buffer size.
func (e *Ethtool) GetLinkSettings(intf string) (*LinkSettings, error) {
	// 1. Attempt ETHTOOL_GLINKSETTINGS with max buffer size
	var req ethtoolLinkSettingsRequest
	req.Settings.Cmd = ETHTOOL_GLINKSETTINGS
	// Provide the maximum expected nwords based on our constant
	req.Settings.LinkModeMasksNwords = int8(MAX_LINK_MODE_MASK_NWORDS)

	err := e.ioctl(intf, uintptr(unsafe.Pointer(&req)))
	fallbackReason := ""

	var errno syscall.Errno
	switch {
	case errors.As(err, &errno) && errors.Is(errno, unix.EOPNOTSUPP):
		// Condition 1: ioctl returned EOPNOTSUPP
		fallbackReason = "EOPNOTSUPP"
	case err == nil:
		// Condition 2: ioctl succeeded, but nwords might be invalid or buffer too small
		nwords := int(req.Settings.LinkModeMasksNwords)
		switch {
		case nwords <= 0 || nwords > MAX_LINK_MODE_MASK_NWORDS:
			// Sub-case 2a: Invalid nwords -> fallback
			fmt.Printf("Warning: GLINKSETTINGS succeeded but returned invalid nwords (%d), attempting fallback to GSET\n", nwords)
			fallbackReason = "invalid nwords from GLINKSETTINGS"
		case 3*nwords > len(req.Masks):
			// Sub-case 2b: Buffer too small -> error
			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)
		default:
			// Sub-case 2c: Success (nwords valid and buffer sufficient)
			results := &LinkSettings{
				Speed:                req.Settings.Speed,
				Duplex:               req.Settings.Duplex,
				Port:                 req.Settings.Port,
				PhyAddress:           req.Settings.PhyAddress,
				Autoneg:              req.Settings.Autoneg,
				MdixSupport:          req.Settings.MdixSupport,
				EthTpMdix:            req.Settings.EthTpMdix,
				EthTpMdixCtrl:        req.Settings.EthTpMdixCtrl,
				Transceiver:          req.Settings.Transceiver,
				MasterSlaveCfg:       req.Settings.MasterSlaveCfg,
				MasterSlaveState:     req.Settings.MasterSlaveState,
				SupportedLinkModes:   parseLinkModeMasks(req.Masks[0*nwords : 1*nwords]),
				AdvertisingLinkModes: parseLinkModeMasks(req.Masks[1*nwords : 2*nwords]),
				LpAdvertisingModes:   parseLinkModeMasks(req.Masks[2*nwords : 3*nwords]),
				Source:               SourceGLinkSettings,
			}
			return results, nil
		}
	default:
		// Condition 3: ioctl failed with an error other than EOPNOTSUPP
		// No fallback in this case.
		return nil, fmt.Errorf("ETHTOOL_GLINKSETTINGS ioctl failed: %w", err)
	}

	// Fallback to ETHTOOL_GSET using e.CmdGet
	var cmd EthtoolCmd
	_, errGet := e.CmdGet(&cmd, intf)
	if errGet != nil {
		return nil, fmt.Errorf("ETHTOOL_GLINKSETTINGS failed (%s), fallback ETHTOOL_GSET (CmdGet) also failed: %w", fallbackReason, errGet)
	}
	results := convertCmdToLinkSettings(&cmd)
	results.Source = SourceGSet
	return results, nil
}

// SetLinkSettings applies link settings, determining whether to use ETHTOOL_SLINKSETTINGS or ETHTOOL_SSET.
func (e *Ethtool) SetLinkSettings(intf string, settings *LinkSettings) error {
	var checkReq ethtoolLinkSettingsRequest
	checkReq.Settings.Cmd = ETHTOOL_GLINKSETTINGS
	checkReq.Settings.LinkModeMasksNwords = int8(MAX_LINK_MODE_MASK_NWORDS)

	errGLinkSettings := e.ioctl(intf, uintptr(unsafe.Pointer(&checkReq)))
	canUseGLinkSettings := false
	nwords := 0

	if errGLinkSettings == nil {
		nwords = int(checkReq.Settings.LinkModeMasksNwords)
		if nwords <= 0 || nwords > MAX_LINK_MODE_MASK_NWORDS {
			return fmt.Errorf("ETHTOOL_GLINKSETTINGS check succeeded but returned invalid nwords: %d", nwords)
		}
		canUseGLinkSettings = true
	} else {
		var errno syscall.Errno
		if !errors.As(errGLinkSettings, &errno) || !errors.Is(errno, unix.EOPNOTSUPP) {
			return fmt.Errorf("checking support via ETHTOOL_GLINKSETTINGS failed: %w", errGLinkSettings)
		}
	}

	if canUseGLinkSettings {
		var setReq ethtoolLinkSettingsRequest
		if 3*nwords > len(setReq.Masks) {
			return fmt.Errorf("internal error: required nwords (%d) exceeds allocated buffer (%d)", nwords, MAX_LINK_MODE_MASK_NWORDS)
		}
		setReq.Settings.Cmd = ETHTOOL_SLINKSETTINGS
		setReq.Settings.Speed = settings.Speed
		setReq.Settings.Duplex = settings.Duplex
		setReq.Settings.Port = settings.Port
		setReq.Settings.PhyAddress = settings.PhyAddress
		setReq.Settings.Autoneg = settings.Autoneg
		setReq.Settings.EthTpMdixCtrl = settings.EthTpMdixCtrl
		setReq.Settings.MasterSlaveCfg = settings.MasterSlaveCfg
		setReq.Settings.LinkModeMasksNwords = int8(nwords)

		advertisingMask := buildLinkModeMask(settings.AdvertisingLinkModes, nwords)
		if len(advertisingMask) != nwords {
			return fmt.Errorf("failed to build advertising mask with correct size (%d != %d)", len(advertisingMask), nwords)
		}
		copy(setReq.Masks[nwords:2*nwords], advertisingMask)
		zeroMaskSupported := make([]uint32, nwords)
		zeroMaskLp := make([]uint32, nwords)
		copy(setReq.Masks[0*nwords:1*nwords], zeroMaskSupported)
		copy(setReq.Masks[2*nwords:3*nwords], zeroMaskLp)

		if err := e.ioctl(intf, uintptr(unsafe.Pointer(&setReq))); err != nil {
			return fmt.Errorf("ETHTOOL_SLINKSETTINGS ioctl failed: %w", err)
		}
		return nil

	}
	// Check if trying to set high bits when only SSET is available
	advertisingMaskCheck := buildLinkModeMask(settings.AdvertisingLinkModes, MAX_LINK_MODE_MASK_NWORDS)
	for i := 1; i < len(advertisingMaskCheck); i++ {
		if advertisingMaskCheck[i] != 0 {
			return fmt.Errorf("cannot set link modes beyond 32 bits using legacy ETHTOOL_SSET; device does not support ETHTOOL_SLINKSETTINGS")
		}
	}

	// Fallback to SSET
	cmd := convertLinkSettingsToCmd(settings)
	_, errSet := e.CmdSet(cmd, intf)
	if errSet != nil {
		return fmt.Errorf("ETHTOOL_SLINKSETTINGS not supported, fallback ETHTOOL_SSET (CmdSet) failed: %w", errSet)
	}
	return nil
}

// parseLinkModeMasks converts a slice of uint32 bitmasks to a list of mode names.
// It filters out non-speed/duplex modes (like TP, Autoneg, Pause).
func parseLinkModeMasks(mask []uint32) []string {
	modes := make([]string, 0, 8)
	for _, capability := range supportedCapabilities {
		// Only include capabilities that represent a speed/duplex mode
		if capability.speed > 0 {
			bitIndex := int(capability.mask)
			wordIndex := bitIndex / 32
			bitInWord := uint(bitIndex % 32)
			if wordIndex < len(mask) && (mask[wordIndex]>>(bitInWord))&1 != 0 {
				modes = append(modes, capability.name)
			}
		}
	}
	return modes
}

// buildLinkModeMask converts a list of mode names back into a uint32 bitmask slice.
// It filters out non-speed/duplex modes.
func buildLinkModeMask(modes []string, nwords int) []uint32 {
	if nwords <= 0 || nwords > MAX_LINK_MODE_MASK_NWORDS {
		return make([]uint32, 0)
	}
	mask := make([]uint32, nwords)
	modeMap := make(map[string]struct {
		bitIndex int
		speed    uint64
	})
	for _, capability := range supportedCapabilities {
		// Only consider capabilities that represent a speed/duplex mode
		if capability.speed > 0 {
			modeMap[capability.name] = struct {
				bitIndex int
				speed    uint64
			}{bitIndex: int(capability.mask), speed: capability.speed}
		}
	}
	for _, modeName := range modes {
		if info, ok := modeMap[strings.TrimSpace(modeName)]; ok {
			wordIndex := info.bitIndex / 32
			bitInWord := uint(info.bitIndex % 32)
			if wordIndex < nwords {
				mask[wordIndex] |= 1 << bitInWord
			} else {
				fmt.Printf("Warning: Link mode '%s' (bit %d) exceeds device's mask size (%d words)\n", modeName, info.bitIndex, nwords)
			}
		} else {
			// Check if the user provided a non-speed mode name - ignore it for the mask, maybe warn?
			isKnownNonSpeed := false
			for _, capability := range supportedCapabilities {
				if capability.speed == 0 && capability.name == strings.TrimSpace(modeName) {
					isKnownNonSpeed = true
					break
				}
			}
			if !isKnownNonSpeed {
				fmt.Printf("Warning: Unknown link mode '%s' specified for mask building\n", modeName)
			} // Silently ignore known non-speed modes like Autoneg, TP, Pause for the mask
		}
	}
	return mask
}

// convertCmdToLinkSettings converts data from the legacy EthtoolCmd to the new LinkSettings format.
func convertCmdToLinkSettings(cmd *EthtoolCmd) *LinkSettings {
	ls := &LinkSettings{
		Speed:                (uint32(cmd.Speed_hi) << 16) | uint32(cmd.Speed),
		Duplex:               cmd.Duplex,
		Port:                 cmd.Port,
		PhyAddress:           cmd.Phy_address,
		Autoneg:              cmd.Autoneg,
		MdixSupport:          cmd.Mdio_support,
		EthTpMdix:            cmd.Eth_tp_mdix,
		EthTpMdixCtrl:        ETH_TP_MDI_INVALID,
		Transceiver:          cmd.Transceiver,
		MasterSlaveCfg:       0, // No equivalent in EthtoolCmd
		MasterSlaveState:     0, // No equivalent in EthtoolCmd
		SupportedLinkModes:   parseLegacyLinkModeMask(cmd.Supported),
		AdvertisingLinkModes: parseLegacyLinkModeMask(cmd.Advertising),
		LpAdvertisingModes:   parseLegacyLinkModeMask(cmd.Lp_advertising),
	}
	if cmd.Speed == math.MaxUint16 && cmd.Speed_hi == math.MaxUint16 {
		ls.Speed = SPEED_UNKNOWN // GSET uses 0xFFFF/0xFFFF for unknown/auto
	}
	return ls
}

// parseLegacyLinkModeMask helper for converting single uint32 mask.
func parseLegacyLinkModeMask(mask uint32) []string {
	return parseLinkModeMasks([]uint32{mask})
}

// convertLinkSettingsToCmd converts new LinkSettings data back to the legacy EthtoolCmd format for SSET fallback.
func convertLinkSettingsToCmd(ls *LinkSettings) *EthtoolCmd {
	cmd := &EthtoolCmd{}
	if ls.Speed == 0 || ls.Speed == SPEED_UNKNOWN {
		cmd.Speed = math.MaxUint16
		cmd.Speed_hi = math.MaxUint16
	} else {
		cmd.Speed = uint16(ls.Speed & 0xFFFF)
		cmd.Speed_hi = uint16((ls.Speed >> 16) & 0xFFFF)
	}
	cmd.Duplex = ls.Duplex
	cmd.Port = ls.Port
	cmd.Phy_address = ls.PhyAddress
	cmd.Autoneg = ls.Autoneg
	// Cannot set EthTpMdixCtrl via EthtoolCmd
	cmd.Transceiver = ls.Transceiver
	cmd.Advertising = buildLegacyLinkModeMask(ls.AdvertisingLinkModes)
	return cmd
}

// buildLegacyLinkModeMask helper for building single uint32 mask from names.
func buildLegacyLinkModeMask(modes []string) uint32 {
	maskSlice := buildLinkModeMask(modes, 1)
	if len(maskSlice) > 0 {
		return maskSlice[0]
	}
	return 0
}