blob: dbcc79e9033a166aef933554847facea946b8cac [file] [log] [blame]
// Package lacp contains an integration test for our custom LACP patches.
// It tests relevant behavior that other parts of the Monogon network stack
// rely on, like proper carrier state indications.
package lacp
import (
"hash/fnv"
"math"
"net"
"os"
"strings"
"testing"
"time"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)
// createVethPair is a helper creating a pair of virtual network interfaces
// acting as a cable (i.e. traffic going in one interface comes back out on
// the other one). Both ends are returned.
func createVethPair(t *testing.T, name string) (*netlink.Veth, *netlink.Veth) {
t.Helper()
vethLink := netlink.Veth{
LinkAttrs: netlink.LinkAttrs{
Name: name + "a",
NetNsID: -1,
TxQLen: -1,
},
PeerName: name + "b",
}
if err := netlink.LinkAdd(&vethLink); err != nil {
t.Fatalf("while creating veth pair: %v", err)
}
vethLinkB, err := netlink.LinkByName(name + "b")
if err != nil {
t.Fatalf("while creating veth pair: while getting veth peer: %v", err)
}
return &vethLink, vethLinkB.(*netlink.Veth)
}
// setupNetem is a helper for setting up Linux's network emulation queuing
// discipline on a network interface, which can simulate various network
// imperfections like extra latency, reordering or packet loss on packets
// transmitted on the interface specified. As it is internally implemented
// as a queuing discipline it only affects transmitted packets.
func setupNetem(t *testing.T, link netlink.Link, conf netlink.NetemQdiscAttrs) *netlink.Netem {
t.Helper()
h := fnv.New32a()
h.Write([]byte(link.Attrs().Name))
qdisc := netlink.NewNetem(netlink.QdiscAttrs{
LinkIndex: link.Attrs().Index,
Handle: netlink.MakeHandle(uint16(h.Sum32()%math.MaxUint16), 0),
Parent: netlink.HANDLE_ROOT,
}, conf)
if err := netlink.QdiscAdd(qdisc); err != nil {
t.Fatalf("while setting up qdisc netem for %q: %v", link.Attrs().Name, err)
}
return qdisc
}
// changeNetem is a helper for reconfiguring an existing netem instance
// on-the-fly with new parameters.
func changeNetem(t *testing.T, qdisc *netlink.Netem, conf netlink.NetemQdiscAttrs) {
t.Helper()
changedQd := netlink.NewNetem(qdisc.QdiscAttrs, conf)
if err := netlink.QdiscChange(changedQd); err != nil {
t.Fatalf("while changing qdisc netem for link index %v: %v", qdisc.LinkIndex, err)
}
}
func createBond(t *testing.T, name string, links ...netlink.Link) *netlink.Bond {
t.Helper()
bondLink := netlink.NewLinkBond(netlink.LinkAttrs{
Name: name,
NetNsID: -1,
TxQLen: -1,
Flags: net.FlagUp,
})
bondLink.Mode = netlink.BOND_MODE_802_3AD
bondLink.LacpRate = netlink.BOND_LACP_RATE_FAST
bondLink.MinLinks = 1
bondLink.AdSelect = netlink.BOND_AD_SELECT_BANDWIDTH
if err := netlink.LinkAdd(bondLink); err != nil {
t.Fatalf("while creating bond: %v", err)
}
for _, l := range links {
if err := netlink.LinkSetBondSlave(l, bondLink); err != nil {
t.Fatalf("while enslaving link to bond %q: %v", name, err)
}
}
return bondLink
}
// assertRunning is a helper for asserting an interface's IFF_RUNNING state.
func assertRunning(t *testing.T, l netlink.Link, expected bool) {
t.Helper()
linkCurrent, err := netlink.LinkByIndex(l.Attrs().Index)
if err != nil {
t.Fatalf("while checking if link %q is running: %v", l.Attrs().Name, err)
}
is := linkCurrent.Attrs().RawFlags&unix.IFF_RUNNING != 0
if expected != is {
t.Errorf("expected interface %q running state to be %v, is %v", l.Attrs().Name, expected, is)
}
}
func TestLACP(t *testing.T) {
if os.Getenv("IN_KTEST") != "true" {
t.Skip("Not in ktest")
}
if err := os.WriteFile("/sys/kernel/debug/dynamic_debug/control", []byte("module bonding +p"), 0); err != nil {
t.Fatal(err)
}
// Log dynamic debug to console
if err := os.WriteFile("/proc/sys/kernel/printk", []byte("8"), 0); err != nil {
t.Fatal(err)
}
link1a, link1b := createVethPair(t, "link1")
link2a, link2b := createVethPair(t, "link2")
// Drop all traffic
l1aq := setupNetem(t, link1a, netlink.NetemQdiscAttrs{Loss: 100.0})
l1bq := setupNetem(t, link1b, netlink.NetemQdiscAttrs{Loss: 100.0})
l2aq := setupNetem(t, link2a, netlink.NetemQdiscAttrs{Loss: 100.0})
l2bq := setupNetem(t, link2b, netlink.NetemQdiscAttrs{Loss: 100.0})
bondA := createBond(t, "bonda", link1a, link2a)
bondB := createBond(t, "bondb", link1b, link2b)
time.Sleep(5 * time.Second)
// Bonds should not come up with links dropping all traffic
assertRunning(t, bondA, false)
assertRunning(t, bondB, false)
changeNetem(t, l1aq, netlink.NetemQdiscAttrs{Loss: 0.0})
changeNetem(t, l1bq, netlink.NetemQdiscAttrs{Loss: 0.0})
t.Log("Enabled L1")
time.Sleep(5 * time.Second)
// Bonds should come up with one link working
assertRunning(t, bondA, true)
assertRunning(t, bondB, true)
changeNetem(t, l2aq, netlink.NetemQdiscAttrs{Loss: 0.0})
changeNetem(t, l2bq, netlink.NetemQdiscAttrs{Loss: 0.0})
t.Log("Enabled L2")
time.Sleep(3 * time.Second)
// Bonds be up with both links
assertRunning(t, bondA, true)
assertRunning(t, bondB, true)
bondAState, err := os.ReadFile("/proc/net/bonding/bonda")
if err != nil {
panic(err)
}
t.Log(string(bondAState))
if !strings.Contains(string(bondAState), "Number of ports: 2") {
t.Errorf("bonda aggregator should contain two ports")
}
if !strings.Contains(string(bondAState), "port state: 63") {
t.Errorf("bonda port state should be 63")
}
t.Log("------------")
bondBState, err := os.ReadFile("/proc/net/bonding/bondb")
if err != nil {
panic(err)
}
t.Log(string(bondBState))
if !strings.Contains(string(bondBState), "Number of ports: 2") {
t.Errorf("bondb aggregator should contain two ports")
}
if !strings.Contains(string(bondBState), "port state: 63") {
t.Errorf("bondb port state should be 63")
}
changeNetem(t, l1aq, netlink.NetemQdiscAttrs{Loss: 100.0})
changeNetem(t, l1bq, netlink.NetemQdiscAttrs{Loss: 100.0})
changeNetem(t, l2aq, netlink.NetemQdiscAttrs{Loss: 100.0})
changeNetem(t, l2bq, netlink.NetemQdiscAttrs{Loss: 100.0})
t.Log("Disabled both links")
time.Sleep(5 * time.Second)
// Bonds should be back down
assertRunning(t, bondA, false)
assertRunning(t, bondB, false)
}