blob: 925ae0d8b250d3fe43b774941f8f910ab35da79d [file] [log] [blame]
// This test requires the following Linux kernel configuration options to be
// set:
//
// CONFIG_NET_CLS_ACT
// CONFIG_NET_CLS_MATCHALL
// CONFIG_NET_SCHED
// CONFIG_NET_SCH_INGRESS
// CONFIG_PSAMPLE
// CONFIG_NET_ACT_SAMPLE
package psample
import (
"errors"
"net"
"os"
"strings"
"syscall"
"testing"
"golang.org/x/sys/unix"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/vishvananda/netlink"
)
// setupLink adds and brings up a named link suited for packet capture.
func setupLink(t *testing.T, name string) netlink.Link {
t.Helper()
lnk := &netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{Name: name},
}
if err := netlink.LinkAdd(lnk); err != nil {
t.Fatalf("while adding link: %v", err)
}
if err := netlink.LinkSetUp(lnk); err != nil {
t.Fatalf("while setting up link: %v", err)
}
return lnk
}
// setupQdisc registers a clsact qdisc, which works on both the ingress and
// the egress. This is important as we'll be sampling packets leaving the test
// interface 'lk'. The qdisc is registered with the link lk.
// More on clsact: https://lwn.net/Articles/671458/
func setupQdisc(t *testing.T, lk netlink.Link) netlink.GenericQdisc {
t.Helper()
qdisc := netlink.GenericQdisc{
QdiscAttrs: netlink.QdiscAttrs{
LinkIndex: lk.Attrs().Index,
Handle: netlink.MakeHandle(0xffff, 0),
Parent: netlink.HANDLE_CLSACT,
},
QdiscType: "clsact",
}
if err := netlink.QdiscAdd(&qdisc); err != nil {
t.Fatalf("while adding qdisc: %v", err)
}
return qdisc
}
// setupSamplingFilter adds a filter on 'lk' link that will sample packets
// exiting the interface.
func setupSamplingFilter(t *testing.T, lk netlink.Link) netlink.Filter {
t.Helper()
sa := netlink.NewSampleAction()
// Sampled packets can be assigned their distinct group, allowing for
// multiple flows to be sampled and analyzed separately at the same time.
sa.Group = 7
// Every 10th packet will be sampled.
sa.Rate = 10
// Packet samples will be truncated to sa.TruncSize.
sa.TruncSize = 1500
fcid := netlink.MakeHandle(1, 1)
filter := &netlink.MatchAll{
FilterAttrs: netlink.FilterAttrs{
LinkIndex: lk.Attrs().Index,
Parent: netlink.HANDLE_MIN_EGRESS,
Priority: 1,
Protocol: unix.ETH_P_ALL,
},
ClassId: fcid,
Actions: []netlink.Action{
sa,
},
}
if err := netlink.FilterAdd(filter); err != nil {
t.Fatalf("while adding filter: %v", err)
}
return filter
}
// packetAttrs contains the test attributes looked for in a packet sample.
type packetAttrs struct {
// magic is the string expected to be found in the packet's application layer
// contents.
magic string
// oifIdx identifies the egress interface the packet is exiting.
oifIdx uint16
}
// match returns true if packet 'raw' matches attributes specified in 'pa'.
func (pa packetAttrs) match(t *testing.T, raw Packet) bool {
t.Helper()
// Check the packet's indicated egress interface.
if raw.OutgoingInterfaceIndex != pa.oifIdx {
return false
}
// Check the packet's payload.
if raw.Data == nil {
t.Fatalf("missing payload")
}
p := gopacket.NewPacket(raw.Data, layers.LayerTypeEthernet, gopacket.Default)
if app := p.ApplicationLayer(); app != nil {
if strings.Contains(string(app.Payload()), pa.magic) {
return true
}
}
return false
}
// TestSampling ascertains that packet samples can be obtained through use of
// this package's Subscribe(), and Receive().
func TestSampling(t *testing.T) {
if os.Getenv("IN_KTEST") != "true" {
t.Skip("Not in ktest")
}
// Make sure 'psample' module is loaded.
if _, err := netlink.GenlFamilyGet("psample"); err != nil {
t.Fatalf("psample genetlink family unavailable - is CONFIG_PSAMPLE enabled?")
}
// Set up the test link/interface, and supply it with a network address,
// which will enable routing for the test packets.
var localA = netlink.Addr{
IPNet: &net.IPNet{
IP: net.IPv4(10, 0, 0, 4),
Mask: net.CIDRMask(24, 32),
},
}
lk := setupLink(t, "if1")
if err := netlink.AddrAdd(lk, &localA); err != nil {
t.Fatalf("while adding network address: %v", err)
}
// Set up sampling.
setupQdisc(t, lk)
setupSamplingFilter(t, lk)
c, err := Subscribe()
if err != nil {
t.Fatalf("while subscribing to psample notifications: %v", err)
}
// Test case: we'll send UDP datagrams to a remote address within the network
// associated with link lk, expecting these packets to show up in the sampled
// egress traffic.
var dstA = netlink.Addr{
IPNet: &net.IPNet{
IP: net.IPv4(10, 0, 0, 5),
Mask: net.CIDRMask(24, 32),
},
}
// The sampled packets are expected to:
// - egress from interface 'if1'
// - contain a magic payload
sa := packetAttrs{
magic: "test datagram",
oifIdx: uint16(lk.Attrs().Index),
}
// Look for packets matching attributes defined in 'sa'. Signal on 'dC'
// immediately after the expected packet has been received, then return.
dC := make(chan struct{})
go func() {
for {
pkts, err := Receive(c)
// Receiving ENOBUFS is expected in this case. It signals that some of
// the sampled traffic could not have been captured, and had been
// dropped instead.
if err != nil && !errors.Is(err, syscall.ENOBUFS) {
t.Fatalf("while receiving psamples: %v", err)
}
for _, raw := range pkts {
if sa.match(t, raw) {
t.Logf("Got the expected packet sample.")
dC <- struct{}{}
return
}
}
}
}()
// Send out the test datagrams. Return as soon as the test succeeds.
dstE := net.JoinHostPort(dstA.IP.String(), "1234")
conn, err := net.Dial("udp", dstE)
if err != nil {
t.Fatalf("while dialing UDP address: %v", err)
}
defer conn.Close()
for {
select {
case <-dC:
return
default:
conn.Write([]byte(sa.magic))
}
}
}