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
}
|