blob: 925ae0d8b250d3fe43b774941f8f910ab35da79d [file] [log] [blame]
Mateusz Zalega3ccf6962023-01-23 17:01:40 +00001// This test requires the following Linux kernel configuration options to be
2// set:
3//
4// CONFIG_NET_CLS_ACT
5// CONFIG_NET_CLS_MATCHALL
6// CONFIG_NET_SCHED
7// CONFIG_NET_SCH_INGRESS
8// CONFIG_PSAMPLE
9// CONFIG_NET_ACT_SAMPLE
10package psample
11
12import (
13 "errors"
14 "net"
15 "os"
16 "strings"
17 "syscall"
18 "testing"
19
20 "golang.org/x/sys/unix"
21
22 "github.com/google/gopacket"
23 "github.com/google/gopacket/layers"
24 "github.com/vishvananda/netlink"
25)
26
27// setupLink adds and brings up a named link suited for packet capture.
28func setupLink(t *testing.T, name string) netlink.Link {
29 t.Helper()
30
31 lnk := &netlink.Dummy{
32 LinkAttrs: netlink.LinkAttrs{Name: name},
33 }
34 if err := netlink.LinkAdd(lnk); err != nil {
35 t.Fatalf("while adding link: %v", err)
36 }
37 if err := netlink.LinkSetUp(lnk); err != nil {
38 t.Fatalf("while setting up link: %v", err)
39 }
40 return lnk
41}
42
43// setupQdisc registers a clsact qdisc, which works on both the ingress and
44// the egress. This is important as we'll be sampling packets leaving the test
45// interface 'lk'. The qdisc is registered with the link lk.
46// More on clsact: https://lwn.net/Articles/671458/
47func setupQdisc(t *testing.T, lk netlink.Link) netlink.GenericQdisc {
48 t.Helper()
49
50 qdisc := netlink.GenericQdisc{
51 QdiscAttrs: netlink.QdiscAttrs{
52 LinkIndex: lk.Attrs().Index,
53 Handle: netlink.MakeHandle(0xffff, 0),
54 Parent: netlink.HANDLE_CLSACT,
55 },
56 QdiscType: "clsact",
57 }
58 if err := netlink.QdiscAdd(&qdisc); err != nil {
59 t.Fatalf("while adding qdisc: %v", err)
60 }
61 return qdisc
62}
63
64// setupSamplingFilter adds a filter on 'lk' link that will sample packets
65// exiting the interface.
66func setupSamplingFilter(t *testing.T, lk netlink.Link) netlink.Filter {
67 t.Helper()
68
69 sa := netlink.NewSampleAction()
70 // Sampled packets can be assigned their distinct group, allowing for
71 // multiple flows to be sampled and analyzed separately at the same time.
72 sa.Group = 7
73 // Every 10th packet will be sampled.
74 sa.Rate = 10
75 // Packet samples will be truncated to sa.TruncSize.
76 sa.TruncSize = 1500
77
78 fcid := netlink.MakeHandle(1, 1)
79 filter := &netlink.MatchAll{
80 FilterAttrs: netlink.FilterAttrs{
81 LinkIndex: lk.Attrs().Index,
82 Parent: netlink.HANDLE_MIN_EGRESS,
83 Priority: 1,
84 Protocol: unix.ETH_P_ALL,
85 },
86 ClassId: fcid,
87 Actions: []netlink.Action{
88 sa,
89 },
90 }
91 if err := netlink.FilterAdd(filter); err != nil {
92 t.Fatalf("while adding filter: %v", err)
93 }
94 return filter
95}
96
97// packetAttrs contains the test attributes looked for in a packet sample.
98type packetAttrs struct {
99 // magic is the string expected to be found in the packet's application layer
100 // contents.
101 magic string
102 // oifIdx identifies the egress interface the packet is exiting.
103 oifIdx uint16
104}
105
106// match returns true if packet 'raw' matches attributes specified in 'pa'.
107func (pa packetAttrs) match(t *testing.T, raw Packet) bool {
108 t.Helper()
109
110 // Check the packet's indicated egress interface.
111 if raw.OutgoingInterfaceIndex != pa.oifIdx {
112 return false
113 }
114
115 // Check the packet's payload.
116 if raw.Data == nil {
117 t.Fatalf("missing payload")
118 }
119 p := gopacket.NewPacket(raw.Data, layers.LayerTypeEthernet, gopacket.Default)
120 if app := p.ApplicationLayer(); app != nil {
121 if strings.Contains(string(app.Payload()), pa.magic) {
122 return true
123 }
124 }
125 return false
126}
127
128// TestSampling ascertains that packet samples can be obtained through use of
129// this package's Subscribe(), and Receive().
130func TestSampling(t *testing.T) {
131 if os.Getenv("IN_KTEST") != "true" {
132 t.Skip("Not in ktest")
133 }
134
135 // Make sure 'psample' module is loaded.
136 if _, err := netlink.GenlFamilyGet("psample"); err != nil {
137 t.Fatalf("psample genetlink family unavailable - is CONFIG_PSAMPLE enabled?")
138 }
139
140 // Set up the test link/interface, and supply it with a network address,
141 // which will enable routing for the test packets.
142 var localA = netlink.Addr{
143 IPNet: &net.IPNet{
144 IP: net.IPv4(10, 0, 0, 4),
145 Mask: net.CIDRMask(24, 32),
146 },
147 }
148 lk := setupLink(t, "if1")
149 if err := netlink.AddrAdd(lk, &localA); err != nil {
150 t.Fatalf("while adding network address: %v", err)
151 }
152
153 // Set up sampling.
154
155 setupQdisc(t, lk)
156 setupSamplingFilter(t, lk)
157
158 c, err := Subscribe()
159 if err != nil {
160 t.Fatalf("while subscribing to psample notifications: %v", err)
161 }
162
163 // Test case: we'll send UDP datagrams to a remote address within the network
164 // associated with link lk, expecting these packets to show up in the sampled
165 // egress traffic.
166 var dstA = netlink.Addr{
167 IPNet: &net.IPNet{
168 IP: net.IPv4(10, 0, 0, 5),
169 Mask: net.CIDRMask(24, 32),
170 },
171 }
172
173 // The sampled packets are expected to:
174 // - egress from interface 'if1'
175 // - contain a magic payload
176 sa := packetAttrs{
177 magic: "test datagram",
178 oifIdx: uint16(lk.Attrs().Index),
179 }
180
181 // Look for packets matching attributes defined in 'sa'. Signal on 'dC'
182 // immediately after the expected packet has been received, then return.
183 dC := make(chan struct{})
184 go func() {
185 for {
186 pkts, err := Receive(c)
187 // Receiving ENOBUFS is expected in this case. It signals that some of
188 // the sampled traffic could not have been captured, and had been
189 // dropped instead.
190 if err != nil && !errors.Is(err, syscall.ENOBUFS) {
191 t.Fatalf("while receiving psamples: %v", err)
192 }
193 for _, raw := range pkts {
194 if sa.match(t, raw) {
195 t.Logf("Got the expected packet sample.")
196 dC <- struct{}{}
197 return
198 }
199 }
200 }
201 }()
202
203 // Send out the test datagrams. Return as soon as the test succeeds.
204
205 dstE := net.JoinHostPort(dstA.IP.String(), "1234")
206 conn, err := net.Dial("udp", dstE)
207 if err != nil {
208 t.Fatalf("while dialing UDP address: %v", err)
209 }
210 defer conn.Close()
211
212 for {
213 select {
214 case <-dC:
215 return
216 default:
217 conn.Write([]byte(sa.magic))
218 }
219 }
220}