2026-07-25 18:07:25 -07:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
"unsafe"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ethtoolIfreq struct {
|
|
|
|
|
name [unix.IFNAMSIZ]byte
|
|
|
|
|
data unsafe.Pointer
|
|
|
|
|
_ [16]byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type flagsIfreq struct {
|
|
|
|
|
name [unix.IFNAMSIZ]byte
|
|
|
|
|
flags uint16
|
|
|
|
|
_ [22]byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ethtoolRingparam struct {
|
|
|
|
|
cmd uint32
|
|
|
|
|
rxMaxPending uint32
|
|
|
|
|
rxMiniMaxPending uint32
|
|
|
|
|
rxJumboMaxPending uint32
|
|
|
|
|
txMaxPending uint32
|
|
|
|
|
rxPending uint32
|
|
|
|
|
rxMiniPending uint32
|
|
|
|
|
rxJumboPending uint32
|
|
|
|
|
txPending uint32
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ethtoolCoalesce struct {
|
|
|
|
|
cmd uint32
|
|
|
|
|
rxCoalesceUsecs uint32
|
|
|
|
|
rxMaxCoalescedFrames uint32
|
|
|
|
|
rxCoalesceUsecsIrq uint32
|
|
|
|
|
rxMaxCoalescedFramesIrq uint32
|
|
|
|
|
txCoalesceUsecs uint32
|
|
|
|
|
txMaxCoalescedFrames uint32
|
|
|
|
|
txCoalesceUsecsIrq uint32
|
|
|
|
|
txMaxCoalescedFramesIrq uint32
|
|
|
|
|
statsBlockCoalesceUsecs uint32
|
|
|
|
|
useAdaptiveRxCoalesce uint32
|
|
|
|
|
useAdaptiveTxCoalesce uint32
|
|
|
|
|
pktRateLow uint32
|
|
|
|
|
rxCoalesceUsecsLow uint32
|
|
|
|
|
rxMaxCoalescedFramesLow uint32
|
|
|
|
|
txCoalesceUsecsLow uint32
|
|
|
|
|
txMaxCoalescedFramesLow uint32
|
|
|
|
|
pktRateHigh uint32
|
|
|
|
|
rxCoalesceUsecsHigh uint32
|
|
|
|
|
rxMaxCoalescedFramesHigh uint32
|
|
|
|
|
txCoalesceUsecsHigh uint32
|
|
|
|
|
txMaxCoalescedFramesHigh uint32
|
|
|
|
|
rateSampleInterval uint32
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type tuneConfig struct {
|
|
|
|
|
governor string
|
|
|
|
|
rxUsecs uint32
|
|
|
|
|
txUsecs uint32
|
|
|
|
|
rxRing uint32
|
|
|
|
|
txRing uint32
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type tuneResult struct {
|
|
|
|
|
item string
|
|
|
|
|
state string
|
|
|
|
|
fixed bool
|
|
|
|
|
fatal bool
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:13:58 -07:00
|
|
|
func (r tuneResult) status() string {
|
2026-07-25 18:07:25 -07:00
|
|
|
switch {
|
|
|
|
|
case r.err != nil:
|
2026-07-25 18:13:58 -07:00
|
|
|
return paint("FAIL", cRed)
|
2026-07-25 18:07:25 -07:00
|
|
|
case r.fixed:
|
2026-07-25 18:13:58 -07:00
|
|
|
return paint("FIXED", cYellow)
|
2026-07-25 18:07:25 -07:00
|
|
|
default:
|
2026-07-25 18:13:58 -07:00
|
|
|
return paint("ok", cGreen)
|
2026-07-25 18:07:25 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:13:58 -07:00
|
|
|
func (r tuneResult) detail() string {
|
|
|
|
|
if r.err != nil {
|
|
|
|
|
if r.state == "" {
|
|
|
|
|
return r.err.Error()
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("%s: %v", r.state, r.err)
|
|
|
|
|
}
|
|
|
|
|
return r.state
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:07:25 -07:00
|
|
|
func ethtoolCall(fd int, ifname string, data unsafe.Pointer) error {
|
|
|
|
|
var ifr ethtoolIfreq
|
|
|
|
|
if len(ifname) >= unix.IFNAMSIZ {
|
|
|
|
|
return fmt.Errorf("interface name %q too long", ifname)
|
|
|
|
|
}
|
|
|
|
|
copy(ifr.name[:], ifname)
|
|
|
|
|
ifr.data = data
|
|
|
|
|
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd),
|
|
|
|
|
uintptr(unix.SIOCETHTOOL), uintptr(unsafe.Pointer(&ifr)))
|
|
|
|
|
if errno != 0 {
|
|
|
|
|
return errno
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getRings(fd int, ifname string) (ethtoolRingparam, error) {
|
|
|
|
|
rp := ethtoolRingparam{cmd: unix.ETHTOOL_GRINGPARAM}
|
|
|
|
|
err := ethtoolCall(fd, ifname, unsafe.Pointer(&rp))
|
|
|
|
|
return rp, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getCoalesce(fd int, ifname string) (ethtoolCoalesce, error) {
|
|
|
|
|
ec := ethtoolCoalesce{cmd: unix.ETHTOOL_GCOALESCE}
|
|
|
|
|
err := ethtoolCall(fd, ifname, unsafe.Pointer(&ec))
|
|
|
|
|
return ec, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:16:34 -07:00
|
|
|
func checkGovernor(want string) tuneResult {
|
2026-07-25 18:07:25 -07:00
|
|
|
res := tuneResult{item: "cpu governor"}
|
2026-07-25 18:16:34 -07:00
|
|
|
paths, err := filepath.Glob("/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor")
|
2026-07-25 18:07:25 -07:00
|
|
|
if err != nil || len(paths) == 0 {
|
|
|
|
|
res.err = fmt.Errorf("no cpufreq governors found")
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
var wrong []string
|
|
|
|
|
counts := map[string]int{}
|
|
|
|
|
for _, p := range paths {
|
|
|
|
|
b, err := os.ReadFile(p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
res.err = err
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
got := strings.TrimSpace(string(b))
|
|
|
|
|
counts[got]++
|
|
|
|
|
if got != want {
|
|
|
|
|
wrong = append(wrong, p)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(wrong) == 0 {
|
|
|
|
|
res.state = fmt.Sprintf("%s on all %d cpus", want, len(paths))
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
var found []string
|
|
|
|
|
for k, v := range counts {
|
|
|
|
|
found = append(found, fmt.Sprintf("%s:%d", k, v))
|
|
|
|
|
}
|
|
|
|
|
for _, p := range wrong {
|
|
|
|
|
if err := os.WriteFile(p, []byte(want), 0o644); err != nil {
|
|
|
|
|
res.err = err
|
|
|
|
|
res.state = fmt.Sprintf("could not set %s", p)
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
res.fixed = true
|
|
|
|
|
res.state = fmt.Sprintf("was %s, now %s on all %d cpus", strings.Join(found, " "), want, len(paths))
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:16:34 -07:00
|
|
|
func checkLinkUp(fd int, ifname string) tuneResult {
|
2026-07-25 18:07:25 -07:00
|
|
|
res := tuneResult{item: ifname + " link up"}
|
|
|
|
|
var ifr flagsIfreq
|
|
|
|
|
copy(ifr.name[:], ifname)
|
|
|
|
|
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd),
|
|
|
|
|
uintptr(unix.SIOCGIFFLAGS), uintptr(unsafe.Pointer(&ifr)))
|
|
|
|
|
if errno != 0 {
|
|
|
|
|
res.err = errno
|
2026-07-25 18:16:34 -07:00
|
|
|
res.fatal = true
|
2026-07-25 18:07:25 -07:00
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
if ifr.flags&unix.IFF_UP != 0 {
|
|
|
|
|
res.state = "up"
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
ifr.flags |= unix.IFF_UP
|
|
|
|
|
_, _, errno = unix.Syscall(unix.SYS_IOCTL, uintptr(fd),
|
|
|
|
|
uintptr(unix.SIOCSIFFLAGS), uintptr(unsafe.Pointer(&ifr)))
|
|
|
|
|
if errno != 0 {
|
|
|
|
|
res.err = errno
|
|
|
|
|
res.state = "could not set IFF_UP"
|
|
|
|
|
res.fatal = true
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
res.fixed = true
|
|
|
|
|
res.state = "was down, now up"
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func checkCarrier(ifname string, wait time.Duration) tuneResult {
|
|
|
|
|
res := tuneResult{item: ifname + " carrier"}
|
|
|
|
|
deadline := time.Now().Add(wait)
|
|
|
|
|
for {
|
|
|
|
|
v, ok := readUint("/sys/class/net/" + ifname + "/carrier")
|
|
|
|
|
if ok && v == 1 {
|
|
|
|
|
res.state = "present"
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
if time.Now().After(deadline) {
|
|
|
|
|
res.err = fmt.Errorf("no carrier after %s", wait)
|
|
|
|
|
res.fatal = true
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:16:34 -07:00
|
|
|
func checkCoalesce(fd int, ifname string, rxUsecs, txUsecs uint32) tuneResult {
|
2026-07-25 18:07:25 -07:00
|
|
|
res := tuneResult{item: ifname + " coalesce"}
|
|
|
|
|
ec, err := getCoalesce(fd, ifname)
|
|
|
|
|
if err != nil {
|
|
|
|
|
res.err = err
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
desc := func(e ethtoolCoalesce) string {
|
|
|
|
|
return fmt.Sprintf("adaptive rx=%d tx=%d rx-usecs=%d tx-usecs=%d",
|
|
|
|
|
e.useAdaptiveRxCoalesce, e.useAdaptiveTxCoalesce, e.rxCoalesceUsecs, e.txCoalesceUsecs)
|
|
|
|
|
}
|
2026-07-25 18:16:34 -07:00
|
|
|
if ec.useAdaptiveRxCoalesce == 0 && ec.useAdaptiveTxCoalesce == 0 &&
|
|
|
|
|
ec.rxCoalesceUsecs == rxUsecs && ec.txCoalesceUsecs == txUsecs {
|
2026-07-25 18:07:25 -07:00
|
|
|
res.state = desc(ec)
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
was := desc(ec)
|
|
|
|
|
ec.cmd = unix.ETHTOOL_SCOALESCE
|
|
|
|
|
ec.useAdaptiveRxCoalesce = 0
|
|
|
|
|
ec.useAdaptiveTxCoalesce = 0
|
|
|
|
|
ec.rxCoalesceUsecs = rxUsecs
|
|
|
|
|
ec.txCoalesceUsecs = txUsecs
|
|
|
|
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&ec)); err != nil {
|
|
|
|
|
res.err = err
|
|
|
|
|
res.state = "could not set"
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
res.fixed = true
|
|
|
|
|
res.state = fmt.Sprintf("was %s, now adaptive off rx-usecs=%d tx-usecs=%d", was, rxUsecs, txUsecs)
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:16:34 -07:00
|
|
|
func checkRings(fd int, ifname string, rxWant, txWant uint32) (tuneResult, bool) {
|
2026-07-25 18:07:25 -07:00
|
|
|
res := tuneResult{item: ifname + " rings"}
|
|
|
|
|
rp, err := getRings(fd, ifname)
|
|
|
|
|
if err != nil {
|
|
|
|
|
res.err = err
|
|
|
|
|
return res, false
|
|
|
|
|
}
|
|
|
|
|
rx := min(rxWant, rp.rxMaxPending)
|
|
|
|
|
tx := min(txWant, rp.txMaxPending)
|
|
|
|
|
if rp.rxPending == rx && rp.txPending == tx {
|
|
|
|
|
res.state = fmt.Sprintf("rx=%d tx=%d", rp.rxPending, rp.txPending)
|
|
|
|
|
return res, false
|
|
|
|
|
}
|
|
|
|
|
was := fmt.Sprintf("rx=%d tx=%d", rp.rxPending, rp.txPending)
|
|
|
|
|
rp.cmd = unix.ETHTOOL_SRINGPARAM
|
|
|
|
|
rp.rxPending = rx
|
|
|
|
|
rp.txPending = tx
|
|
|
|
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&rp)); err != nil {
|
|
|
|
|
res.err = err
|
|
|
|
|
res.state = "could not set"
|
|
|
|
|
return res, false
|
|
|
|
|
}
|
|
|
|
|
res.fixed = true
|
2026-07-25 18:16:34 -07:00
|
|
|
res.state = fmt.Sprintf("was %s, now rx=%d tx=%d (link reset)", was, rx, tx)
|
2026-07-25 18:07:25 -07:00
|
|
|
return res, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func checkNoAddrs(ifname string) tuneResult {
|
|
|
|
|
res := tuneResult{item: ifname + " unmanaged"}
|
|
|
|
|
ifi, err := net.InterfaceByName(ifname)
|
|
|
|
|
if err != nil {
|
|
|
|
|
res.err = err
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
addrs, err := ifi.Addrs()
|
|
|
|
|
if err != nil {
|
|
|
|
|
res.err = err
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
var routable []string
|
|
|
|
|
for _, a := range addrs {
|
|
|
|
|
ipn, ok := a.(*net.IPNet)
|
|
|
|
|
if !ok || ipn.IP.IsLinkLocalUnicast() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
routable = append(routable, a.String())
|
|
|
|
|
}
|
|
|
|
|
if len(routable) == 0 {
|
|
|
|
|
res.state = "no routable addresses"
|
|
|
|
|
return res
|
|
|
|
|
}
|
2026-07-25 18:16:34 -07:00
|
|
|
res.err = fmt.Errorf("has %s; NetworkManager or DHCP is configuring this interface",
|
|
|
|
|
strings.Join(routable, ","))
|
2026-07-25 18:07:25 -07:00
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:16:34 -07:00
|
|
|
func withIoctlSocket(fn func(fd int) []tuneResult) []tuneResult {
|
2026-07-25 18:07:25 -07:00
|
|
|
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
|
|
|
|
if err != nil {
|
2026-07-25 18:16:34 -07:00
|
|
|
return []tuneResult{{item: "ioctl socket", err: err, fatal: true}}
|
2026-07-25 18:07:25 -07:00
|
|
|
}
|
|
|
|
|
defer unix.Close(fd)
|
2026-07-25 18:16:34 -07:00
|
|
|
return fn(fd)
|
|
|
|
|
}
|
2026-07-25 18:07:25 -07:00
|
|
|
|
2026-07-25 18:16:34 -07:00
|
|
|
func applyTuning(cfg tuneConfig, ifnames []string) []tuneResult {
|
|
|
|
|
return withIoctlSocket(func(fd int) []tuneResult {
|
|
|
|
|
out := []tuneResult{checkGovernor(cfg.governor)}
|
|
|
|
|
for _, ifname := range ifnames {
|
|
|
|
|
out = append(out, checkLinkUp(fd, ifname))
|
|
|
|
|
out = append(out, checkCoalesce(fd, ifname, cfg.rxUsecs, cfg.txUsecs))
|
2026-07-25 18:07:25 -07:00
|
|
|
|
2026-07-25 18:16:34 -07:00
|
|
|
carrierWait := 3 * time.Second
|
|
|
|
|
r, reset := checkRings(fd, ifname, cfg.rxRing, cfg.txRing)
|
2026-07-25 18:07:25 -07:00
|
|
|
out = append(out, r)
|
|
|
|
|
if reset {
|
|
|
|
|
carrierWait = 10 * time.Second
|
|
|
|
|
}
|
2026-07-25 18:16:34 -07:00
|
|
|
out = append(out, checkCarrier(ifname, carrierWait))
|
|
|
|
|
out = append(out, checkNoAddrs(ifname))
|
2026-07-25 18:07:25 -07:00
|
|
|
}
|
2026-07-25 18:16:34 -07:00
|
|
|
return out
|
|
|
|
|
})
|
2026-07-25 18:07:25 -07:00
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:13:58 -07:00
|
|
|
func verifyTuning(cfg tuneConfig, ifnames []string) []tuneResult {
|
2026-07-25 18:16:34 -07:00
|
|
|
return withIoctlSocket(func(fd int) []tuneResult {
|
|
|
|
|
var drifted []tuneResult
|
|
|
|
|
if r := checkGovernor(cfg.governor); r.fixed || r.err != nil {
|
2026-07-25 18:13:58 -07:00
|
|
|
drifted = append(drifted, r)
|
2026-07-25 18:07:25 -07:00
|
|
|
}
|
2026-07-25 18:16:34 -07:00
|
|
|
for _, ifname := range ifnames {
|
|
|
|
|
if r := checkCoalesce(fd, ifname, cfg.rxUsecs, cfg.txUsecs); r.fixed || r.err != nil {
|
|
|
|
|
drifted = append(drifted, r)
|
|
|
|
|
}
|
|
|
|
|
r, reset := checkRings(fd, ifname, cfg.rxRing, cfg.txRing)
|
|
|
|
|
if r.fixed || r.err != nil {
|
|
|
|
|
drifted = append(drifted, r)
|
|
|
|
|
}
|
|
|
|
|
if reset {
|
|
|
|
|
if c := checkCarrier(ifname, 10*time.Second); c.err != nil {
|
|
|
|
|
drifted = append(drifted, c)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-25 18:07:25 -07:00
|
|
|
}
|
2026-07-25 18:16:34 -07:00
|
|
|
return drifted
|
|
|
|
|
})
|
2026-07-25 18:07:25 -07:00
|
|
|
}
|