| // 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)) |
| } |
| } |
| } |