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