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