207 lines
6.0 KiB
Go
207 lines
6.0 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|