// Package nflog reads the packets the ruleset logs (log ... group N), // through nfnetlink_log: housecarld binds the group and decodes each // packet's prefix, interfaces and headers for the drop log. package nflog import ( "context" "encoding/binary" "fmt" "log/slog" "net" "net/netip" "regexp" "strings" "sync" "time" "golang.org/x/sys/unix" "housecarl/internal/api" ) const ( subsysULOG = 4 msgPacket = 0 msgConfig = 1 cfgCmd = 1 cfgMode = 2 cmdBind = 1 copyPacket = 2 attrPacketHdr = 1 attrTimestamp = 3 attrInDev = 4 attrOutDev = 5 attrPayload = 9 attrPrefix = 10 nfnetlinkV0 = 0 headerLen = unix.SizeofNlMsghdr nfgenmsgLen = 4 copyRangeBytes = 128 // headers are all we read ) // Listen binds the log group and passes each logged packet to handle until // ctx ends. Errors (no CAP_NET_ADMIN, say) are logged once: the log stays // empty, nothing else is affected. func Listen(ctx context.Context, group uint16, log *slog.Logger, handle func(api.LogEntry)) { fd, err := open(group) if err != nil { log.Warn("can't read logged packets, so the log stays empty", "group", group, "err", err) return } go func() { <-ctx.Done() unix.Close(fd) }() names := &ifnames{m: map[uint32]string{}} buf := make([]byte, 1<<16) for { n, _, err := unix.Recvfrom(fd, buf, 0) if err != nil { if ctx.Err() != nil { return } if err == unix.ENOBUFS { continue // packets were lost; go on } log.Warn("reading logged packets failed", "err", err) return } for _, e := range Parse(buf[:n], names.name) { handle(e) } } } func open(group uint16) (int, error) { fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_NETFILTER) if err != nil { return -1, err } if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil { unix.Close(fd) return -1, err } // Bind the group, then ask for packets (their headers). cmd := attr(cfgCmd, []byte{cmdBind}) mode := make([]byte, 6) binary.BigEndian.PutUint32(mode, copyRangeBytes) mode[4] = copyPacket for _, a := range [][]byte{cmd, attr(cfgMode, mode)} { if err := request(fd, group, a); err != nil { unix.Close(fd) return -1, fmt.Errorf("binding nflog group %d: %w", group, err) } } return fd, nil } func attr(typ uint16, data []byte) []byte { l := 4 + len(data) b := make([]byte, (l+3)&^3) binary.LittleEndian.PutUint16(b, uint16(l)) binary.LittleEndian.PutUint16(b[2:], typ) copy(b[4:], data) return b } // request sends a config message and waits for its acknowledgement. func request(fd int, group uint16, a []byte) error { msg := make([]byte, headerLen+nfgenmsgLen+len(a)) binary.LittleEndian.PutUint32(msg, uint32(len(msg))) binary.LittleEndian.PutUint16(msg[4:], subsysULOG<<8|msgConfig) binary.LittleEndian.PutUint16(msg[6:], unix.NLM_F_REQUEST|unix.NLM_F_ACK) msg[headerLen] = unix.AF_UNSPEC msg[headerLen+1] = nfnetlinkV0 binary.BigEndian.PutUint16(msg[headerLen+2:], group) copy(msg[headerLen+nfgenmsgLen:], a) if err := unix.Sendto(fd, msg, 0, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil { return err } buf := make([]byte, 4096) n, _, err := unix.Recvfrom(fd, buf, 0) if err != nil { return err } if n >= headerLen+4 && binary.LittleEndian.Uint16(buf[4:]) == unix.NLMSG_ERROR { if code := int32(binary.LittleEndian.Uint32(buf[headerLen:])); code != 0 { return unix.Errno(-code) } } return nil } type ifnames struct { mu sync.Mutex m map[uint32]string } func (c *ifnames) name(i uint32) string { c.mu.Lock() defer c.mu.Unlock() if n, ok := c.m[i]; ok { return n } n := fmt.Sprint(i) if ifi, err := net.InterfaceByIndex(int(i)); err == nil { n = ifi.Name } c.m[i] = n return n } // Parse decodes the logged packets in a netlink datagram; ifname turns // interface indexes into names. func Parse(b []byte, ifname func(uint32) string) []api.LogEntry { var out []api.LogEntry for len(b) >= headerLen { l := int(binary.LittleEndian.Uint32(b)) typ := binary.LittleEndian.Uint16(b[4:]) if l < headerLen || l > len(b) { break } if typ == subsysULOG<<8|msgPacket && l >= headerLen+nfgenmsgLen { if e, ok := packet(b[headerLen:l], ifname); ok { out = append(out, e) } } step := (l + 3) &^ 3 if step >= len(b) { break } b = b[step:] } return out } func packet(b []byte, ifname func(uint32) string) (api.LogEntry, bool) { e := api.LogEntry{Time: time.Now()} switch b[0] { case unix.AF_INET: e.Family = "ipv4" case unix.AF_INET6: e.Family = "ipv6" } attrs := b[nfgenmsgLen:] var payload []byte for len(attrs) >= 4 { l := int(binary.LittleEndian.Uint16(attrs)) typ := binary.LittleEndian.Uint16(attrs[2:]) & 0x3fff if l < 4 || l > len(attrs) { break } v := attrs[4:l] switch typ { case attrPrefix: e.Prefix = strings.TrimRight(string(v), "\x00") case attrInDev: if len(v) >= 4 { e.In = ifname(binary.BigEndian.Uint32(v)) } case attrOutDev: if len(v) >= 4 { e.Out = ifname(binary.BigEndian.Uint32(v)) } case attrTimestamp: if len(v) >= 16 { sec, usec := binary.BigEndian.Uint64(v), binary.BigEndian.Uint64(v[8:]) e.Time = time.Unix(int64(sec), int64(usec)*1000) } case attrPacketHdr: if len(v) >= 2 && e.Family == "" { switch binary.BigEndian.Uint16(v) { case 0x0800: e.Family = "ipv4" case 0x86dd: e.Family = "ipv6" } } case attrPayload: payload = v } attrs = attrs[min((l+3)&^3, len(attrs)):] } decodePayload(&e, payload) e.RuleMark, e.Verdict = markOf(e.Prefix) return e, e.Prefix != "" || payload != nil } var icmpNames = map[byte]string{0: "echo-reply", 3: "destination-unreachable", 5: "redirect", 8: "echo-request", 11: "time-exceeded", 12: "parameter-problem"} var icmp6Names = map[byte]string{1: "destination-unreachable", 2: "packet-too-big", 3: "time-exceeded", 4: "parameter-problem", 128: "echo-request", 129: "echo-reply", 133: "nd-router-solicit", 134: "nd-router-advert", 135: "nd-neighbor-solicit", 136: "nd-neighbor-advert"} func decodePayload(e *api.LogEntry, p []byte) { var proto byte var l4 []byte switch { case len(p) >= 20 && p[0]>>4 == 4: e.Family = "ipv4" ihl := int(p[0]&0x0f) * 4 e.Length = int(binary.BigEndian.Uint16(p[2:])) proto = p[9] e.Source = netip.AddrFrom4([4]byte(p[12:16])).String() e.Dest = netip.AddrFrom4([4]byte(p[16:20])).String() if ihl <= len(p) { l4 = p[ihl:] } case len(p) >= 40 && p[0]>>4 == 6: e.Family = "ipv6" e.Length = int(binary.BigEndian.Uint16(p[4:])) + 40 proto = p[6] e.Source = netip.AddrFrom16([16]byte(p[8:24])).String() e.Dest = netip.AddrFrom16([16]byte(p[24:40])).String() l4 = p[40:] default: return } switch proto { case 6: e.Protocol = "tcp" if len(l4) >= 14 { e.SrcPort, e.DestPort = int(binary.BigEndian.Uint16(l4)), int(binary.BigEndian.Uint16(l4[2:])) e.TCPFlags = tcpFlags(l4[13]) } case 17: e.Protocol = "udp" if len(l4) >= 4 { e.SrcPort, e.DestPort = int(binary.BigEndian.Uint16(l4)), int(binary.BigEndian.Uint16(l4[2:])) } case 132: e.Protocol = "sctp" if len(l4) >= 4 { e.SrcPort, e.DestPort = int(binary.BigEndian.Uint16(l4)), int(binary.BigEndian.Uint16(l4[2:])) } case 1: e.Protocol = "icmp" if len(l4) >= 1 { e.ICMPType = icmpNames[l4[0]] if e.ICMPType == "" { e.ICMPType = fmt.Sprint(l4[0]) } } case 58: e.Protocol = "icmpv6" if len(l4) >= 1 { e.ICMPType = icmp6Names[l4[0]] if e.ICMPType == "" { e.ICMPType = fmt.Sprint(l4[0]) } } default: e.Protocol = fmt.Sprint(proto) } } func tcpFlags(f byte) string { var out []string for i, n := range []string{"fin", "syn", "rst", "psh", "ack", "urg"} { if f&(1<