Files
tsnet-proxy/vendor/github.com/safchain/ethtool/ethtool_link_settings.go
T
2026-10-22 12:27:19 +01:00

325 lines
12 KiB
Go

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
}