netstat: dump AF_PACKET sockets from the kernel

We'll try to dump the AF_PACKET sockets from the kernel. If it's not
possible, we'll fallback to read /proc/net/packet.
This commit is contained in:
Gustavo Iñiguez Goia 2025-02-06 01:49:40 +01:00
parent 335f2a783d
commit 1a39122c1d
Failed to generate hash of commit
3 changed files with 255 additions and 36 deletions

View file

@ -0,0 +1,172 @@
package netlink
import (
"encoding/binary"
"syscall"
"unsafe"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/vishvananda/netlink/nl"
"golang.org/x/sys/unix"
)
// request:
// {nlmsg_len=36, nlmsg_type=SOCK_DIAG_BY_FAMILY, nlmsg_flags=NLM_F_REQUEST|NLM_F_DUMP, nlmsg_seq=123456, nlmsg_pid=0},
// {sdiag_family=AF_PACKET, sdiag_protocol=0, pdiag_ino=0, pdiag_show=PACKET_SHOW_INFO, pdiag_cookie=[0, 0]}
// responses (depends on what filters are passed in the request):
// {pdiag_family=AF_PACKET, pdiag_type=SOCK_RAW, pdiag_num=ETH_P_ALL, pdiag_ino=257944535, pdiag_cookie=[1291434, 0]},
// {nla_len=28, nla_type=PACKET_DIAG_INFO},
// {pdi_index=if_nametoindex("wifi0"), pdi_version=TPACKET_V3, pdi_reserve=4, pdi_copy_thresh=0, pdi_tstamp=0, pdi_flags=PDI_RUNNING|PDI_AUXDATA}
// {nla_len=8, nla_type=PACKET_DIAG_UID}, 0}
// {nla_len=32, nla_type=PACKET_DIAG_RX_RING},
// {pdr_block_size=262144, pdr_block_nr=8, pdr_frame_size=262144, pdr_frame_nr=8, pdr_retire_tmo=10, pdr_sizeof_priv=0, pdr_features=0}
// {nla_len=40, nla_type=PACKET_DIAG_MEMINFO},
// [[SK_MEMINFO_RMEM_ALLOC]=0, [SK_MEMINFO_RCVBUF]=212992, [SK_MEMINFO_WMEM_ALLOC]=0, [SK_MEMINFO_SNDBUF]=212992, [SK_MEMINFO_FWD_ALLOC]=0, [SK_MEMINFO_WMEM_QUEUED]=0, [SK_MEMINFO_OPTMEM]=128, [SK_MEMINFO_BACKLOG]=0, [SK_MEMINFO_DROPS]=0]],
// {nla_len=12, nla_type=PACKET_DIAG_FILTER}, 0x512e76dfc140}
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/packet_diag.h#L16
// list of possible information to request
const (
PACKET_SHOW_INFO = 0x00000001 /* Basic packet_sk information */
PACKET_SHOW_MCLIST = 0x00000002 /* A set of packet_diag_mclist-s */
PACKET_SHOW_RING_CFG = 0x00000004 /* Rings configuration parameters */
PACKET_SHOW_FANOUT = 0x00000008
PACKET_SHOW_MEMINFO = 0x00000010
PACKET_SHOW_FILTER = 0x00000020
)
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/packet_diag.h#L32
// types of messages retrieved from kernel
const (
PACKET_DIAG_INFO = iota
PACKET_DIAG_MCLIST
PACKET_DIAG_RX_RING
PACKET_DIAG_TX_RING
PACKET_DIAG_FANOUT
PACKET_DIAG_UID
PACKET_DIAG_MEMINFO
PACKET_DIAG_FILTER
)
const (
sizePktDiagReq = 20
sizePktDiagMclist = 28
)
// PacketDiagMsg holds the message(s) sent by the kernel
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/packet_diag.h#L23
type PacketDiagMsg struct {
Mclist PacketDiagMclist
Cookie [2]uint32
Inode uint32
UID uint32
Num uint16 // ETH_P_ALL, etc
Family uint8
Type uint8
}
// PacketDiagMclist struct
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/packet_diag.h#L63
type PacketDiagMclist struct {
Index uint32
Count uint32
Type uint16
Alen uint16
Addr [32]uint8 /* MAX_ADDR_LEN */
}
func (pm *PacketDiagMsg) deserialize(b []byte) error {
rb := readBuffer{Bytes: b}
// 1st message: PacketDiagMsg
pm.Family = rb.Read()
pm.Type = rb.Read()
pm.Num = native.Uint16(rb.Next(2))
pm.Inode = native.Uint32(rb.Next(4))
pm.Cookie[0] = native.Uint32(rb.Next(4))
pm.Cookie[1] = native.Uint32(rb.Next(4))
nextMsg := rb.Read() // next msg size
if nextMsg == sizePktDiagMclist {
pm.Mclist = PacketDiagMclist{
// XXX: wrong values with native.Uint32()
Index: binary.BigEndian.Uint32(rb.Next(4)),
Count: binary.BigEndian.Uint32(rb.Next(4)),
Type: binary.BigEndian.Uint16(rb.Next(2)),
Alen: binary.BigEndian.Uint16(rb.Next(2)),
}
copy(pm.Mclist.Addr[:], rb.Next(16))
}
// {nla_len=8, nla_type=PACKET_DIAG_UID}, 1000}
nextMsg = rb.Read() // 8, size of next msg
nextMsg = rb.Read() // pad?
if nextMsg == PACKET_DIAG_UID {
rb.Read() // pad?
pm.UID = native.Uint32(rb.Next(4))
}
log.Trace("PktDiagMsg.deserialize (size: %d, sizeOf(PacketDiagMsg): %d): %+v", len(b), unsafe.Sizeof(b), pm)
return nil
}
// PacketDiagReq struct to request data from the kernel
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/packet_diag.h#L7
type PacketDiagReq struct {
Family uint8
Protocol uint8
Pad uint16
Inode uint32
Show uint32
Cookie [2]uint32
}
// Serialize ...
func (p *PacketDiagReq) Serialize() []byte {
b := writeBuffer{Bytes: make([]byte, sizePktDiagReq)}
b.Write(p.Family)
b.Write(p.Protocol)
native.PutUint16(b.Next(2), p.Pad)
native.PutUint32(b.Next(4), p.Inode)
native.PutUint32(b.Next(4), p.Show)
native.PutUint32(b.Next(4), p.Cookie[0])
native.PutUint32(b.Next(4), p.Cookie[1])
return b.Bytes
}
// Len ...
func (p *PacketDiagReq) Len() int { return sizePktDiagReq }
// SocketDiagPacket dumps AF_PACKET sockets from kernel
func SocketDiagPacket(proto uint8) ([]*PacketDiagMsg, error) {
req := nl.NewNetlinkRequest(nl.SOCK_DIAG_BY_FAMILY, syscall.NLM_F_DUMP)
req.AddData(&PacketDiagReq{
Family: unix.AF_PACKET,
Protocol: proto,
// TODO: dump bpf filters | PACKET_SHOW_FILTER
Show: PACKET_SHOW_INFO | PACKET_SHOW_MCLIST,
})
msgs, err := req.Execute(syscall.NETLINK_INET_DIAG, 0)
if err != nil {
log.Debug("[netlink] socket.packetRequest: %s", err)
return nil, err
}
if len(msgs) == 0 {
log.Debug("[netlink] socket.packetRequest: 0 msgs")
return []*PacketDiagMsg{}, nil
}
pkts := make([]*PacketDiagMsg, len(msgs))
for n, m := range msgs {
log.Trace("[netlink] AF_PACKET, size: %d, %+v", len(m), m)
p := &PacketDiagMsg{}
if err = p.deserialize(m); err != nil {
log.Trace("[%d] netlink socket.packet error: %s", n, err)
continue
}
pkts[n] = p
}
return pkts, nil
}

