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