| Lorenz Brun | 8bc8286 | 2024-04-30 11:47:09 +0000 | [diff] [blame] | 1 | // Package lacp contains an integration test for our custom LACP patches. |
| 2 | // It tests relevant behavior that other parts of the Monogon network stack |
| 3 | // rely on, like proper carrier state indications. |
| 4 | package lacp |
| 5 | |
| 6 | import ( |
| 7 | "hash/fnv" |
| 8 | "math" |
| 9 | "net" |
| 10 | "os" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/vishvananda/netlink" |
| 16 | "golang.org/x/sys/unix" |
| 17 | ) |
| 18 | |
| 19 | // createVethPair is a helper creating a pair of virtual network interfaces |
| 20 | // acting as a cable (i.e. traffic going in one interface comes back out on |
| 21 | // the other one). Both ends are returned. |
| 22 | func createVethPair(t *testing.T, name string) (*netlink.Veth, *netlink.Veth) { |
| 23 | t.Helper() |
| 24 | vethLink := netlink.Veth{ |
| 25 | LinkAttrs: netlink.LinkAttrs{ |
| 26 | Name: name + "a", |
| 27 | NetNsID: -1, |
| 28 | TxQLen: -1, |
| 29 | }, |
| 30 | PeerName: name + "b", |
| 31 | } |
| 32 | if err := netlink.LinkAdd(&vethLink); err != nil { |
| 33 | t.Fatalf("while creating veth pair: %v", err) |
| 34 | } |
| 35 | vethLinkB, err := netlink.LinkByName(name + "b") |
| 36 | if err != nil { |
| 37 | t.Fatalf("while creating veth pair: while getting veth peer: %v", err) |
| 38 | } |
| 39 | |
| 40 | return &vethLink, vethLinkB.(*netlink.Veth) |
| 41 | } |
| 42 | |
| 43 | // setupNetem is a helper for setting up Linux's network emulation queuing |
| 44 | // discipline on a network interface, which can simulate various network |
| 45 | // imperfections like extra latency, reordering or packet loss on packets |
| 46 | // transmitted on the interface specified. As it is internally implemented |
| 47 | // as a queuing discipline it only affects transmitted packets. |
| 48 | func setupNetem(t *testing.T, link netlink.Link, conf netlink.NetemQdiscAttrs) *netlink.Netem { |
| 49 | t.Helper() |
| 50 | h := fnv.New32a() |
| 51 | h.Write([]byte(link.Attrs().Name)) |
| 52 | qdisc := netlink.NewNetem(netlink.QdiscAttrs{ |
| 53 | LinkIndex: link.Attrs().Index, |
| 54 | Handle: netlink.MakeHandle(uint16(h.Sum32()%math.MaxUint16), 0), |
| 55 | Parent: netlink.HANDLE_ROOT, |
| 56 | }, conf) |
| 57 | if err := netlink.QdiscAdd(qdisc); err != nil { |
| 58 | t.Fatalf("while setting up qdisc netem for %q: %v", link.Attrs().Name, err) |
| 59 | } |
| 60 | return qdisc |
| 61 | } |
| 62 | |
| 63 | // changeNetem is a helper for reconfiguring an existing netem instance |
| 64 | // on-the-fly with new parameters. |
| 65 | func changeNetem(t *testing.T, qdisc *netlink.Netem, conf netlink.NetemQdiscAttrs) { |
| 66 | t.Helper() |
| 67 | changedQd := netlink.NewNetem(qdisc.QdiscAttrs, conf) |
| 68 | if err := netlink.QdiscChange(changedQd); err != nil { |
| 69 | t.Fatalf("while changing qdisc netem for link index %v: %v", qdisc.LinkIndex, err) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | func createBond(t *testing.T, name string, links ...netlink.Link) *netlink.Bond { |
| 74 | t.Helper() |
| 75 | bondLink := netlink.NewLinkBond(netlink.LinkAttrs{ |
| 76 | Name: name, |
| 77 | NetNsID: -1, |
| 78 | TxQLen: -1, |
| 79 | Flags: net.FlagUp, |
| 80 | }) |
| 81 | bondLink.Mode = netlink.BOND_MODE_802_3AD |
| 82 | bondLink.LacpRate = netlink.BOND_LACP_RATE_FAST |
| 83 | bondLink.MinLinks = 1 |
| 84 | bondLink.AdSelect = netlink.BOND_AD_SELECT_BANDWIDTH |
| 85 | if err := netlink.LinkAdd(bondLink); err != nil { |
| 86 | t.Fatalf("while creating bond: %v", err) |
| 87 | } |
| 88 | for _, l := range links { |
| 89 | if err := netlink.LinkSetBondSlave(l, bondLink); err != nil { |
| 90 | t.Fatalf("while enslaving link to bond %q: %v", name, err) |
| 91 | } |
| 92 | } |
| 93 | return bondLink |
| 94 | } |
| 95 | |
| 96 | // assertRunning is a helper for asserting an interface's IFF_RUNNING state. |
| 97 | func assertRunning(t *testing.T, l netlink.Link, expected bool) { |
| 98 | t.Helper() |
| 99 | linkCurrent, err := netlink.LinkByIndex(l.Attrs().Index) |
| 100 | if err != nil { |
| 101 | t.Fatalf("while checking if link %q is running: %v", l.Attrs().Name, err) |
| 102 | } |
| 103 | is := linkCurrent.Attrs().RawFlags&unix.IFF_RUNNING != 0 |
| 104 | if expected != is { |
| 105 | t.Errorf("expected interface %q running state to be %v, is %v", l.Attrs().Name, expected, is) |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | func TestLACP(t *testing.T) { |
| 110 | if os.Getenv("IN_KTEST") != "true" { |
| 111 | t.Skip("Not in ktest") |
| 112 | } |
| 113 | |
| 114 | if err := os.WriteFile("/sys/kernel/debug/dynamic_debug/control", []byte("module bonding +p"), 0); err != nil { |
| 115 | t.Fatal(err) |
| 116 | } |
| 117 | // Log dynamic debug to console |
| 118 | if err := os.WriteFile("/proc/sys/kernel/printk", []byte("8"), 0); err != nil { |
| 119 | t.Fatal(err) |
| 120 | } |
| 121 | |
| 122 | link1a, link1b := createVethPair(t, "link1") |
| 123 | link2a, link2b := createVethPair(t, "link2") |
| 124 | |
| 125 | // Drop all traffic |
| 126 | l1aq := setupNetem(t, link1a, netlink.NetemQdiscAttrs{Loss: 100.0}) |
| 127 | l1bq := setupNetem(t, link1b, netlink.NetemQdiscAttrs{Loss: 100.0}) |
| 128 | l2aq := setupNetem(t, link2a, netlink.NetemQdiscAttrs{Loss: 100.0}) |
| 129 | l2bq := setupNetem(t, link2b, netlink.NetemQdiscAttrs{Loss: 100.0}) |
| 130 | |
| 131 | bondA := createBond(t, "bonda", link1a, link2a) |
| 132 | bondB := createBond(t, "bondb", link1b, link2b) |
| 133 | |
| 134 | time.Sleep(5 * time.Second) |
| 135 | |
| 136 | // Bonds should not come up with links dropping all traffic |
| 137 | assertRunning(t, bondA, false) |
| 138 | assertRunning(t, bondB, false) |
| 139 | |
| 140 | changeNetem(t, l1aq, netlink.NetemQdiscAttrs{Loss: 0.0}) |
| 141 | changeNetem(t, l1bq, netlink.NetemQdiscAttrs{Loss: 0.0}) |
| 142 | t.Log("Enabled L1") |
| 143 | |
| 144 | time.Sleep(5 * time.Second) |
| 145 | |
| 146 | // Bonds should come up with one link working |
| 147 | assertRunning(t, bondA, true) |
| 148 | assertRunning(t, bondB, true) |
| 149 | |
| 150 | changeNetem(t, l2aq, netlink.NetemQdiscAttrs{Loss: 0.0}) |
| 151 | changeNetem(t, l2bq, netlink.NetemQdiscAttrs{Loss: 0.0}) |
| 152 | t.Log("Enabled L2") |
| 153 | |
| 154 | time.Sleep(3 * time.Second) |
| 155 | |
| 156 | // Bonds be up with both links |
| 157 | assertRunning(t, bondA, true) |
| 158 | assertRunning(t, bondB, true) |
| 159 | |
| 160 | bondAState, err := os.ReadFile("/proc/net/bonding/bonda") |
| 161 | if err != nil { |
| 162 | panic(err) |
| 163 | } |
| 164 | t.Log(string(bondAState)) |
| 165 | if !strings.Contains(string(bondAState), "Number of ports: 2") { |
| 166 | t.Errorf("bonda aggregator should contain two ports") |
| 167 | } |
| 168 | if !strings.Contains(string(bondAState), "port state: 63") { |
| 169 | t.Errorf("bonda port state should be 63") |
| 170 | } |
| 171 | t.Log("------------") |
| 172 | bondBState, err := os.ReadFile("/proc/net/bonding/bondb") |
| 173 | if err != nil { |
| 174 | panic(err) |
| 175 | } |
| 176 | t.Log(string(bondBState)) |
| 177 | if !strings.Contains(string(bondBState), "Number of ports: 2") { |
| 178 | t.Errorf("bondb aggregator should contain two ports") |
| 179 | } |
| 180 | if !strings.Contains(string(bondBState), "port state: 63") { |
| 181 | t.Errorf("bondb port state should be 63") |
| 182 | } |
| 183 | changeNetem(t, l1aq, netlink.NetemQdiscAttrs{Loss: 100.0}) |
| 184 | changeNetem(t, l1bq, netlink.NetemQdiscAttrs{Loss: 100.0}) |
| 185 | changeNetem(t, l2aq, netlink.NetemQdiscAttrs{Loss: 100.0}) |
| 186 | changeNetem(t, l2bq, netlink.NetemQdiscAttrs{Loss: 100.0}) |
| 187 | t.Log("Disabled both links") |
| 188 | |
| 189 | time.Sleep(5 * time.Second) |
| 190 | |
| 191 | // Bonds should be back down |
| 192 | assertRunning(t, bondA, false) |
| 193 | assertRunning(t, bondB, false) |
| 194 | } |