blob: f28302c0dd474f629faf4bbf916ebd4be84886a7 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brun8bc82862024-04-30 11:47:09 +00004// 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.
7package lacp
8
9import (
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.
25func 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.
51func 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.
68func 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
76func 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.
100func 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
112func 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}