Drive a noise cable on the i40e pair, cycling its link down and up since the module PHYs ignore everything softer, and flag its absence beside the error columns
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// The noise cable: a deliberately bad cable twisted around the one under test,
|
||||
// there to radiate into it. Its ports are driven for the interference they
|
||||
// produce, not measured: nothing is ever received from them, and nothing they
|
||||
// count reaches the error columns.
|
||||
//
|
||||
// The wire cannot be quieted by going idle, because without EEE the PHYs
|
||||
// signal at full power whether or not frames flow, and these PHYs live inside
|
||||
// the SFP+ modules where no EEE control reaches them. Closing the port is the
|
||||
// one switch the host actually has, so the cycle is built on it: links up and
|
||||
// carrying frames for a spell, then administratively down for one. Every wake
|
||||
// re-runs 10GBASE-T training, which is as loud as this wire ever gets.
|
||||
const (
|
||||
noiseDriver = "i40e"
|
||||
noiseFrameLen = 1514
|
||||
noiseUpSpan = 5 * time.Second
|
||||
noiseDownSpan = 5 * time.Second
|
||||
noiseFrameGap = 10 * time.Millisecond
|
||||
|
||||
noiseEther uint16 = probeEther + 1
|
||||
)
|
||||
|
||||
// Kernel names shift with which drivers are built in, since ethN is handed out
|
||||
// in link order rather than by slot. The driver name is the one label a port
|
||||
// keeps across kernel configs, so pairs are found by it rather than named.
|
||||
func driverPair(driver string) (string, string, error) {
|
||||
ents, err := os.ReadDir("/sys/class/net")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
var names []string
|
||||
for _, e := range ents {
|
||||
link, err := os.Readlink("/sys/class/net/" + e.Name() + "/device/driver")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if filepath.Base(link) == driver {
|
||||
names = append(names, e.Name())
|
||||
}
|
||||
}
|
||||
if len(names) != 2 {
|
||||
return "", "", fmt.Errorf("want 2 %s interfaces, found %d [%s]",
|
||||
driver, len(names), strings.Join(names, " "))
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names[0], names[1], nil
|
||||
}
|
||||
|
||||
type noisePort struct {
|
||||
name string
|
||||
fd int
|
||||
frame []byte
|
||||
}
|
||||
|
||||
type noiser struct {
|
||||
eps [2]endpoint
|
||||
ports [2]noisePort
|
||||
|
||||
// Whether the cable is judged present: both carriers seen during an up
|
||||
// phase. Latched across the down phase, where the missing carrier is our
|
||||
// own doing and says nothing about the cable.
|
||||
connected atomic.Bool
|
||||
}
|
||||
|
||||
func newNoiser() (*noiser, error) {
|
||||
aName, bName, err := driverPair(noiseDriver)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("noise: %w", err)
|
||||
}
|
||||
a, err := lookupEndpoint(aName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("noise: %w", err)
|
||||
}
|
||||
b, err := lookupEndpoint(bName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("noise: %w", err)
|
||||
}
|
||||
|
||||
n := &noiser{eps: [2]endpoint{a, b}}
|
||||
for i, p := range [][2]endpoint{{a, b}, {b, a}} {
|
||||
fd, err := openTxSocket(p[0].idx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("noise tx socket %s: %w", p[0].name, err)
|
||||
}
|
||||
// The payload is left zero: the PCS scrambles everything on the wire,
|
||||
// so no pattern radiates differently from any other. The frame exists
|
||||
// to occupy the link, not to say anything.
|
||||
frame := make([]byte, noiseFrameLen)
|
||||
copy(frame[0:6], p[1].mac[:])
|
||||
copy(frame[6:12], p[0].mac[:])
|
||||
binary.BigEndian.PutUint16(frame[12:14], noiseEther)
|
||||
n.ports[i] = noisePort{name: p[0].name, fd: fd, frame: frame}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (n *noiser) names() []string {
|
||||
return []string{n.eps[0].name, n.eps[1].name}
|
||||
}
|
||||
|
||||
// Zero while the cable was there at the last verdict, one while it was not:
|
||||
// the shape the error cells already colour by, so absence paints as the fault
|
||||
// it is and presence as the usual green.
|
||||
func (n *noiser) missing() uint64 {
|
||||
if n.connected.Load() {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// The ports were reachable when the noiser was built, so one that stops taking
|
||||
// the ioctl now is the interface going away underneath us, the same fault the
|
||||
// counter reads stop for.
|
||||
func (n *noiser) setLinks(fd int, up bool) {
|
||||
for i := range n.ports {
|
||||
var ifr flagsIfreq
|
||||
copy(ifr.name[:], n.ports[i].name)
|
||||
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd),
|
||||
uintptr(unix.SIOCGIFFLAGS), uintptr(unsafe.Pointer(&ifr))); errno != 0 {
|
||||
panic(fmt.Sprintf("reading %s flags: %v", n.ports[i].name, errno))
|
||||
}
|
||||
if up {
|
||||
ifr.flags |= unix.IFF_UP
|
||||
} else {
|
||||
ifr.flags &^= unix.IFF_UP
|
||||
}
|
||||
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd),
|
||||
uintptr(unix.SIOCSIFFLAGS), uintptr(unsafe.Pointer(&ifr))); errno != 0 {
|
||||
panic(fmt.Sprintf("setting %s flags: %v", n.ports[i].name, errno))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Down reads as EINVAL rather than zero, and either way the answer is the
|
||||
// same: no carrier here now.
|
||||
func carrierUp(name string) bool {
|
||||
v, ok := readUint("/sys/class/net/" + name + "/carrier")
|
||||
return ok && v == 1
|
||||
}
|
||||
|
||||
func (n *noiser) bothUp() bool {
|
||||
return carrierUp(n.ports[0].name) && carrierUp(n.ports[1].name)
|
||||
}
|
||||
|
||||
// Send results are deliberately dropped: the cable is bad on purpose, the link
|
||||
// comes and goes under the cycle, and a frame this side declined to send is as
|
||||
// good as one the wire mangled. What matters is only ever what the test cable
|
||||
// counted.
|
||||
func (n *noiser) run(done *atomic.Bool) {
|
||||
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("noise ioctl socket: %v", err))
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
tick := time.NewTicker(noiseFrameGap)
|
||||
defer tick.Stop()
|
||||
|
||||
for !done.Load() {
|
||||
n.setLinks(fd, true)
|
||||
linked := false
|
||||
for end := time.Now().Add(noiseUpSpan); time.Now().Before(end) && !done.Load(); {
|
||||
<-tick.C
|
||||
if !n.bothUp() {
|
||||
continue
|
||||
}
|
||||
linked = true
|
||||
n.connected.Store(true)
|
||||
for i := range n.ports {
|
||||
unix.Write(n.ports[i].fd, n.ports[i].frame)
|
||||
}
|
||||
}
|
||||
// A whole up phase with no link is many times the ~1s the wire needs
|
||||
// to train, so by now the silence is the cable's answer.
|
||||
n.connected.Store(linked)
|
||||
|
||||
n.setLinks(fd, false)
|
||||
for end := time.Now().Add(noiseDownSpan); time.Now().Before(end) && !done.Load(); {
|
||||
<-tick.C
|
||||
}
|
||||
}
|
||||
// Left up rather than wherever the cycle stopped, so a run never strands
|
||||
// the ports down for whoever looks next.
|
||||
n.setLinks(fd, true)
|
||||
}
|
||||
|
||||
func (n *noiser) close() {
|
||||
for i := range n.ports {
|
||||
unix.Close(n.ports[i].fd)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user