Files
cabletest/noise.go
T

217 lines
6.2 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 = etherBase + numStreams
)
// 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
// The cycle's up phase: ports admin-up and the wire loud, training or frames.
radiating 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}
}
type noiseView struct {
missing uint64
on bool
}
// missing is 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. on is the cycle's phase, presence granted.
func (n *noiser) view() noiseView {
v := noiseView{on: n.radiating.Load()}
if !n.connected.Load() {
v.missing = 1
}
return v
}
// 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)
n.radiating.Store(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)
n.radiating.Store(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)
}
}