|  | // Package psample provides a receiver for sampled network packets using the | 
|  | // Netlink psample interface. | 
|  | package psample | 
|  |  | 
|  | import ( | 
|  | "fmt" | 
|  |  | 
|  | "github.com/mdlayher/genetlink" | 
|  | "github.com/mdlayher/netlink" | 
|  | ) | 
|  |  | 
|  | // attrId identifies psample netlink message attributes. | 
|  | // Identifier numbers are based on psample kernel module sources: | 
|  | // https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/uapi/linux/psample.h?h=v5.15.89#n5 | 
|  | type attrId uint16 | 
|  |  | 
|  | const ( | 
|  | aIIfIndex      attrId = iota // u16 | 
|  | aOIfIndex                    // u16 | 
|  | aOrigSize                    // u32 | 
|  | aSampleGroup                 // u32 | 
|  | aGroupSeq                    // u32 | 
|  | aSampleRate                  // u32 | 
|  | aData                        // []byte | 
|  | aGroupRefcount               // u32 | 
|  | aTunnel | 
|  |  | 
|  | aPad | 
|  | aOutTC     // u16 | 
|  | aOutTCOCC  // u64 | 
|  | aLatency   // u64, nanoseconds | 
|  | aTimestamp // u64, nanoseconds | 
|  | aProto     // u16 | 
|  | ) | 
|  |  | 
|  | // Packet contains the sampled packet in its raw form, along with its | 
|  | // 'psample' metadata. | 
|  | type Packet struct { | 
|  | // IncomingInterfaceIndex is the incoming interface index of the packet or 0 | 
|  | // if not applicable. | 
|  | IncomingInterfaceIndex uint16 | 
|  | // OutgoingInterfaceIndex is the outgoing interface index of the packet or 0 | 
|  | // if not applicable. | 
|  | OutgoingInterfaceIndex uint16 | 
|  | // OriginalSize is the packet's original size in bytes without any | 
|  | // truncation. | 
|  | OriginalSize uint32 | 
|  | // SampleGroup is the sample group to which this packet belongs. This is set | 
|  | // by the sampling action and can be used to differentiate different | 
|  | // sampling streams. | 
|  | SampleGroup uint32 | 
|  | // GroupSequence is a monotonically-increasing counter of packets sampled | 
|  | // for each sample group. | 
|  | GroupSequence uint32 | 
|  | // SampleRate is the sampling rate (1 in SampleRate packets) used to capture | 
|  | // this packet. | 
|  | SampleRate uint32 | 
|  | // Data contains the packet data up to the specified size for truncation. | 
|  | Data []byte | 
|  |  | 
|  | // The following attributes are only available on kernel versions 5.13+ | 
|  |  | 
|  | // Latency is the sampled packet's latency as indicated by psample. It's | 
|  | // expressed in nanoseconds. | 
|  | Latency uint64 | 
|  | // Timestamp marks time of the packet's sampling. It's set by the kernel, and | 
|  | // expressed in Unix nanoseconds. | 
|  | Timestamp uint64 | 
|  | } | 
|  |  | 
|  | // decode converts raw generic netlink message attributes into a Packet. In | 
|  | // cases where some of the known psample attributes were left unspecified in | 
|  | // the message, appropriate Packet member variables will be left with their | 
|  | // zero values. | 
|  | func decode(b []byte) (*Packet, error) { | 
|  | ad, err := netlink.NewAttributeDecoder(b) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | var p Packet | 
|  | for ad.Next() { | 
|  | switch attrId(ad.Type()) { | 
|  | case aIIfIndex: | 
|  | p.IncomingInterfaceIndex = ad.Uint16() | 
|  | case aOIfIndex: | 
|  | p.OutgoingInterfaceIndex = ad.Uint16() | 
|  | case aOrigSize: | 
|  | p.OriginalSize = ad.Uint32() | 
|  | case aSampleGroup: | 
|  | p.SampleGroup = ad.Uint32() | 
|  | case aGroupSeq: | 
|  | p.GroupSequence = ad.Uint32() | 
|  | case aSampleRate: | 
|  | p.SampleRate = ad.Uint32() | 
|  | case aData: | 
|  | p.Data = ad.Bytes() | 
|  | case aLatency: | 
|  | p.Latency = ad.Uint64() | 
|  | case aTimestamp: | 
|  | p.Timestamp = ad.Uint64() | 
|  | } | 
|  | } | 
|  | return &p, nil | 
|  | } | 
|  |  | 
|  | // Subscribe returns a NetlinkSocket that's already subscribed to "packets" | 
|  | // psample multicast group, which makes it ready to receive packet samples. | 
|  | // Close should be called on the returned socket. | 
|  | func Subscribe() (*genetlink.Conn, error) { | 
|  | // Create a netlink socket. | 
|  | c, err := genetlink.Dial(nil) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("while dialing netlink socket: %w", err) | 
|  | } | 
|  |  | 
|  | // Lookup the netlink family id associated with psample kernel module. | 
|  | f, err := c.GetFamily("psample") | 
|  | if err != nil { | 
|  | c.Close() | 
|  | return nil, fmt.Errorf("couldn't lookup \"psample\" netlink family: %w", err) | 
|  | } | 
|  |  | 
|  | // Lookup psample's packet sampling netlink multicast group. | 
|  | var pktGrpId uint32 | 
|  | for _, mgrp := range f.Groups { | 
|  | if mgrp.Name == "packets" { | 
|  | pktGrpId = mgrp.ID | 
|  | break | 
|  | } | 
|  | } | 
|  | if pktGrpId == 0 { | 
|  | c.Close() | 
|  | return nil, fmt.Errorf("packets multicast group not found") | 
|  | } | 
|  |  | 
|  | // Subscribe to 'packets' multicast group in order to receive packet | 
|  | // samples. | 
|  | if err := c.JoinGroup(pktGrpId); err != nil { | 
|  | c.Close() | 
|  | return nil, fmt.Errorf("couldn't join multicast group: %w", err) | 
|  | } | 
|  | return c, nil | 
|  | } | 
|  |  | 
|  | // Receive returns one or more of the sampled packets as soon as they're | 
|  | // available. It may return a syscall.ENOBUFS error which indicates that the | 
|  | // kernel-side buffer of the netlink connection has overflowed and lost | 
|  | // packets. This is a transient error, calling Receive again will retrieve | 
|  | // future packet samples. | 
|  | func Receive(c *genetlink.Conn) ([]Packet, error) { | 
|  | // Wait for the samples to arrive over generic netlink connection c. | 
|  | gnms, nms, err := c.Receive() | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("while receiving netlink notifications: %w", err) | 
|  | } | 
|  |  | 
|  | var pkts []Packet | 
|  | for i := 0; i < len(nms); i++ { | 
|  | // Only process multicast notifications. | 
|  | if nms[i].Header.PID != 0 { | 
|  | continue | 
|  | } | 
|  |  | 
|  | // PSAMPLE_CMD_SAMPLE should be zero in multicast notifications. | 
|  | if gnms[i].Header.Command != 0 { | 
|  | continue | 
|  | } | 
|  |  | 
|  | // Iterate over the Generic Netlink attributes present in the message, | 
|  | // extracting any relating to the sampled packet. | 
|  | pkt, err := decode(gnms[i].Data) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("while decoding netlink notification: %w", err) | 
|  | } | 
|  | pkts = append(pkts, *pkt) | 
|  | } | 
|  | return pkts, nil | 
|  | } |