View file

@ -21,12 +21,13 @@ const (
) )
// Socket represents every socket dumped from the kernel for the given filter. // Socket represents every socket dumped from the kernel for the given filter.
// Internal to this package, and sent to the GUI as JSON.
type Socket struct { type Socket struct {
Socket *netlink.Socket Socket *netlink.Socket
Iface string Iface string
PID int PID int
Mark uint32 Mark uint32
Proto uint8 Proto uint16
} }
// SocketsTable holds all the dumped sockets, after applying the filters, if any. // SocketsTable holds all the dumped sockets, after applying the filters, if any.
@ -69,14 +70,25 @@ func (pm *SocketsMonitor) dumpSockets() *SocketsTable {
} }
wg.Add(1) wg.Add(1)
// XXX: firing a goroutine per socket may be too much on some scenarios // XXX: firing a goroutine per socket may be too much on some scenarios
go addSocketToTable(pm.Ctx, &wg, opt.Proto, socketList, *sock) go addSocketToTable(pm.Ctx, &wg, uint16(opt.Proto), socketList, *sock)
} }
wg.Wait() wg.Wait()
} }
if !exclude(pm.Config.Family, unix.AF_XDP) && !exclude(pm.Config.Proto, syscall.IPPROTO_RAW) { dumpXDPSockets(pm.Ctx, pm.Config, socketList)
dumpPacketSockets(pm.Ctx, pm.Config, socketList)
return socketList
}
func dumpXDPSockets(ctx context.Context, conf *monConfig, socketList *SocketsTable) {
if exclude(conf.Family, unix.AF_XDP) && exclude(conf.Proto, syscall.IPPROTO_RAW) {
return
}
xdpList, err := netlink.SocketGetXDP() xdpList, err := netlink.SocketGetXDP()
if err == nil { if err != nil {
return
}
var wg sync.WaitGroup var wg sync.WaitGroup
for _, xdp := range xdpList { for _, xdp := range xdpList {
s := netlink.Socket{} s := netlink.Socket{}
@ -88,29 +100,56 @@ func (pm *SocketsMonitor) dumpSockets() *SocketsTable {
Cookie: xdp.XDPDiagMsg.Cookie, Cookie: xdp.XDPDiagMsg.Cookie,
} }
wg.Add(1) wg.Add(1)
go addSocketToTable(pm.Ctx, &wg, syscall.IPPROTO_RAW, socketList, s) go addSocketToTable(ctx, &wg, syscall.IPPROTO_RAW, socketList, s)
} }
wg.Wait() wg.Wait()
} }
}
if exclude(pm.Config.Family, unix.AF_PACKET) { func dumpPacketSockets(ctx context.Context, conf *monConfig, socketList *SocketsTable) {
return socketList if exclude(conf.Family, unix.AF_PACKET) {
} return
entries, err := netstat.ParsePacket()
if err != nil {
return socketList
} }
var wg sync.WaitGroup var wg sync.WaitGroup
pktList := make(map[int]struct{}, len(entries)) pktList, err := netlink.SocketDiagPacket(0)
for _, pkt := range pktList {
/*if excludePacket(pm.Config.Proto, pkt.Num) {
continue
}*/
s := netlink.Socket{}
s.Family = unix.AF_PACKET
s.INode = uint32(pkt.Inode)
s.UID = uint32(pkt.UID)
s.ID = netlink.SocketID{
Interface: uint32(pkt.Mclist.Index),
Cookie: pkt.Cookie,
}
wg.Add(1)
go addSocketToTable(ctx, &wg, pkt.Num /* proto */, socketList, s)
}
wg.Wait()
if err == nil {
return
}
// dumping AF_PACKET from kernel failed. Try it with /proc
entries, err := netstat.ParsePacket()
if err != nil {
return
}
pktEntr := make(map[int]struct{}, len(entries))
for n, e := range entries { for n, e := range entries {
if _, isDup := pktList[n]; isDup { if _, isDup := pktEntr[n]; isDup {
continue continue
} }
pktList[n] = struct{}{} pktEntr[n] = struct{}{}
wg.Add(1) /*if excludePacket(conf.Proto, opt.Proto) {
continue
}*/
s := netlink.Socket{} s := netlink.Socket{}
s.Family = unix.AF_PACKET s.Family = unix.AF_PACKET
s.INode = uint32(e.INode) s.INode = uint32(e.INode)
@ -118,19 +157,18 @@ func (pm *SocketsMonitor) dumpSockets() *SocketsTable {
s.ID = netlink.SocketID{ s.ID = netlink.SocketID{
Interface: uint32(e.Iface), Interface: uint32(e.Iface),
} }
// TODO: report the protocol and type // TODO: report sock type
go addSocketToTable(pm.Ctx, &wg, syscall.IPPROTO_RAW, socketList, s) wg.Add(1)
go addSocketToTable(ctx, &wg, syscall.IPPROTO_RAW, socketList, s)
} }
wg.Wait() wg.Wait()
return socketList
} }
func exclude(expected, what uint8) bool { func exclude(expected, what uint8) bool {
return expected > AnySocket && expected != what return expected > AnySocket && expected != what
} }
func addSocketToTable(ctx context.Context, wg *sync.WaitGroup, proto uint8, st *SocketsTable, s netlink.Socket) { func addSocketToTable(ctx context.Context, wg *sync.WaitGroup, proto uint16, st *SocketsTable, s netlink.Socket) {
inode := int(s.INode) inode := int(s.INode)
pid := procmon.GetPIDFromINode(inode, fmt.Sprint(inode, pid := procmon.GetPIDFromINode(inode, fmt.Sprint(inode,
s.ID.Source, s.ID.SourcePort, s.ID.Destination, s.ID.DestinationPort), s.ID.Source, s.ID.SourcePort, s.ID.Destination, s.ID.DestinationPort),

View file

@ -15,7 +15,6 @@ Proto = {
'0': 'IP', '0': 'IP',
'1': 'ICMP', '1': 'ICMP',
'2': 'IGMP', '2': 'IGMP',
'3': 'ETH_P_ALL',
'6': 'TCP', '6': 'TCP',
'17': 'UDP', '17': 'UDP',
'33': 'DCCP', '33': 'DCCP',
@ -23,7 +22,17 @@ Proto = {
'58': 'ICMPv6', '58': 'ICMPv6',
'132': 'SCTP', '132': 'SCTP',
'136': 'UDPLITE', '136': 'UDPLITE',
'255': 'RAW' '255': 'RAW',
'3': 'ETH_P_ALL',
'2048': 'ETH_P_IP',
'34525': 'ETH_P_IPV6',
'2054': 'ETH_P_ARP',
'32821': 'ETH_P_RARP',
'33024': 'ETH_P_8021Q',
'4': 'ETH_P_802_2',
'34916': 'ETH_P_PPPOE',
'34958': 'ETH_P_PAE',
'35085': 'ETH_P_FCOE'
} }
State = { State = {