diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1a2ada29..2b31ac1e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,14 +19,15 @@ You can also get everything for `glibc` [here](UPLOAD FILE WITH ALL BINARIES TO 2. Extract them running: ```sh $ for i in `ls *.zip`; do unzip $i; rm .gitkeep ; rm $i; done - $ for i in `ls *.xz`; do tar -xf $i; rm $i* ; done + $ for i in `ls *.tar`; do tar -xf $i; rm $i* ; done ``` 3. Compile branch an run the following tests: ```sh # make clean; make tester - # for i in `seq 0 3`; do ./kernel/legacy_test --netdata-path ../artifacts --content --iteration 1 --pid $i --log-path file_pid$i.txt; done + # for i in `seq 0 3`; do ./tests/legacy_test --netdata-path ../artifacts --content --iteration 1 --pid $i --log-path file_c_pid$i.txt; done + # for i in `seq 0 3`; do ./gotests/go_tester --netdata-path ../artifacts --content --iteration 1 --pid $i --log-path file_go_pid$i.txt; done ``` 4. Every test should ends with `Success`, unless you do not have a specific target (function) available. diff --git a/.gitignore b/.gitignore index 372ad4ef..6ad5379a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ *.a kernel/legacy_test +tests/legacy_test +tests/go_tester +gotests/go_tester +gotests/gotests .local_libbpf/bpf .local_libbpf/pkgconfig - diff --git a/Makefile b/Makefile index 0e06b157..2870ddb8 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ CC=gcc KERNEL_DIR = kernel/ +TESTS_DIR = tests/ KERNEL_PROGRAM = $(KERNEL_DIR)process_kern.o KERNEL_VERSION="$(shell if [ -f /usr/src/linux/include/config/kernel.release ]; then cat /usr/src/linux/include/config/kernel.release; else cat /proc/sys/kernel/osrelease; fi)" @@ -30,11 +31,12 @@ $(KERNEL_PROGRAM): cd $(KERNEL_DIR) && $(MAKE) all; tester: - cd $(KERNEL_DIR) && $(MAKE) tester + cd $(TESTS_DIR) && $(MAKE) tester clean: rm -f *.o; cd $(KERNEL_DIR) && $(MAKE) clean; + cd $(TESTS_DIR) && $(MAKE) clean; rm -f artifacts/* rm -rf .local_libbpf diff --git a/gotests/cgo_helpers.go b/gotests/cgo_helpers.go new file mode 100644 index 00000000..38a27550 --- /dev/null +++ b/gotests/cgo_helpers.go @@ -0,0 +1,367 @@ +package main + +/* +#cgo CFLAGS: -I../.local_libbpf -I../libbpf/include -I../libbpf/include/uapi -I../libbpf/src +#cgo LDFLAGS: -L../.local_libbpf -lbpf -lz -lelf + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef LIBBPF_MAJOR_VERSION +static int netdata_libbpf_probe_bpf_map_type(unsigned int map_type) +{ + return libbpf_probe_bpf_map_type((enum bpf_map_type)map_type, NULL); +} +#else +static int netdata_libbpf_probe_bpf_map_type(unsigned int map_type) +{ + (void)map_type; + return -EOPNOTSUPP; +} +#endif + +static int netdata_libbpf_get_error(const void *ptr) +{ + return (int)libbpf_get_error(ptr); +} + +static int netdata_libbpf_num_possible_cpus(void) +{ + return libbpf_num_possible_cpus(); +} + +static int netdata_open_capture_socket(int program_fd) +{ + struct sockaddr_ll bind_addr = { 0 }; + struct timeval timeout = { .tv_sec = 1, .tv_usec = 0 }; + int sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + + if (sockfd < 0) + return errno ? -errno : -1; + + bind_addr.sll_family = AF_PACKET; + bind_addr.sll_protocol = htons(ETH_P_ALL); + if (bind(sockfd, (struct sockaddr *)&bind_addr, sizeof(bind_addr))) { + int err = errno ? -errno : -1; + close(sockfd); + return err; + } + + if (setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_BPF, &program_fd, sizeof(program_fd))) { + int err = errno ? -errno : -1; + close(sockfd); + return err; + } + + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout))) { + int err = errno ? -errno : -1; + close(sockfd); + return err; + } + + return sockfd; +} + +static int netdata_set_memlock_rlimit(void) +{ + struct rlimit r = { RLIM_INFINITY, RLIM_INFINITY }; + + if (setrlimit(RLIMIT_MEMLOCK, &r)) + return errno ? -errno : -1; + + return 0; +} + +static int netdata_bpf_map_lookup_elem(int fd, const void *key, void *value) +{ + if (bpf_map_lookup_elem(fd, key, value)) + return errno ? -errno : -1; + + return 0; +} + +static int netdata_bpf_map_get_next_key(int fd, const void *key, void *next_key) +{ + if (bpf_map_get_next_key(fd, key, next_key)) + return errno ? -errno : -1; + + return 0; +} + +static int netdata_bpf_map_update_elem(int fd, const void *key, const void *value, __u64 flags) +{ + if (bpf_map_update_elem(fd, key, value, flags)) + return errno ? -errno : -1; + + return 0; +} + +static int netdata_close_fd(int fd) +{ + return close(fd); +} +*/ +import "C" + +import ( + "errors" + "unsafe" +) + +const ( + bpfProgTypeKprobe = uint32(C.BPF_PROG_TYPE_KPROBE) + + // Map types probed to filter incompatible eBPF objects before load. + bpfMapTypeHash = uint32(C.BPF_MAP_TYPE_HASH) + bpfMapTypeArray = uint32(C.BPF_MAP_TYPE_ARRAY) + bpfMapTypePerCPUHash = uint32(C.BPF_MAP_TYPE_PERCPU_HASH) + bpfMapTypePerCPUArray = uint32(C.BPF_MAP_TYPE_PERCPU_ARRAY) +) + +type bpfObject struct { + ptr *C.struct_bpf_object +} + +type bpfProgram struct { + ptr *C.struct_bpf_program +} + +type bpfMap struct { + ptr *C.struct_bpf_map +} + +type bpfLink struct { + ptr *C.struct_bpf_link +} + +type mapMeta struct { + Name string + FD int + Type uint32 + KeySize uint32 + ValueSize uint32 + MaxEntries uint32 +} + +func openBPFObject(filename string) (*bpfObject, int) { + cFilename := C.CString(filename) + defer C.free(unsafe.Pointer(cFilename)) + + obj := C.bpf_object__open_file(cFilename, nil) + if err := int(C.netdata_libbpf_get_error(unsafe.Pointer(obj))); err != 0 { + return nil, err + } + + return &bpfObject{ptr: obj}, 0 +} + +func (o *bpfObject) close() { + if o != nil && o.ptr != nil { + C.bpf_object__close(o.ptr) + } +} + +func (o *bpfObject) load() int { + return int(C.bpf_object__load(o.ptr)) +} + +func (o *bpfObject) firstProgram() *bpfProgram { + prog := C.bpf_object__next_program(o.ptr, nil) + if prog == nil { + return nil + } + + return &bpfProgram{ptr: prog} +} + +func (o *bpfObject) nextProgram(prev *bpfProgram) *bpfProgram { + var previous *C.struct_bpf_program + if prev != nil { + previous = prev.ptr + } + + prog := C.bpf_object__next_program(o.ptr, previous) + if prog == nil { + return nil + } + + return &bpfProgram{ptr: prog} +} + +func (o *bpfObject) firstMap() *bpfMap { + m := C.bpf_object__next_map(o.ptr, nil) + if m == nil { + return nil + } + + return &bpfMap{ptr: m} +} + +func (o *bpfObject) nextMap(prev *bpfMap) *bpfMap { + var previous *C.struct_bpf_map + if prev != nil { + previous = prev.ptr + } + + m := C.bpf_object__next_map(o.ptr, previous) + if m == nil { + return nil + } + + return &bpfMap{ptr: m} +} + +func (o *bpfObject) countPrograms() int { + total := 0 + for prog := o.firstProgram(); prog != nil; prog = o.nextProgram(prog) { + total++ + } + + return total +} + +func (o *bpfObject) hasSocketFilter() bool { + for prog := o.firstProgram(); prog != nil; prog = o.nextProgram(prog) { + if prog.progType() == uint32(C.BPF_PROG_TYPE_SOCKET_FILTER) { + return true + } + } + + return false +} + +func (o *bpfObject) findSocketFilterProgram() *bpfProgram { + for prog := o.firstProgram(); prog != nil; prog = o.nextProgram(prog) { + if prog.progType() == uint32(C.BPF_PROG_TYPE_SOCKET_FILTER) { + return prog + } + } + + return nil +} + +func (o *bpfObject) findMapByName(name string) *bpfMap { + for m := o.firstMap(); m != nil; m = o.nextMap(m) { + if m.name() == name { + return m + } + } + + return nil +} + +func (p *bpfProgram) name() string { + return C.GoString(C.bpf_program__name(p.ptr)) +} + +func (p *bpfProgram) progType() uint32 { + return uint32(C.bpf_program__type(p.ptr)) +} + +func (p *bpfProgram) fd() int { + return int(C.bpf_program__fd(p.ptr)) +} + +func (p *bpfProgram) attach() (*bpfLink, int) { + link := C.bpf_program__attach(p.ptr) + if err := int(C.netdata_libbpf_get_error(unsafe.Pointer(link))); err != 0 { + return nil, err + } + + return &bpfLink{ptr: link}, 0 +} + +func (p *bpfProgram) attachKprobe(retprobe bool, target string) (*bpfLink, int) { + cTarget := C.CString(target) + defer C.free(unsafe.Pointer(cTarget)) + + link := C.bpf_program__attach_kprobe(p.ptr, C.bool(retprobe), cTarget) + if err := int(C.netdata_libbpf_get_error(unsafe.Pointer(link))); err != 0 { + return nil, err + } + + return &bpfLink{ptr: link}, 0 +} + +func (l *bpfLink) destroy() { + if l != nil && l.ptr != nil { + C.bpf_link__destroy(l.ptr) + } +} + +func (m *bpfMap) meta() mapMeta { + return mapMeta{ + Name: m.name(), + FD: int(C.bpf_map__fd(m.ptr)), + Type: uint32(C.bpf_map__type(m.ptr)), + KeySize: uint32(C.bpf_map__key_size(m.ptr)), + ValueSize: uint32(C.bpf_map__value_size(m.ptr)), + MaxEntries: uint32(C.bpf_map__max_entries(m.ptr)), + } +} + +func (m *bpfMap) name() string { + return C.GoString(C.bpf_map__name(m.ptr)) +} + +func probeMapTypeSupport(mapType uint32) int { + return int(C.netdata_libbpf_probe_bpf_map_type(C.uint(mapType))) +} + +func slicePointer(buf []byte) unsafe.Pointer { + if len(buf) == 0 { + return nil + } + + return unsafe.Pointer(&buf[0]) +} + +func bpfMapLookupElem(fd int, key []byte, value []byte) int { + return int(C.netdata_bpf_map_lookup_elem(C.int(fd), slicePointer(key), slicePointer(value))) +} + +func libbpfNumPossibleCPUs() int { + return int(C.netdata_libbpf_num_possible_cpus()) +} + +func bpfMapGetNextKey(fd int, key []byte, nextKey []byte) int { + return int(C.netdata_bpf_map_get_next_key(C.int(fd), slicePointer(key), slicePointer(nextKey))) +} + +func bpfMapUpdateElem(fd int, key []byte, value []byte, flags uint64) int { + return int(C.netdata_bpf_map_update_elem(C.int(fd), slicePointer(key), slicePointer(value), C.__u64(flags))) +} + +func openCaptureSocket(programFD int) (int, int) { + fd := int(C.netdata_open_capture_socket(C.int(programFD))) + if fd < 0 { + return -1, fd + } + + return fd, 0 +} + +func setMemlockLimit() int { + return int(C.netdata_set_memlock_rlimit()) +} + +func closeFD(fd int) error { + if ret := int(C.netdata_close_fd(C.int(fd))); ret != 0 { + return errors.New("close failed") + } + + return nil +} diff --git a/gotests/dns.go b/gotests/dns.go new file mode 100644 index 00000000..52cf598a --- /dev/null +++ b/gotests/dns.go @@ -0,0 +1,743 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "syscall" + "time" +) + +const ( + dnsCaptureInterval = 5 + dnsTimeoutUsec = 5 * 1000000 + dnsMaxDomainLength = 256 + dnsPacketBuffer = 65536 + dnsIPv4MinHeader = 20 + dnsIPv6Header = 40 + dnsUDPHeader = 8 + dnsTCPMinHeader = 20 + ethHeaderLength = 14 + ethProto8021Q = 0x8100 + ethProto8021AD = 0x88A8 + ethProtoIPv4 = 0x0800 + ethProtoIPv6 = 0x86DD +) + +type dnsFlowKey struct { + family uint8 + protocol uint8 + clientPort uint16 + serverIP [16]byte + clientIP [16]byte +} + +type dnsRcodeCounter struct { + code uint32 + count uint32 +} + +type dnsStats struct { + key dnsFlowKey + queryType uint16 + domain string + timeouts uint32 + successLatencySum uint64 + failureLatencySum uint64 + rcodes []dnsRcodeCounter +} + +type dnsState struct { + key dnsFlowKey + transactionID uint16 + queryType uint16 + timestampUsec uint64 + domain string +} + +type dnsCollector struct { + stats []*dnsStats + state []*dnsState + pendingQueries int + totalResults int +} + +type dnsPacket struct { + key dnsFlowKey + transactionID uint16 + queryType uint16 + response bool + rcode uint8 + domain string +} + +type dnsDebug struct { + stage string + operation string + errorCode int + mapsRequested bool + iterations int + captureSeconds int + programFD int + sockFD int + dnsPortsFound bool + dnsPortsFD int + dnsPortsKeySize uint32 + dnsPortsValueSize uint32 + dnsPortsMaxEntries uint32 + dnsPortsType uint32 +} + +func dnsReadU16(src []byte) uint16 { + return (uint16(src[0]) << 8) | uint16(src[1]) +} + +func dnsNowUsec() uint64 { + return uint64(time.Now().UnixNano() / 1000) +} + +func dnsIPSize(family uint8) int { + if family == syscall.AF_INET6 { + return 16 + } + + return 4 +} + +func dnsFlowKeyEqual(a, b dnsFlowKey) bool { + if a.family != b.family || a.protocol != b.protocol || a.clientPort != b.clientPort { + return false + } + + size := dnsIPSize(a.family) + return bytes.Equal(a.serverIP[:size], b.serverIP[:size]) && bytes.Equal(a.clientIP[:size], b.clientIP[:size]) +} + +func dnsReadName(data []byte, offset int) (string, int, bool) { + current := offset + out := make([]byte, 0, dnsMaxDomainLength) + jumps := 0 + jumped := false + next := 0 + + for current < len(data) && jumps < 32 { + label := data[current] + if label&0xC0 == 0xC0 { + if current+1 >= len(data) { + return "", 0, false + } + + pointer := int(label&0x3F)<<8 | int(data[current+1]) + if !jumped { + next = current + 2 + jumped = true + } + + current = pointer + jumps++ + continue + } + + current++ + if label == 0 { + if !jumped { + next = current + } + + if len(out) == 0 { + return ".", next, true + } + + return string(out), next, true + } + + if label > 63 || current+int(label) > len(data) { + return "", 0, false + } + + if len(out) > 0 { + if len(out)+1 >= dnsMaxDomainLength { + return "", 0, false + } + out = append(out, '.') + } + + if len(out)+int(label) >= dnsMaxDomainLength { + return "", 0, false + } + + for i := 0; i < int(label); i++ { + ch := data[current] + current++ + if ch >= 'A' && ch <= 'Z' { + ch = ch - 'A' + 'a' + } + out = append(out, ch) + } + + jumps++ + } + + return "", 0, false +} + +func dnsParsePayload(payload []byte, protocol uint8) (dnsPacket, bool) { + message := payload + messageLen := len(payload) + offset := 12 + var packet dnsPacket + + if protocol == syscall.IPPROTO_TCP { + if len(payload) < 2 { + return dnsPacket{}, false + } + + dnsLength := int(dnsReadU16(payload)) + if dnsLength == 0 || dnsLength+2 > len(payload) { + return dnsPacket{}, false + } + + message = payload[2:] + messageLen = dnsLength + } + + if messageLen < 12 { + return dnsPacket{}, false + } + + packet.transactionID = dnsReadU16(message) + flags := dnsReadU16(message[2:]) + qdcount := dnsReadU16(message[4:]) + if qdcount != 1 { + return dnsPacket{}, false + } + + domain, next, ok := dnsReadName(message[:messageLen], offset) + if !ok { + return dnsPacket{}, false + } + + offset = next + if offset+4 > messageLen { + return dnsPacket{}, false + } + + packet.queryType = dnsReadU16(message[offset:]) + qclass := dnsReadU16(message[offset+2:]) + if qclass != 1 { + return dnsPacket{}, false + } + + packet.response = flags&0x8000 != 0 + packet.rcode = uint8(flags & 0x000F) + packet.domain = domain + + return packet, true +} + +func dnsParseIPv4(packet []byte, offset int) (dnsPacket, bool) { + if offset+dnsIPv4MinHeader > len(packet) || packet[offset]>>4 != 4 { + return dnsPacket{}, false + } + + ihl := int(packet[offset]&0x0F) * 4 + if ihl < dnsIPv4MinHeader || offset+ihl > len(packet) { + return dnsPacket{}, false + } + + totalLength := int(dnsReadU16(packet[offset+2:])) + if totalLength < ihl { + return dnsPacket{}, false + } + + fragOff := dnsReadU16(packet[offset+6:]) + if fragOff&0x1FFF != 0 { + return dnsPacket{}, false + } + + protocol := packet[offset+9] + if protocol != syscall.IPPROTO_UDP && protocol != syscall.IPPROTO_TCP { + return dnsPacket{}, false + } + + l4Offset := offset + ihl + if offset+totalLength < l4Offset { + return dnsPacket{}, false + } + + l4Length := len(packet) - l4Offset + if offset+totalLength <= len(packet) { + l4Length = offset + totalLength - l4Offset + } + + if l4Length == 0 { + return dnsPacket{}, false + } + + var srcPort, dstPort uint16 + var payload []byte + if protocol == syscall.IPPROTO_UDP { + if l4Length < dnsUDPHeader { + return dnsPacket{}, false + } + srcPort = dnsReadU16(packet[l4Offset:]) + dstPort = dnsReadU16(packet[l4Offset+2:]) + payload = packet[l4Offset+dnsUDPHeader : l4Offset+l4Length] + } else { + if l4Length < dnsTCPMinHeader { + return dnsPacket{}, false + } + srcPort = dnsReadU16(packet[l4Offset:]) + dstPort = dnsReadU16(packet[l4Offset+2:]) + tcpHeaderLength := int((packet[l4Offset+12]>>4)&0x0F) * 4 + if tcpHeaderLength < dnsTCPMinHeader || tcpHeaderLength > l4Length { + return dnsPacket{}, false + } + payload = packet[l4Offset+tcpHeaderLength : l4Offset+l4Length] + } + + dnsPkt, ok := dnsParsePayload(payload, protocol) + if !ok { + return dnsPacket{}, false + } + + dnsPkt.key.family = syscall.AF_INET + dnsPkt.key.protocol = protocol + if !dnsPkt.response { + copy(dnsPkt.key.clientIP[:4], packet[offset+12:offset+16]) + copy(dnsPkt.key.serverIP[:4], packet[offset+16:offset+20]) + dnsPkt.key.clientPort = srcPort + } else { + copy(dnsPkt.key.serverIP[:4], packet[offset+12:offset+16]) + copy(dnsPkt.key.clientIP[:4], packet[offset+16:offset+20]) + dnsPkt.key.clientPort = dstPort + } + + return dnsPkt, true +} + +func dnsParseIPv6(packet []byte, offset int) (dnsPacket, bool) { + if offset+dnsIPv6Header > len(packet) || packet[offset]>>4 != 6 { + return dnsPacket{}, false + } + + payloadSize := int(dnsReadU16(packet[offset+4:])) + protocol := packet[offset+6] + if protocol != syscall.IPPROTO_UDP && protocol != syscall.IPPROTO_TCP { + return dnsPacket{}, false + } + + l4Offset := offset + dnsIPv6Header + if l4Offset > len(packet) { + return dnsPacket{}, false + } + + l4Length := len(packet) - l4Offset + if l4Offset+payloadSize <= len(packet) { + l4Length = payloadSize + } + + if l4Length == 0 { + return dnsPacket{}, false + } + + var srcPort, dstPort uint16 + var payload []byte + if protocol == syscall.IPPROTO_UDP { + if l4Length < dnsUDPHeader { + return dnsPacket{}, false + } + srcPort = dnsReadU16(packet[l4Offset:]) + dstPort = dnsReadU16(packet[l4Offset+2:]) + payload = packet[l4Offset+dnsUDPHeader : l4Offset+l4Length] + } else { + if l4Length < dnsTCPMinHeader { + return dnsPacket{}, false + } + srcPort = dnsReadU16(packet[l4Offset:]) + dstPort = dnsReadU16(packet[l4Offset+2:]) + tcpHeaderLength := int((packet[l4Offset+12]>>4)&0x0F) * 4 + if tcpHeaderLength < dnsTCPMinHeader || tcpHeaderLength > l4Length { + return dnsPacket{}, false + } + payload = packet[l4Offset+tcpHeaderLength : l4Offset+l4Length] + } + + dnsPkt, ok := dnsParsePayload(payload, protocol) + if !ok { + return dnsPacket{}, false + } + + dnsPkt.key.family = syscall.AF_INET6 + dnsPkt.key.protocol = protocol + if !dnsPkt.response { + copy(dnsPkt.key.clientIP[:], packet[offset+8:offset+24]) + copy(dnsPkt.key.serverIP[:], packet[offset+24:offset+40]) + dnsPkt.key.clientPort = srcPort + } else { + copy(dnsPkt.key.serverIP[:], packet[offset+8:offset+24]) + copy(dnsPkt.key.clientIP[:], packet[offset+24:offset+40]) + dnsPkt.key.clientPort = dstPort + } + + return dnsPkt, true +} + +func dnsParsePacket(packet []byte) (dnsPacket, bool) { + if len(packet) < ethHeaderLength { + return dnsPacket{}, false + } + + offset := ethHeaderLength + protocol := dnsReadU16(packet[12:]) + for protocol == ethProto8021Q || protocol == ethProto8021AD { + if offset+4 > len(packet) { + return dnsPacket{}, false + } + protocol = dnsReadU16(packet[offset+2:]) + offset += 4 + } + + switch protocol { + case ethProtoIPv4: + return dnsParseIPv4(packet, offset) + case ethProtoIPv6: + return dnsParseIPv6(packet, offset) + default: + return dnsPacket{}, false + } +} + +func (c *dnsCollector) findStats(key dnsFlowKey, domain string, queryType uint16) *dnsStats { + for _, stats := range c.stats { + if stats.queryType == queryType && stats.domain == domain && dnsFlowKeyEqual(stats.key, key) { + return stats + } + } + + return nil +} + +func (c *dnsCollector) getStats(key dnsFlowKey, domain string, queryType uint16) *dnsStats { + if stats := c.findStats(key, domain, queryType); stats != nil { + return stats + } + + stats := &dnsStats{ + key: key, + queryType: queryType, + domain: domain, + } + + c.stats = append([]*dnsStats{stats}, c.stats...) + c.totalResults++ + return stats +} + +func (s *dnsStats) incrementRcode(rcode uint8) { + for i := range s.rcodes { + if s.rcodes[i].code == uint32(rcode) { + s.rcodes[i].count++ + return + } + } + + s.rcodes = append([]dnsRcodeCounter{{code: uint32(rcode), count: 1}}, s.rcodes...) +} + +func (c *dnsCollector) timeoutState(state *dnsState) { + stats := c.getStats(state.key, state.domain, state.queryType) + stats.timeouts++ +} + +func (c *dnsCollector) expireStates(nowUsec uint64) { + remaining := c.state[:0] + for _, state := range c.state { + if nowUsec-state.timestampUsec > dnsTimeoutUsec { + c.timeoutState(state) + c.pendingQueries-- + continue + } + remaining = append(remaining, state) + } + c.state = remaining +} + +func (c *dnsCollector) processQuery(packet dnsPacket, nowUsec uint64) { + for _, state := range c.state { + if state.transactionID == packet.transactionID && dnsFlowKeyEqual(state.key, packet.key) { + return + } + } + + state := &dnsState{ + key: packet.key, + transactionID: packet.transactionID, + queryType: packet.queryType, + timestampUsec: nowUsec, + domain: packet.domain, + } + + c.state = append([]*dnsState{state}, c.state...) + c.pendingQueries++ +} + +func (c *dnsCollector) processResponse(packet dnsPacket, nowUsec uint64) { + for idx, state := range c.state { + if state.transactionID == packet.transactionID && dnsFlowKeyEqual(state.key, packet.key) { + latency := nowUsec - state.timestampUsec + stats := c.getStats(state.key, state.domain, state.queryType) + if latency > dnsTimeoutUsec { + stats.timeouts++ + } else { + stats.incrementRcode(packet.rcode) + if packet.rcode == 0 { + stats.successLatencySum += latency + } else { + stats.failureLatencySum += latency + } + } + + c.state = append(c.state[:idx], c.state[idx+1:]...) + c.pendingQueries-- + return + } + } +} + +func dnsWritePortsJSON(w io.Writer, meta mapMeta, ports []uint16) { + fmt.Fprintf(w, + " \"dns_ports\" : {\n"+ + " \"Info\" : { \"Length\" : { \"Key\" : %d, \"Value\" : %d},\n"+ + " \"Type\" : %d,\n"+ + " \"FD\" : %d,\n"+ + " \"Configured Ports\" : [", + meta.KeySize, meta.ValueSize, meta.Type, meta.FD) + + for i, port := range ports { + if i > 0 { + fmt.Fprint(w, ", ") + } + fmt.Fprint(w, port) + } + + fmt.Fprintf(w, + "],\n"+ + " \"Data\" : [\n"+ + " { \"Iteration\" : 1, \"Total\" : %d, \"Filled\" : %d, \"Zero\" : %d }\n"+ + " ]\n"+ + " }\n"+ + " }", + meta.MaxEntries, len(ports), int(meta.MaxEntries)-len(ports)) +} + +func dnsWriteRcodesJSON(w io.Writer, rcodes []dnsRcodeCounter) { + fmt.Fprint(w, "{ ") + for i, rcode := range rcodes { + if i > 0 { + fmt.Fprint(w, ", ") + } + fmt.Fprintf(w, "\"%d\" : %d", rcode.code, rcode.count) + } + fmt.Fprint(w, " }") +} + +func dnsWriteResultsJSON(w io.Writer, collector *dnsCollector, captureSeconds int) { + fmt.Fprintf(w, + " \"dns_results\" : {\n"+ + " \"Info\" : { \"Collection Seconds\" : %d,\n"+ + " \"Timeout Window Usec\" : %d,\n"+ + " \"Pending Queries\" : %d,\n"+ + " \"Total Results\" : %d,\n"+ + " \"Data\" : [\n", + captureSeconds, dnsTimeoutUsec, collector.pendingQueries, collector.totalResults) + + for i, stats := range collector.stats { + if i > 0 { + fmt.Fprint(w, ",\n") + } + + serverIP := dnsFormatIP(stats.key.family, stats.key.serverIP) + clientIP := dnsFormatIP(stats.key.family, stats.key.clientIP) + fmt.Fprintf(w, + " { \"server_ip\" : \"%s\", \"client_ip\" : \"%s\", "+ + "\"client_port\" : %d, \"protocol\" : %d, \"query_type\" : %d, \"domain\" : \"%s\", "+ + "\"stats\" : { \"Timeouts\" : %d, \"SuccessLatencySum\" : %d, "+ + "\"FailureLatencySum\" : %d, \"CountByRcode\" : ", + serverIP, clientIP, stats.key.clientPort, stats.key.protocol, stats.queryType, stats.domain, + stats.timeouts, stats.successLatencySum, stats.failureLatencySum) + dnsWriteRcodesJSON(w, stats.rcodes) + fmt.Fprint(w, " } }") + } + + if len(collector.stats) > 0 { + fmt.Fprint(w, "\n") + } + + fmt.Fprint(w, + " ]\n"+ + " }\n"+ + " }") +} + +func dnsWriteFailureDebug(w io.Writer, ports []uint16, debug dnsDebug) { + fmt.Fprintf(w, + " \"Debug\" : {\n"+ + " \"Info\" : { \"Stage\" : \"%s\",\n"+ + " \"Operation\" : \"%s\",\n"+ + " \"Error Code\" : %d,\n"+ + " \"Error Message\" : \"%s\",\n"+ + " \"Maps Requested\" : %d,\n"+ + " \"Iterations\" : %d,\n"+ + " \"Capture Seconds\" : %d,\n"+ + " \"Program FD\" : %d,\n"+ + " \"Socket FD\" : %d,\n"+ + " \"dns_ports Found\" : %d,\n"+ + " \"dns_ports FD\" : %d,\n"+ + " \"dns_ports Type\" : %d,\n"+ + " \"dns_ports Key Size\" : %d,\n"+ + " \"dns_ports Value Size\" : %d,\n"+ + " \"dns_ports Max Entries\" : %d,\n"+ + " \"Configured Ports\" : [", + debug.stage, debug.operation, debug.errorCode, describeError(debug.errorCode), + boolToInt(debug.mapsRequested), debug.iterations, debug.captureSeconds, + debug.programFD, debug.sockFD, boolToInt(debug.dnsPortsFound), debug.dnsPortsFD, + debug.dnsPortsType, debug.dnsPortsKeySize, debug.dnsPortsValueSize, debug.dnsPortsMaxEntries) + + for i, port := range ports { + if i > 0 { + fmt.Fprint(w, ", ") + } + fmt.Fprint(w, port) + } + + fmt.Fprint(w, "]\n }\n }\n") +} + +func runDNSSocketFilterTester(obj *bpfObject, maps bool, w io.Writer, iterations int, ports []uint16) string { + const ( + success = "Success" + failure = "Fail" + ) + + debug := dnsDebug{ + stage: "initializing", + operation: "initialize", + mapsRequested: maps, + iterations: iterations, + captureSeconds: iterations * dnsCaptureInterval, + programFD: -1, + sockFD: -1, + dnsPortsFD: -1, + } + + if loadErr := obj.load(); loadErr != 0 { + debug.stage = "load_object" + debug.operation = "bpf_object__load" + debug.errorCode = loadErr + dnsWriteFailureDebug(w, ports, debug) + return failure + } + + prog := obj.findSocketFilterProgram() + if prog == nil { + debug.stage = "find_socket_filter_program" + debug.operation = "bpf_object__next_program" + debug.errorCode = -int(syscall.ENOENT) + dnsWriteFailureDebug(w, ports, debug) + return failure + } + + debug.programFD = prog.fd() + + dnsPortsMap := obj.findMapByName("dns_ports") + if dnsPortsMap == nil { + debug.stage = "configure_ports" + debug.operation = "find_dns_ports_map" + debug.errorCode = -int(syscall.ENOENT) + dnsWriteFailureDebug(w, ports, debug) + return failure + } + + meta := dnsPortsMap.meta() + debug.dnsPortsFound = true + debug.dnsPortsFD = meta.FD + debug.dnsPortsType = meta.Type + debug.dnsPortsKeySize = meta.KeySize + debug.dnsPortsValueSize = meta.ValueSize + debug.dnsPortsMaxEntries = meta.MaxEntries + + for _, port := range ports { + key := make([]byte, meta.KeySize) + value := make([]byte, meta.ValueSize) + putUint16(key, port) + if len(value) > 0 { + value[0] = 1 + } + if err := bpfMapUpdateElem(meta.FD, key, value, 0); err != 0 { + debug.stage = "configure_ports" + debug.operation = "bpf_map_update_elem" + debug.errorCode = err + dnsWriteFailureDebug(w, ports, debug) + return failure + } + } + + sockFD, sockErr := openCaptureSocket(debug.programFD) + if sockErr != 0 { + debug.stage = "open_capture_socket" + debug.operation = "netdata_open_capture_socket" + debug.errorCode = sockErr + dnsWriteFailureDebug(w, ports, debug) + return failure + } + defer closeFD(sockFD) + + debug.sockFD = sockFD + + if maps { + collector := &dnsCollector{} + dnsCollectPackets(sockFD, collector, debug.captureSeconds) + dnsWritePortsJSON(w, meta, ports) + fmt.Fprint(w, ",\n") + dnsWriteResultsJSON(w, collector, debug.captureSeconds) + fmt.Fprint(w, ",\n \"Total tables\" : 2\n") + } + + return success +} + +func dnsCollectPackets(sockFD int, collector *dnsCollector, captureSeconds int) { + packet := make([]byte, dnsPacketBuffer) + deadline := time.Now().Add(time.Duration(captureSeconds) * time.Second) + + for time.Now().Before(deadline) { + n, err := syscall.Read(sockFD, packet) + nowUsec := dnsNowUsec() + collector.expireStates(nowUsec) + + if err != nil { + if err == syscall.EINTR || err == syscall.EAGAIN || err == syscall.EWOULDBLOCK { + continue + } + break + } + + if n <= 0 { + continue + } + + dnsPacket, ok := dnsParsePacket(packet[:n]) + if !ok { + continue + } + + if !dnsPacket.response { + collector.processQuery(dnsPacket, nowUsec) + } else { + collector.processResponse(dnsPacket, nowUsec) + } + } + + collector.expireStates(dnsNowUsec()) +} diff --git a/gotests/dns_test.go b/gotests/dns_test.go new file mode 100644 index 00000000..ae040c17 --- /dev/null +++ b/gotests/dns_test.go @@ -0,0 +1,430 @@ +package main + +import ( + "bytes" + "encoding/binary" + "net" + "strings" + "syscall" + "testing" +) + +func encodeDNSName(domain string) []byte { + if domain == "." { + return []byte{0} + } + + var out []byte + for _, label := range strings.Split(domain, ".") { + out = append(out, byte(len(label))) + out = append(out, []byte(label)...) + } + + return append(out, 0) +} + +func buildDNSMessage(id uint16, flags uint16, domain string, qtype uint16) []byte { + question := encodeDNSName(domain) + msg := make([]byte, 12, 12+len(question)+4) + binary.BigEndian.PutUint16(msg[0:], id) + binary.BigEndian.PutUint16(msg[2:], flags) + binary.BigEndian.PutUint16(msg[4:], 1) + msg = append(msg, question...) + + qtail := make([]byte, 4) + binary.BigEndian.PutUint16(qtail[0:], qtype) + binary.BigEndian.PutUint16(qtail[2:], 1) + msg = append(msg, qtail...) + + return msg +} + +func buildUDPDatagram(srcPort, dstPort uint16, payload []byte) []byte { + out := make([]byte, dnsUDPHeader+len(payload)) + binary.BigEndian.PutUint16(out[0:], srcPort) + binary.BigEndian.PutUint16(out[2:], dstPort) + binary.BigEndian.PutUint16(out[4:], uint16(len(out))) + copy(out[dnsUDPHeader:], payload) + return out +} + +func buildTCPSegment(srcPort, dstPort uint16, payload []byte) []byte { + out := make([]byte, dnsTCPMinHeader+len(payload)) + binary.BigEndian.PutUint16(out[0:], srcPort) + binary.BigEndian.PutUint16(out[2:], dstPort) + out[12] = 5 << 4 + copy(out[dnsTCPMinHeader:], payload) + return out +} + +func buildIPv4Packet(srcIP, dstIP [4]byte, protocol uint8, l4 []byte) []byte { + out := make([]byte, dnsIPv4MinHeader+len(l4)) + out[0] = 0x45 + binary.BigEndian.PutUint16(out[2:], uint16(len(out))) + out[8] = 64 + out[9] = protocol + copy(out[12:16], srcIP[:]) + copy(out[16:20], dstIP[:]) + copy(out[dnsIPv4MinHeader:], l4) + return out +} + +func buildIPv6Packet(srcIP, dstIP [16]byte, protocol uint8, l4 []byte) []byte { + out := make([]byte, dnsIPv6Header+len(l4)) + out[0] = 0x60 + binary.BigEndian.PutUint16(out[4:], uint16(len(l4))) + out[6] = protocol + out[7] = 64 + copy(out[8:24], srcIP[:]) + copy(out[24:40], dstIP[:]) + copy(out[dnsIPv6Header:], l4) + return out +} + +func buildEthernetFrame(etherType uint16, payload []byte) []byte { + out := make([]byte, ethHeaderLength+len(payload)) + binary.BigEndian.PutUint16(out[12:], etherType) + copy(out[ethHeaderLength:], payload) + return out +} + +func buildVLANFrame(innerType uint16, payload []byte) []byte { + out := make([]byte, ethHeaderLength+4+len(payload)) + binary.BigEndian.PutUint16(out[12:], ethProto8021Q) + binary.BigEndian.PutUint16(out[16:], innerType) + copy(out[18:], payload) + return out +} + +func mustIPv6(t *testing.T, value string) [16]byte { + t.Helper() + + ip := net.ParseIP(value) + if ip == nil { + t.Fatalf("invalid IPv6 literal %q", value) + } + + var out [16]byte + copy(out[:], ip.To16()) + return out +} + +func TestDNSReadName(t *testing.T) { + t.Run("simple name is lowercased", func(t *testing.T) { + data := encodeDNSName("WWW.Example.COM") + + got, next, ok := dnsReadName(data, 0) + if !ok { + t.Fatal("dnsReadName returned ok=false") + } + if got != "www.example.com" { + t.Fatalf("unexpected domain: %q", got) + } + if next != len(data) { + t.Fatalf("unexpected next offset: got %d want %d", next, len(data)) + } + }) + + t.Run("compressed name follows pointer", func(t *testing.T) { + base := encodeDNSName("example.com") + data := append(append([]byte{}, base...), 3, 'w', 'w', 'w', 0xC0, 0x00) + + got, next, ok := dnsReadName(data, len(base)) + if !ok { + t.Fatal("dnsReadName returned ok=false") + } + if got != "www.example.com" { + t.Fatalf("unexpected domain: %q", got) + } + if next != len(base)+6 { + t.Fatalf("unexpected next offset: got %d want %d", next, len(base)+6) + } + }) + + t.Run("truncated label is rejected", func(t *testing.T) { + if _, _, ok := dnsReadName([]byte{3, 'w', 'w'}, 0); ok { + t.Fatal("expected truncated label to fail") + } + }) +} + +func TestDNSParsePayload(t *testing.T) { + t.Run("udp query", func(t *testing.T) { + payload := buildDNSMessage(0x1234, 0x0100, "WWW.Example.COM", 1) + + packet, ok := dnsParsePayload(payload, syscall.IPPROTO_UDP) + if !ok { + t.Fatal("dnsParsePayload returned ok=false") + } + if packet.transactionID != 0x1234 { + t.Fatalf("unexpected transaction ID: %#x", packet.transactionID) + } + if packet.queryType != 1 { + t.Fatalf("unexpected query type: %d", packet.queryType) + } + if packet.response { + t.Fatal("expected query packet") + } + if packet.domain != "www.example.com" { + t.Fatalf("unexpected domain: %q", packet.domain) + } + }) + + t.Run("tcp response", func(t *testing.T) { + msg := buildDNSMessage(0x4321, 0x8003, "example.com", 28) + payload := make([]byte, 2, 2+len(msg)) + binary.BigEndian.PutUint16(payload, uint16(len(msg))) + payload = append(payload, msg...) + + packet, ok := dnsParsePayload(payload, syscall.IPPROTO_TCP) + if !ok { + t.Fatal("dnsParsePayload returned ok=false") + } + if !packet.response { + t.Fatal("expected response packet") + } + if packet.rcode != 3 { + t.Fatalf("unexpected rcode: %d", packet.rcode) + } + if packet.queryType != 28 { + t.Fatalf("unexpected query type: %d", packet.queryType) + } + }) + + t.Run("invalid question count is rejected", func(t *testing.T) { + payload := buildDNSMessage(0x9999, 0x0100, "example.com", 1) + binary.BigEndian.PutUint16(payload[4:], 2) + + if _, ok := dnsParsePayload(payload, syscall.IPPROTO_UDP); ok { + t.Fatal("expected invalid question count to fail") + } + }) +} + +func TestDNSParsePacket(t *testing.T) { + t.Run("ipv4 udp query over vlan", func(t *testing.T) { + payload := buildDNSMessage(0x1111, 0x0100, "example.com", 1) + ip := buildIPv4Packet( + [4]byte{192, 0, 2, 10}, + [4]byte{198, 51, 100, 53}, + syscall.IPPROTO_UDP, + buildUDPDatagram(12345, 53, payload), + ) + frame := buildVLANFrame(ethProtoIPv4, ip) + + packet, ok := dnsParsePacket(frame) + if !ok { + t.Fatal("dnsParsePacket returned ok=false") + } + if packet.key.family != syscall.AF_INET { + t.Fatalf("unexpected family: %d", packet.key.family) + } + if packet.key.protocol != syscall.IPPROTO_UDP { + t.Fatalf("unexpected protocol: %d", packet.key.protocol) + } + if packet.key.clientPort != 12345 { + t.Fatalf("unexpected client port: %d", packet.key.clientPort) + } + if !bytes.Equal(packet.key.clientIP[:4], []byte{192, 0, 2, 10}) { + t.Fatalf("unexpected client ip: %v", packet.key.clientIP[:4]) + } + if !bytes.Equal(packet.key.serverIP[:4], []byte{198, 51, 100, 53}) { + t.Fatalf("unexpected server ip: %v", packet.key.serverIP[:4]) + } + }) + + t.Run("ipv6 tcp response", func(t *testing.T) { + msg := buildDNSMessage(0x2222, 0x8000, "ipv6.example.com", 28) + tcpPayload := make([]byte, 2, 2+len(msg)) + binary.BigEndian.PutUint16(tcpPayload, uint16(len(msg))) + tcpPayload = append(tcpPayload, msg...) + + src := mustIPv6(t, "2001:db8::53") + dst := mustIPv6(t, "2001:db8::1234") + ip := buildIPv6Packet(src, dst, syscall.IPPROTO_TCP, buildTCPSegment(53, 40000, tcpPayload)) + frame := buildEthernetFrame(ethProtoIPv6, ip) + + packet, ok := dnsParsePacket(frame) + if !ok { + t.Fatal("dnsParsePacket returned ok=false") + } + if packet.key.family != syscall.AF_INET6 { + t.Fatalf("unexpected family: %d", packet.key.family) + } + if packet.key.protocol != syscall.IPPROTO_TCP { + t.Fatalf("unexpected protocol: %d", packet.key.protocol) + } + if packet.key.clientPort != 40000 { + t.Fatalf("unexpected client port: %d", packet.key.clientPort) + } + if packet.domain != "ipv6.example.com" { + t.Fatalf("unexpected domain: %q", packet.domain) + } + if !bytes.Equal(packet.key.serverIP[:], src[:]) { + t.Fatalf("unexpected server ip: %v", packet.key.serverIP) + } + if !bytes.Equal(packet.key.clientIP[:], dst[:]) { + t.Fatalf("unexpected client ip: %v", packet.key.clientIP) + } + }) +} + +func TestDNSParseIPv4RejectsFragments(t *testing.T) { + payload := buildDNSMessage(0x3333, 0x0100, "fragment.example.com", 1) + packet := buildIPv4Packet( + [4]byte{192, 0, 2, 1}, + [4]byte{198, 51, 100, 1}, + syscall.IPPROTO_UDP, + buildUDPDatagram(53000, 53, payload), + ) + binary.BigEndian.PutUint16(packet[6:], 1) + + if _, ok := dnsParseIPv4(packet, 0); ok { + t.Fatal("expected fragmented packet to fail") + } +} + +func TestDNSCollectorStateManagement(t *testing.T) { + key := dnsFlowKey{ + family: syscall.AF_INET, + protocol: syscall.IPPROTO_UDP, + clientPort: 53000, + serverIP: [16]byte{198, 51, 100, 10}, + clientIP: [16]byte{192, 0, 2, 20}, + } + + t.Run("query deduplication and successful response", func(t *testing.T) { + collector := &dnsCollector{} + query := dnsPacket{key: key, transactionID: 7, queryType: 1, domain: "example.com"} + + collector.processQuery(query, 100) + collector.processQuery(query, 120) + + if collector.pendingQueries != 1 || len(collector.state) != 1 { + t.Fatalf("unexpected collector state after dedupe: pending=%d states=%d", collector.pendingQueries, len(collector.state)) + } + + response := query + response.response = true + collector.processResponse(response, 600) + + if collector.pendingQueries != 0 || len(collector.state) != 0 { + t.Fatalf("unexpected collector state after response: pending=%d states=%d", collector.pendingQueries, len(collector.state)) + } + if collector.totalResults != 1 || len(collector.stats) != 1 { + t.Fatalf("unexpected stats count: totalResults=%d stats=%d", collector.totalResults, len(collector.stats)) + } + + stats := collector.stats[0] + if stats.successLatencySum != 500 { + t.Fatalf("unexpected success latency: %d", stats.successLatencySum) + } + if stats.failureLatencySum != 0 || stats.timeouts != 0 { + t.Fatalf("unexpected failure stats: failure=%d timeouts=%d", stats.failureLatencySum, stats.timeouts) + } + if len(stats.rcodes) != 1 || stats.rcodes[0].code != 0 || stats.rcodes[0].count != 1 { + t.Fatalf("unexpected rcodes: %+v", stats.rcodes) + } + }) + + t.Run("failure response tracks rcode", func(t *testing.T) { + collector := &dnsCollector{} + query := dnsPacket{key: key, transactionID: 8, queryType: 28, domain: "failure.example.com"} + + collector.processQuery(query, 1_000) + response := query + response.response = true + response.rcode = 3 + collector.processResponse(response, 1_900) + + stats := collector.stats[0] + if stats.failureLatencySum != 900 { + t.Fatalf("unexpected failure latency: %d", stats.failureLatencySum) + } + if len(stats.rcodes) != 1 || stats.rcodes[0].code != 3 || stats.rcodes[0].count != 1 { + t.Fatalf("unexpected rcodes: %+v", stats.rcodes) + } + }) + + t.Run("expired states become timeouts", func(t *testing.T) { + collector := &dnsCollector{} + query := dnsPacket{key: key, transactionID: 9, queryType: 15, domain: "timeout.example.com"} + + collector.processQuery(query, 10) + collector.expireStates(10 + dnsTimeoutUsec + 1) + + if collector.pendingQueries != 0 || len(collector.state) != 0 { + t.Fatalf("unexpected collector state after expire: pending=%d states=%d", collector.pendingQueries, len(collector.state)) + } + if collector.totalResults != 1 || len(collector.stats) != 1 { + t.Fatalf("unexpected stats count: totalResults=%d stats=%d", collector.totalResults, len(collector.stats)) + } + if collector.stats[0].timeouts != 1 { + t.Fatalf("unexpected timeout count: %d", collector.stats[0].timeouts) + } + if len(collector.stats[0].rcodes) != 0 { + t.Fatalf("unexpected rcodes for timeout-only entry: %+v", collector.stats[0].rcodes) + } + }) +} + +func TestDNSJSONWriters(t *testing.T) { + t.Run("ports writer includes metadata and ports", func(t *testing.T) { + var out bytes.Buffer + dnsWritePortsJSON(&out, mapMeta{KeySize: 2, ValueSize: 4, Type: 1, FD: 9, MaxEntries: 8}, []uint16{53, 5353}) + + got := out.String() + for _, want := range []string{ + `"dns_ports"`, + `"FD" : 9`, + `"Configured Ports" : [53, 5353]`, + `"Filled" : 2`, + `"Zero" : 6`, + } { + if !strings.Contains(got, want) { + t.Fatalf("missing %q in output: %s", want, got) + } + } + }) + + t.Run("results writer renders IPs and rcodes", func(t *testing.T) { + var out bytes.Buffer + collector := &dnsCollector{ + pendingQueries: 1, + totalResults: 1, + stats: []*dnsStats{{ + key: dnsFlowKey{ + family: syscall.AF_INET, + protocol: syscall.IPPROTO_UDP, + clientPort: 53123, + serverIP: [16]byte{8, 8, 8, 8}, + clientIP: [16]byte{192, 0, 2, 44}, + }, + queryType: 1, + domain: "example.com", + timeouts: 2, + successLatencySum: 17, + failureLatencySum: 5, + rcodes: []dnsRcodeCounter{ + {code: 0, count: 3}, + {code: 3, count: 1}, + }, + }}, + } + + dnsWriteResultsJSON(&out, collector, 5) + got := out.String() + for _, want := range []string{ + `"Collection Seconds" : 5`, + `"Pending Queries" : 1`, + `"server_ip" : "8.8.8.8"`, + `"client_ip" : "192.0.2.44"`, + `"domain" : "example.com"`, + `"CountByRcode" : { "0" : 3, "3" : 1 }`, + } { + if !strings.Contains(got, want) { + t.Fatalf("missing %q in output: %s", want, got) + } + } + }) +} diff --git a/gotests/go.mod b/gotests/go.mod new file mode 100644 index 00000000..5a7d220d --- /dev/null +++ b/gotests/go.mod @@ -0,0 +1,3 @@ +module github.com/netdata/kernel-collector/gotests + +go 1.25.0 diff --git a/gotests/main.go b/gotests/main.go new file mode 100644 index 00000000..99351d4e --- /dev/null +++ b/gotests/main.go @@ -0,0 +1,1454 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strconv" + "strings" + "syscall" + "time" +) + +const ( + versionStringLen = 256 + netdataDefaultProcessNumber = 4096 + netdataDNSMaxPorts = 32 + netdataDNSDefaultPort = 53 + netdataControllerEnd = 6 + netdataMinimumEBPFKernel = 264960 + netdataEBPFKernel414 = 265728 + netdataEBPFKernel415 = 265984 + netdataEBPFKernel417 = 266496 + netdataEBPFKernel54 = 328704 + netdataEBPFKernel510 = 330240 + netdataEBPFKernel511 = 330496 + netdataEBPFKernel514 = 331264 + netdataEBPFKernel515 = 331520 + netdataEBPFKernel516 = 331776 + netdataEBPFKernel68 = 395264 + + netdataV310 = 1 << 0 + netdataV414 = 1 << 1 + netdataV416 = 1 << 2 + netdataV418 = 1 << 3 + netdataV54 = 1 << 4 + netdataV510 = 1 << 5 + netdataV511 = 1 << 6 + netdataV514 = 1 << 7 + netdataV515 = 1 << 8 + netdataV516 = 1 << 9 + netdataV68 = 1 << 10 + + flagBtrfs uint64 = 1 << 0 + flagCachestat uint64 = 1 << 1 + flagDC uint64 = 1 << 2 + flagDisk uint64 = 1 << 3 + flagExt4 uint64 = 1 << 4 + flagFD uint64 = 1 << 5 + flagSync uint64 = 1 << 6 + flagHardIRQ uint64 = 1 << 7 + flagMDFlush uint64 = 1 << 8 + flagMount uint64 = 1 << 9 + flagNetworkViewer uint64 = 1 << 10 + flagOOMKill uint64 = 1 << 11 + flagProcess uint64 = 1 << 12 + flagSHM uint64 = 1 << 13 + flagSocket uint64 = 1 << 14 + flagSoftIRQ uint64 = 1 << 15 + flagSwap uint64 = 1 << 16 + flagVFS uint64 = 1 << 17 + flagNFS uint64 = 1 << 18 + flagXFS uint64 = 1 << 19 + flagZFS uint64 = 1 << 20 + flagLoadBinary uint64 = 1 << 21 + flagContent uint64 = 1 << 22 + flagDNS uint64 = 1 << 23 + + flagFS uint64 = flagBtrfs | flagExt4 | flagVFS | flagNFS | flagXFS | flagZFS + flagAll uint64 = ^uint64(0) +) + +type specifyName struct { + programName string + functionToAttach string + fallbackFunction string + optional string + retprobe bool + required bool +} + +type module struct { + kernels uint32 + flags uint64 + name string + updateNames *[]specifyName + ctrlTable string +} + +type options struct { + flags uint64 + specificEBPF string + netdataPath string + logPath string + iterations int + mapLevel int + dnsPorts []uint16 + unitTest bool + showHelp bool +} + +type logState struct { + writer io.Writer + file *os.File +} + +type attachSummary struct { + links []*bpfLink + success int + fail int + skipped int + lastError int + failedProgramName string + failedProgramType uint32 +} + +type tableData struct { + key []byte + nextKey []byte + value []byte + defValue []byte + keyLength int + valueLength int + filled uint64 + zero uint64 +} + +var ( + dcOptionalNames = []specifyName{ + { + programName: "netdata_lookup_fast", + functionToAttach: "lookup_fast", + retprobe: false, + }, + } + + swapOptionalNames = []specifyName{ + { + programName: "netdata_swap_readpage", + functionToAttach: "swap_read_folio", + fallbackFunction: "swap_readpage", + retprobe: false, + required: true, + }, + { + programName: "netdata_swap_writepage", + functionToAttach: "__swap_writepage", + fallbackFunction: "swap_writepage", + retprobe: false, + required: true, + }, + } + + zfsOptionalNames = []specifyName{ + { + programName: "netdata_zpl_iter_read", + functionToAttach: "zpl_iter_read", + retprobe: false, + }, + { + programName: "netdata_zpl_iter_write", + functionToAttach: "zpl_iter_write", + retprobe: false, + }, + { + programName: "netdata_zpl_open", + functionToAttach: "zpl_open", + retprobe: false, + }, + { + programName: "netdata_zpl_fsync", + functionToAttach: "zpl_fsync", + retprobe: false, + }, + { + programName: "netdata_ret_zpl_iter_read", + functionToAttach: "zpl_iter_read", + retprobe: true, + }, + { + programName: "netdata_ret_zpl_iter_write", + functionToAttach: "zpl_iter_write", + retprobe: true, + }, + { + programName: "netdata_ret_zpl_open", + functionToAttach: "zpl_open", + retprobe: true, + }, + { + programName: "netdata_ret_zpl_fsync", + functionToAttach: "zpl_fsync", + retprobe: true, + }, + } + + ebpfModules = []module{ + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV510 | netdataV514, flags: flagBtrfs, name: "btrfs", ctrlTable: "btrfs_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV515 | netdataV514 | netdataV516, flags: flagCachestat, name: "cachestat", ctrlTable: "cstat_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagDC, name: "dc", updateNames: &dcOptionalNames, ctrlTable: "dcstat_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagDisk, name: "disk", ctrlTable: "disk_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagExt4, name: "ext4", ctrlTable: "ext4_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV511 | netdataV514, flags: flagFD, name: "fd", ctrlTable: "fd_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagSync, name: "fdatasync"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagSync, name: "fsync"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagHardIRQ, name: "hardirq"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagMDFlush, name: "mdflush"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagMount, name: "mount"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagSync, name: "msync"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagSocket, name: "socket", ctrlTable: "socket_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagDNS, name: "dns"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagNFS, name: "nfs", ctrlTable: "nfs_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagNetworkViewer, name: "network_viewer", ctrlTable: "nv_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagOOMKill, name: "oomkill"}, + {kernels: netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514 | netdataV510, flags: flagProcess, name: "process", ctrlTable: "process_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagSHM, name: "shm", ctrlTable: "shm_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagSoftIRQ, name: "softirq"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagSync, name: "sync"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagSync, name: "syncfs"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagSync, name: "sync_file_range"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514 | netdataV68, flags: flagSwap, name: "swap", updateNames: &swapOptionalNames, ctrlTable: "swap_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagVFS, name: "vfs", ctrlTable: "vfs_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagXFS, name: "xfs", ctrlTable: "xfs_ctrl"}, + {kernels: netdataV310 | netdataV414 | netdataV416 | netdataV418 | netdataV54 | netdataV514, flags: flagZFS, name: "zfs", updateNames: &zfsOptionalNames, ctrlTable: "zfs_ctrl"}, + } +) + +func main() { + os.Exit(run()) +} + +func run() int { + kernelVersion := getKernelVersion() + rhfVersion := getRedHatRelease() + nprocesses := libbpfNumPossibleCPUs() + if nprocesses < 1 { + nprocesses = runtime.NumCPU() + } + if nprocesses < 1 { + fmt.Fprintf(os.Stderr, "Cannot find number of process, using the default %d\n", netdataDefaultProcessNumber) + nprocesses = netdataDefaultProcessNumber + } + + logger := &logState{writer: os.Stderr} + opts, parseCode := parseArguments(os.Args[1:], kernelVersion, logger) + if logger.file != nil { + defer logger.file.Close() + } + if parseCode != 0 || opts.showHelp { + return parseCode + } + if opts.unitTest { + return runUnitTests() + } + + writer := bufio.NewWriter(logger.writer) + defer writer.Flush() + + fmt.Fprint(writer, "{") + if err := memlockLimit(); err != nil { + writeErrorExit(writer, "Cannot adjust memory limit.") + return 2 + } + + if opts.flags&flagLoadBinary == 0 { + fillNames() + runNetdataTests(writer, rhfVersion, kernelVersion, true, opts, nprocesses) + runNetdataTests(writer, rhfVersion, kernelVersion, false, opts, nprocesses) + } else if opts.specificEBPF != "" { + startExternalJSON(writer, opts.specificEBPF) + result := ebpfTester(writer, opts.specificEBPF, nil, opts.flags&flagContent != 0, "", opts, nprocesses) + fmt.Fprintf(writer, " },\n \"Status\" : \"%s\"\n},\n", result) + } + + fmt.Fprint(writer, "\"End\" : \"Good bye!!!\" }\n") + return 0 +} + +func getKernelVersion() int { + data, err := os.ReadFile("/proc/sys/kernel/osrelease") + if err != nil { + return -1 + } + + return parseKernelRelease(strings.TrimSpace(string(data))) +} + +func parseLeadingLong(s string) int { + s = strings.TrimSpace(s) + sign := 1 + if strings.HasPrefix(s, "-") { + sign = -1 + s = s[1:] + } else if strings.HasPrefix(s, "+") { + s = s[1:] + } + + n := 0 + for _, r := range s { + if r < '0' || r > '9' { + break + } + n = n*10 + int(r-'0') + } + + return sign * n +} + +func parseKernelRelease(version string) int { + parts := strings.SplitN(version, ".", 3) + if len(parts) < 3 { + return -1 + } + + patch := parts[2] + if idx := strings.IndexAny(patch, "-\n"); idx >= 0 { + patch = patch[:idx] + } + + major := parseLeadingLong(parts[0]) + minor := parseLeadingLong(parts[1]) + sublevel := parseLeadingLong(patch) + if major < 0 || minor < 0 || sublevel < 0 { + return -1 + } + + if sublevel > 255 { + sublevel = 255 + } + + return major*65536 + minor*256 + sublevel +} + +func getRedHatRelease() int { + data, err := os.ReadFile("/etc/redhat-release") + if err != nil { + return -1 + } + + return parseRedHatRelease(string(data)) +} + +func parseRedHatRelease(release string) int { + major := 0 + minor := -1 + + if len(release) <= 4 { + return -1 + } + + if idx := strings.IndexByte(release, '.'); idx >= 0 { + head := release[:idx] + if idx > 0 { + major = parseLeadingLong(head[idx-1:]) + tail := release[idx+1:] + if end := strings.IndexByte(tail, ' '); end >= 0 { + minor = parseLeadingLong(tail[:end]) + } + } + } + + return major*256 + minor +} + +func memlockLimit() error { + if ret := setMemlockLimit(); ret != 0 { + return fmt.Errorf("setrlimit failed: %d", ret) + } + + return nil +} + +func helpText(exe string) string { + return fmt.Sprintf("Usage: ./%s [OPTION]....\n"+ + "Load eBPF binaries printing final status of the test.\n\n"+ + "The following global options are available:\n"+ + "--help Prints this help.\n"+ + "--unit-test Run Go unit tests for gotests and exit.\n"+ + "--all Test all netdata eBPF programs.\n"+ + "--common Test eBPF programs that does not need specific module to be loaded.\n"+ + " This option does not test mdflush, ext4, nfs, zfs, xfs and btrfs.\n"+ + "--load-binary Load a given eBPF program into kernel.\n"+ + "--dns-port Comma separated list of DNS ports to monitor. Default value is 53.\n"+ + "--netdata-path Directory where eBPF programs are present.\n"+ + "--log-path Filename to write log information. When this option is not given,\n"+ + " software will use stderr.\n\n"+ + "--content Test content stored inside hash tables.\n"+ + "--iteration Number of iterations when content is read, default value is 1.\n"+ + "--pid Specify the number that identifies PID that will be monitored: 0 - Real Parent PID (Default), 1 - Parent PID, and 2 - All PID \n\n"+ + "You can also specify an unique eBPF program developed by Netdata with the following\n"+ + "options:\n"+ + "--btrfs Latency for btrfs.\n"+ + "--cachestat Linux page cache.\n"+ + "--dc Linux directory cache.\n"+ + "--disk Disk latency using tracepoints.\n"+ + "--ext4 Latency for ext4.\n"+ + "--filedescriptor File descriptor actions(open and close).\n"+ + "--sync Calls for sync (2) syscall.\n"+ + "--hardirq Latency for hard IRQ.\n"+ + "--mdflush Calls for md_flush_request.\n"+ + "--mount Calls for mount (2) and umount (2) syscalls.\n"+ + "--networkviewer Network Viewer.\n"+ + "--oomkill Monitoring oomkill events.\n"+ + "--process Monitoring process life(Threads, start, exit).\n"+ + "--shm Calls for syscalls shmget(2), shmat (2), shmdt (2), and shmctl (2).\n"+ + "--socket Monitoring for TCP and UDP traffic.\n"+ + "--dns Monitoring DNS traffic with socket/dns_filter and local aggregation.\n"+ + "--softirq Latency for soft IRQ.\n"+ + "--swap Monitor the exact time that processes try to execute IO events in swap.\n"+ + "--vfs Monitor Virtual Filesystem functions.\n"+ + "--nfs Latency for Network Filesystem NFS.\n"+ + "--xfs Latency for XFS.\n"+ + "--zfs Latency for ZFS.\n\n"+ + "Exit status:\n"+ + "0 if OK.\n"+ + "1 if kernel version cannot load eBPF programs.\n"+ + "2 if software cannot adjust memory or cannot start unit tests.\n"+ + "When --unit-test is used, the process returns the go test exit status.\n", exe) +} + +func setCommonFlag() uint64 { + return flagAll & ^(flagFS | flagLoadBinary | flagMDFlush | flagContent) +} + +func parseArguments(args []string, kernelVersion int, logger *logState) (options, int) { + opts := options{ + iterations: 1, + mapLevel: 0, + dnsPorts: []uint16{netdataDNSDefaultPort}, + } + + moduleOptions := map[string]uint64{ + "btrfs": flagBtrfs, + "cachestat": flagCachestat, + "dc": flagDC, + "disk": flagDisk, + "ext4": flagExt4, + "filedescriptor": flagFD, + "sync": flagSync, + "hardirq": flagHardIRQ, + "mdflush": flagMDFlush, + "mount": flagMount, + "networkviewer": flagNetworkViewer, + "oomkill": flagOOMKill, + "process": flagProcess, + "shm": flagSHM, + "socket": flagSocket, + "dns": flagDNS, + "softirq": flagSoftIRQ, + "swap": flagSwap, + "vfs": flagVFS, + "nfs": flagNFS, + "xfs": flagXFS, + "zfs": flagZFS, + } + + for i := 0; i < len(args); i++ { + arg := args[i] + if !strings.HasPrefix(arg, "--") { + continue + } + + name := strings.TrimPrefix(arg, "--") + value := "" + if idx := strings.IndexByte(name, '='); idx >= 0 { + value = name[idx+1:] + name = name[:idx] + } + + if flagValue, ok := moduleOptions[name]; ok { + opts.flags |= flagValue + continue + } + + switch name { + case "help": + fmt.Fprint(os.Stdout, helpText(filepath.Base(os.Args[0]))) + opts.showHelp = true + return opts, 0 + case "unit-test": + opts.unitTest = true + case "all": + opts.flags |= flagAll + case "common": + opts.flags |= setCommonFlag() + case "load-binary": + value, i = optionValue(args, i, value) + opts.specificEBPF = value + opts.flags |= flagLoadBinary + case "dns-port": + value, i = optionValue(args, i, value) + opts.dnsPorts = parseDNSPortList(logger.writer, value, opts.dnsPorts) + case "netdata-path": + value, i = optionValue(args, i, value) + opts.netdataPath = value + case "log-path": + value, i = optionValue(args, i, value) + opts.logPath = value + file, err := os.OpenFile(value, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0o644) + if err != nil { + logger.writer = os.Stderr + fmt.Fprintf(logger.writer, "\"Error\": \"Cannot open %s\",\n", value) + } else { + logger.file = file + logger.writer = file + } + case "content": + opts.flags |= flagContent + case "iteration": + value, i = optionValue(args, i, value) + iteration, err := strconv.Atoi(value) + if err != nil || iteration < 1 { + fmt.Fprintf(logger.writer, "\"Error\" : \"Value given (%d) is smaller than the minimum, reseting to default 1.\",\n", iteration) + iteration = 1 + } + opts.iterations = iteration + case "pid": + value, i = optionValue(args, i, value) + pidLevel, err := strconv.Atoi(value) + if err != nil || pidLevel < 0 || pidLevel >= 4 { + fmt.Fprintf(logger.writer, "\"Error\" : \"Value given (%d) is not valid, reseting to default 0 (Real Parent).\",\n", pidLevel) + pidLevel = 0 + } + opts.mapLevel = pidLevel + } + } + + if !opts.unitTest && opts.flags&(flagAll&^flagContent) == 0 { + opts.flags |= setCommonFlag() + } + + if !opts.unitTest && kernelVersion < netdataEBPFKernel414 { + opts.flags &^= flagOOMKill + } + + return opts, 0 +} + +func optionValue(args []string, idx int, inline string) (string, int) { + if inline != "" { + return inline, idx + } + if idx+1 >= len(args) { + return "", idx + } + return args[idx+1], idx + 1 +} + +func parseDNSPortList(w io.Writer, input string, existing []uint16) []uint16 { + ports := make([]uint16, 0, len(existing)) + seen := map[uint16]bool{} + + for _, token := range strings.Split(input, ",") { + token = strings.TrimSpace(token) + if token == "" { + continue + } + + portValue, err := strconv.ParseUint(token, 10, 16) + if err != nil || portValue == 0 { + fmt.Fprintf(w, "\"Error\" : \"DNS port value (%s) is not valid.\",\n", token) + continue + } + + port := uint16(portValue) + if seen[port] { + continue + } + if len(ports) >= netdataDNSMaxPorts { + fmt.Fprintf(w, "\"Error\" : \"Maximum number of DNS ports (%d) reached.\",\n", netdataDNSMaxPorts) + break + } + + seen[port] = true + ports = append(ports, port) + } + + if len(ports) == 0 { + return []uint16{netdataDNSDefaultPort} + } + + return ports +} + +func resolveBinaryDir(netdataPath string) string { + if netdataPath == "" { + cwd, err := os.Getwd() + if err != nil { + return "." + } + + return cwd + } + + resolved, err := filepath.Abs(netdataPath) + if err == nil { + return resolved + } + + return netdataPath +} + +func candidateMatches(filename string, moduleName string, isReturn bool, version string, rhfVersion int) bool { + prefix := fmt.Sprintf("%cnetdata_ebpf_%s.", map[bool]rune{true: 'r', false: 'p'}[isReturn], moduleName) + if !strings.HasPrefix(filename, prefix) || !strings.HasSuffix(filename, ".o") { + return false + } + + rest := strings.TrimSuffix(strings.TrimPrefix(filename, prefix), ".o") + if !strings.HasPrefix(rest, version) { + return false + } + if len(rest) > len(version) && rest[len(version)] != '.' { + return false + } + + hasRHF := strings.Contains(rest, ".rhf") + if rhfVersion != -1 { + return hasRHF + } + + return !hasRHF +} + +func discoverCandidates(moduleName string, isReturn bool, version string, rhfVersion int, netdataPath string) []string { + path := resolveBinaryDir(netdataPath) + entries, err := os.ReadDir(path) + if err != nil { + return nil + } + + candidates := make([]string, 0, len(entries)) + for _, entry := range entries { + if entry.IsDir() { + continue + } + if !candidateMatches(entry.Name(), moduleName, isReturn, version, rhfVersion) { + continue + } + + candidates = append(candidates, filepath.Join(path, entry.Name())) + } + + sort.Strings(candidates) + return candidates +} + +func fallbackPerCPUMapSupport(_ int, kernelVersion int) bool { + return kernelVersion >= netdataMinimumEBPFKernel +} + +func detectSupportedMapTypes(rhfVersion int, kernelVersion int) map[uint32]bool { + supported := map[uint32]bool{ + bpfMapTypeHash: kernelVersion >= netdataMinimumEBPFKernel || rhfVersion > 0, + bpfMapTypeArray: kernelVersion >= netdataMinimumEBPFKernel || rhfVersion > 0, + bpfMapTypePerCPUHash: fallbackPerCPUMapSupport(rhfVersion, kernelVersion), + bpfMapTypePerCPUArray: fallbackPerCPUMapSupport(rhfVersion, kernelVersion), + } + + for _, mapType := range []uint32{bpfMapTypeHash, bpfMapTypeArray, bpfMapTypePerCPUHash, bpfMapTypePerCPUArray} { + if probe := probeMapTypeSupport(mapType); probe >= 0 { + supported[mapType] = probe > 0 + } + } + + return supported +} + +func mapTypeName(mapType uint32) string { + switch mapType { + case bpfMapTypeHash: + return "hash" + case bpfMapTypeArray: + return "array" + case bpfMapTypePerCPUHash: + return "percpu_hash" + case bpfMapTypePerCPUArray: + return "percpu_array" + default: + return fmt.Sprintf("type_%d", mapType) + } +} + +func writeSupportedMapTypes(w io.Writer, supported map[uint32]bool) { + names := make([]string, 0, 4) + for _, mapType := range []uint32{bpfMapTypeHash, bpfMapTypeArray, bpfMapTypePerCPUHash, bpfMapTypePerCPUArray} { + if supported[mapType] { + names = append(names, fmt.Sprintf("\"%s\"", mapTypeName(mapType))) + } + } + + fmt.Fprintf(w, "[%s]", strings.Join(names, ", ")) +} + +func writeObjectMapTypes(w io.Writer, obj *bpfObject) { + seen := map[uint32]bool{} + names := make([]string, 0, 4) + + if obj != nil { + for m := obj.firstMap(); m != nil; m = obj.nextMap(m) { + mapType := m.meta().Type + if seen[mapType] { + continue + } + + seen[mapType] = true + names = append(names, fmt.Sprintf("\"%s\"", mapTypeName(mapType))) + } + } + + fmt.Fprintf(w, " \"Map Types Used\" : [%s],\n", strings.Join(names, ", ")) +} + +func firstUnsupportedMapType(mapTypes []uint32, supported map[uint32]bool) (uint32, bool) { + for _, mapType := range mapTypes { + if allowed, ok := supported[mapType]; ok && !allowed { + return mapType, true + } + } + + return 0, false +} + +func candidateMapTypes(filename string) ([]uint32, int) { + obj, errCode := openBPFObject(filename) + if errCode != 0 { + return nil, errCode + } + defer obj.close() + + mapTypes := make([]uint32, 0, 8) + for m := obj.firstMap(); m != nil; m = obj.nextMap(m) { + mapTypes = append(mapTypes, m.meta().Type) + } + + return mapTypes, 0 +} + +func filterCompatibleCandidates(candidates []string, supported map[uint32]bool) ([]string, string, uint32) { + compatible := make([]string, 0, len(candidates)) + var incompatible string + var unsupportedType uint32 + + for _, candidate := range candidates { + mapTypes, errCode := candidateMapTypes(candidate) + if errCode != 0 { + compatible = append(compatible, candidate) + continue + } + + if mapType, ok := firstUnsupportedMapType(mapTypes, supported); ok { + if incompatible == "" { + incompatible = candidate + unsupportedType = mapType + } + continue + } + + compatible = append(compatible, candidate) + } + + return compatible, incompatible, unsupportedType +} + +func writeMapCompatibilityDebug(w io.Writer, unsupportedType uint32, supported map[uint32]bool) { + errCode := -int(syscall.EOPNOTSUPP) + fmt.Fprintf(w, + " \"Debug\" : {\n"+ + " \"Info\" : { \"Stage\" : \"map_compatibility\",\n"+ + " \"Error Code\" : %d,\n"+ + " \"Error Message\" : \"%s\",\n"+ + " \"Unsupported Map Type\" : \"%s\",\n"+ + " \"Supported Map Types\" : ", + errCode, describeError(errCode), mapTypeName(unsupportedType)) + writeSupportedMapTypes(w, supported) + fmt.Fprint(w, ",\n \"Programs\" : []\n }\n }\n") +} + +func fillNames() { + updateNames(dcOptionalNames) + updateNames(swapOptionalNames) + updateNames(zfsOptionalNames) +} + +func updateNames(names []specifyName) { + if len(names) == 0 { + return + } + + file, err := os.Open("/proc/kallsyms") + if err != nil { + return + } + defer file.Close() + + remaining := 0 + for i := range names { + if names[i].optional == "" { + remaining++ + } + } + if remaining == 0 { + return + } + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if len(line) <= 19 { + continue + } + + data := line[19:] + for i := range names { + if names[i].optional != "" { + continue + } + + candidates := []string{names[i].functionToAttach, names[i].fallbackFunction} + for _, candidate := range candidates { + if candidate == "" || !strings.HasPrefix(data, candidate) { + continue + } + + end := strings.IndexAny(data, " \n") + if end < 0 { + end = len(data) + } + + names[i].optional = data[:end] + remaining-- + if remaining == 0 { + return + } + break + } + } + } +} + +func runNetdataTests(w io.Writer, rhfVersion int, kernelVersion int, isReturn bool, opts options, nprocesses int) { + supportedMapTypes := detectSupportedMapTypes(rhfVersion, kernelVersion) + + for _, mod := range ebpfModules { + if opts.flags&mod.flags == 0 { + continue + } + + idx := selectIndex(mod.kernels, rhfVersion, kernelVersion) + version := selectKernelName(idx) + candidates := discoverCandidates(mod.name, isReturn, version, rhfVersion, opts.netdataPath) + compatible, incompatible, unsupportedType := filterCompatibleCandidates(candidates, supportedMapTypes) + + if len(compatible) == 0 { + if incompatible != "" { + startNetdataJSON(w, incompatible, isReturn) + writeMapCompatibilityDebug(w, unsupportedType, supportedMapTypes) + fmt.Fprintf(w, " },\n \"Status\" : \"%s\"\n},\n", "Fail") + continue + } + + compatible = []string{mountName(idx, mod.name, isReturn, rhfVersion, opts.netdataPath)} + } + + for _, filename := range compatible { + startNetdataJSON(w, filename, isReturn) + result := ebpfTester(w, filename, mod.updateNames, opts.flags&flagContent != 0, mod.ctrlTable, opts, nprocesses) + fmt.Fprintf(w, " },\n \"Status\" : \"%s\"\n},\n", result) + } + } +} + +func selectKernelName(selector uint32) string { + kernelNames := []string{"3.10", "4.14", "4.16", "4.18", "5.4", "5.10", "5.11", "5.14", "5.15", "5.16", "6.8"} + return kernelNames[selector] +} + +func selectMaxIndex(rhfVersion int, kernelVersion int) uint32 { + if rhfVersion > 0 { + switch { + case kernelVersion >= netdataEBPFKernel514: + return 7 + case kernelVersion >= netdataEBPFKernel54: + return 4 + case kernelVersion >= netdataMinimumEBPFKernel: + return 3 + } + } else { + switch { + case kernelVersion >= netdataEBPFKernel68: + return 10 + case kernelVersion >= netdataEBPFKernel516: + return 9 + case kernelVersion >= netdataEBPFKernel515: + return 8 + case kernelVersion >= netdataEBPFKernel511: + return 6 + case kernelVersion >= netdataEBPFKernel510: + return 5 + case kernelVersion >= netdataEBPFKernel417: + return 4 + case kernelVersion >= netdataEBPFKernel415: + return 2 + case kernelVersion >= netdataMinimumEBPFKernel: + return 1 + } + } + + return 0 +} + +func selectIndex(kernels uint32, rhfVersion int, kernelVersion int) uint32 { + start := selectMaxIndex(rhfVersion, kernelVersion) + if rhfVersion == -1 { + kernels &^= netdataV514 + } + + for idx := start; idx > 0; idx-- { + if kernels&(1< 0 { + writeFailureDebug(w, obj, "attach_programs", summary.lastError, socketFilterDetected, total, summary) + } + + if maps { + if ctrl != "" { + fillCtrl(obj, ctrl, opts.mapLevel, nprocesses) + } + testMaps(w, obj, ctrl, opts.iterations, nprocesses) + } + + for _, link := range summary.links { + link.destroy() + } + + if summary.fail == 0 { + return success + } + + return failure +} + +func attachPrograms(obj *bpfObject, names *[]specifyName) attachSummary { + var summary attachSummary + + for prog := obj.firstProgram(); prog != nil; prog = obj.nextProgram(prog) { + var ( + link *bpfLink + err int + ) + + override := findOptionalName(names, prog.name()) + if override != nil && prog.progType() == bpfProgTypeKprobe { + target := override.optional + if target == "" && override.required { + target = override.functionToAttach + } + if target == "" { + summary.skipped++ + continue + } + link, err = prog.attachKprobe(override.retprobe, target) + } else { + link, err = prog.attach() + } + + if err != 0 { + summary.lastError = err + summary.failedProgramName = prog.name() + summary.failedProgramType = prog.progType() + summary.fail++ + continue + } + + summary.links = append(summary.links, link) + summary.success++ + } + + return summary +} + +func findOptionalName(names *[]specifyName, programName string) *specifyName { + if names == nil { + return nil + } + + for i := range *names { + if (*names)[i].programName == programName { + return &(*names)[i] + } + } + + return nil +} + +func writeFailureDebug(w io.Writer, obj *bpfObject, stage string, err int, socketFilterDetected bool, total int, summary attachSummary) { + fmt.Fprintf(w, + " \"Debug\" : {\n"+ + " \"Info\" : { \"Stage\" : \"%s\",\n"+ + " \"Error Code\" : %d,\n"+ + " \"Error Message\" : \"%s\",\n"+ + " \"Socket Filter Detected\" : %d,\n"+ + " \"Program Count\" : %d,\n"+ + " \"Attach Success\" : %d,\n"+ + " \"Attach Fail\" : %d", + stage, err, describeError(err), boolToInt(socketFilterDetected), total, summary.success, summary.fail) + + if summary.failedProgramName != "" { + fmt.Fprintf(w, + ",\n"+ + " \"Failed Program\" : \"%s\",\n"+ + " \"Failed Program Type\" : %d", + summary.failedProgramName, summary.failedProgramType) + } + + fmt.Fprint(w, ",\n \"Programs\" : ") + writeProgramInventory(w, obj) + fmt.Fprint(w, "\n }\n }\n") +} + +func writeProgramInventory(w io.Writer, obj *bpfObject) { + fmt.Fprint(w, "[") + first := true + if obj != nil { + for prog := obj.firstProgram(); prog != nil; prog = obj.nextProgram(prog) { + if !first { + fmt.Fprint(w, ", ") + } + fmt.Fprintf(w, "{ \"Name\" : \"%s\", \"Type\" : %d }", prog.name(), prog.progType()) + first = false + } + } + fmt.Fprint(w, "]") +} + +func isPerCPUMapType(mapType uint32) bool { + return mapType == bpfMapTypePerCPUArray || mapType == bpfMapTypePerCPUHash +} + +func roundUpSize(value int, align int) int { + return ((value + align - 1) / align) * align +} + +func mapValueStride(meta mapMeta) int { + if !isPerCPUMapType(meta.Type) { + return int(meta.ValueSize) + } + + return roundUpSize(int(meta.ValueSize), 8) +} + +func mapValueLength(meta mapMeta, nprocesses int) int { + if !isPerCPUMapType(meta.Type) { + return int(meta.ValueSize) + } + + if nprocesses < 1 { + nprocesses = 1 + } + + return mapValueStride(meta) * nprocesses +} + +func controllerEntryLimit(meta mapMeta) int { + limit := netdataControllerEnd + + if meta.MaxEntries > 0 && int(meta.MaxEntries) < limit { + limit = int(meta.MaxEntries) + } + + return limit +} + +func fillScalarValue(dst []byte, valueSize uint32, value uint64) { + switch { + case valueSize >= 8 && len(dst) >= 8: + binary.LittleEndian.PutUint64(dst, value) + case valueSize >= 4 && len(dst) >= 4: + binary.LittleEndian.PutUint32(dst, uint32(value)) + } +} + +func allocateTableData(meta mapMeta, nprocesses int) *tableData { + valueLength := mapValueLength(meta, nprocesses) + return &tableData{ + key: make([]byte, meta.KeySize), + nextKey: make([]byte, meta.KeySize), + value: make([]byte, valueLength), + defValue: make([]byte, valueLength), + keyLength: int(meta.KeySize), + valueLength: valueLength, + } +} + +func readGenericTable(values *tableData, fd int) { + // Passing a nil key to bpf_map_get_next_key retrieves the first entry, + // which correctly includes key 0 in array-type maps. The previous pattern + // starting from a zero-initialized key skipped key 0 in arrays and + // double-counted the last entry in every map type. + if bpfMapGetNextKey(fd, nil, values.nextKey) != 0 { + return + } + + for { + for i := range values.value { + values.value[i] = 0 + } + if bpfMapLookupElem(fd, values.nextKey, values.value) == 0 { + if bytes.Equal(values.value, values.defValue) { + values.zero++ + } else { + values.filled++ + } + } + copy(values.key, values.nextKey) + if bpfMapGetNextKey(fd, values.key, values.nextKey) != 0 { + break + } + } +} + +func writeCommonJSONVector(w io.Writer, values *tableData, fd int, iterations int) { + for i := 0; i < iterations; i++ { + time.Sleep(5 * time.Second) + readGenericTable(values, fd) + if i > 0 { + fmt.Fprint(w, ",\n") + } + fmt.Fprintf(w, " { \"Iteration\" : %d, \"Total\" : %d, \"Filled\" : %d, \"Zero\" : %d }", + i, values.filled+values.zero, values.filled, values.zero) + } + fmt.Fprint(w, "\n") +} + +func controllerJSON(w io.Writer, fd int, meta mapMeta, nprocesses int) { + var filled, zero uint32 + key := make([]byte, meta.KeySize) + value := make([]byte, mapValueLength(meta, nprocesses)) + for idx := 0; idx < controllerEntryLimit(meta); idx++ { + putUint32(key, uint32(idx)) + if bpfMapLookupElem(fd, key, value) != 0 { + zero++ + } else { + filled++ + } + } + fmt.Fprintf(w, " { \"Iteration\" : 1, \"Total\" : %d, \"Filled\" : %d, \"Zero\" : %d }\n", + filled+zero, filled, zero) +} + +func testMaps(w io.Writer, obj *bpfObject, ctrl string, iterations int, nprocesses int) { + tables := 0 + for m := obj.firstMap(); m != nil; m = obj.nextMap(m) { + meta := m.meta() + values := allocateTableData(meta, nprocesses) + fmt.Fprintf(w, + " \"%s\" : {\n \"Info\" : { \"Length\" : { \"Key\" : %d, \"Value\" : %d},\n"+ + " \"Type\" : %d,\n"+ + " \"FD\" : %d,\n"+ + " \"Data\" : [\n", + meta.Name, meta.KeySize, meta.ValueSize, meta.Type, meta.FD) + + if ctrl == "" || ctrl != meta.Name { + writeCommonJSONVector(w, values, meta.FD, iterations) + } else { + controllerJSON(w, meta.FD, meta, nprocesses) + } + + fmt.Fprint(w, " ]\n }\n },\n") + tables++ + } + + if tables > 0 { + fmt.Fprintf(w, " \"Total tables\" : %d\n", tables) + } +} + +func fillCtrl(obj *bpfObject, ctrl string, mapLevel int, nprocesses int) { + m := obj.findMapByName(ctrl) + if m == nil { + return + } + + meta := m.meta() + values := []uint64{1, uint64(mapLevel), 0, 0, 0, 0} + key := make([]byte, meta.KeySize) + value := make([]byte, mapValueLength(meta, nprocesses)) + stride := mapValueStride(meta) + cpuCount := 1 + + if stride > 0 { + cpuCount = len(value) / stride + } + + for i := uint32(0); i < meta.MaxEntries && int(i) < len(values); i++ { + for j := range value { + value[j] = 0 + } + + for cpu := 0; cpu < cpuCount; cpu++ { + offset := cpu * stride + fillScalarValue(value[offset:], meta.ValueSize, values[i]) + } + + putUint32(key, i) + bpfMapUpdateElem(meta.FD, key, value, 0) + } +} + +func putUint16(dst []byte, value uint16) { + if len(dst) >= 2 { + binary.LittleEndian.PutUint16(dst, value) + } +} + +func putUint32(dst []byte, value uint32) { + if len(dst) >= 4 { + binary.LittleEndian.PutUint32(dst, value) + } +} + +func writeErrorExit(w io.Writer, msg string) { + fmt.Fprintf(w, "\"Error\" : \"%s\",\n", msg) +} + +func describeError(err int) string { + if err == 0 { + return "No error information" + } + + if err < 0 { + err = -err + } + + return syscall.Errno(err).Error() +} + +func boolToInt(v bool) int { + if v { + return 1 + } + return 0 +} + +func dnsFormatIP(family uint8, raw [16]byte) string { + size := dnsIPSize(family) + ip := net.IP(raw[:size]) + if family == syscall.AF_INET6 { + ip = net.IP(raw[:16]) + } + if ip == nil { + return "invalid" + } + + return ip.String() +} + +const gotestsModuleName = "github.com/netdata/kernel-collector/gotests" + +func runUnitTests() int { + workDir, err := locateUnitTestDir() + if err != nil { + fmt.Fprintf(os.Stderr, "cannot locate gotests module directory: %v\n", err) + return 2 + } + + goBinary, err := exec.LookPath("go") + if err != nil { + fmt.Fprintf(os.Stderr, "cannot find go tool: %v\n", err) + return 2 + } + + cmd := exec.Command(goBinary, "test", "./...") + cmd.Dir = workDir + cmd.Env = unitTestEnv(os.Environ()) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return exitErr.ExitCode() + } + + fmt.Fprintf(os.Stderr, "cannot run go test: %v\n", err) + return 2 + } + + return 0 +} + +func locateUnitTestDir() (string, error) { + cwd, _ := os.Getwd() + executable, _ := os.Executable() + return resolveUnitTestDir(cwd, executable) +} + +func resolveUnitTestDir(cwd string, executable string) (string, error) { + candidates := []string{cwd} + if cwd != "" { + candidates = append(candidates, filepath.Join(cwd, "gotests")) + } + + if executable != "" { + exeDir := filepath.Dir(executable) + candidates = append(candidates, exeDir, filepath.Join(exeDir, "gotests")) + } + + seen := map[string]struct{}{} + for _, candidate := range candidates { + if candidate == "" { + continue + } + + candidate = filepath.Clean(candidate) + if _, ok := seen[candidate]; ok { + continue + } + seen[candidate] = struct{}{} + + if dir, ok := findGoModuleDir(candidate, gotestsModuleName); ok { + return dir, nil + } + } + + return "", fmt.Errorf("module %s not found from cwd=%q executable=%q", gotestsModuleName, cwd, executable) +} + +func findGoModuleDir(start string, moduleName string) (string, bool) { + for current := filepath.Clean(start); ; { + if isGoModuleDir(current, moduleName) { + return current, true + } + + parent := filepath.Dir(current) + if parent == current { + return "", false + } + + current = parent + } +} + +func isGoModuleDir(dir string, moduleName string) bool { + data, err := os.ReadFile(filepath.Join(dir, "go.mod")) + if err != nil { + return false + } + + moduleLine := "module " + moduleName + for _, line := range strings.Split(string(data), "\n") { + if strings.TrimSpace(line) == moduleLine { + return true + } + } + + return false +} + +func unitTestEnv(env []string) []string { + for _, entry := range env { + if strings.HasPrefix(entry, "GOCACHE=") { + return env + } + } + + cloned := append([]string{}, env...) + cloned = append(cloned, "GOCACHE="+filepath.Join(os.TempDir(), "kernel-collector-gocache")) + return cloned +} diff --git a/gotests/main_test.go b/gotests/main_test.go new file mode 100644 index 00000000..b87455fd --- /dev/null +++ b/gotests/main_test.go @@ -0,0 +1,350 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + "testing" +) + +func TestParseDNSPortList(t *testing.T) { + t.Run("returns default when no valid ports exist", func(t *testing.T) { + var out bytes.Buffer + + ports := parseDNSPortList(&out, "0, abc, ,", nil) + if len(ports) != 1 || ports[0] != netdataDNSDefaultPort { + t.Fatalf("unexpected ports: %v", ports) + } + + log := out.String() + for _, want := range []string{ + `DNS port value (0) is not valid`, + `DNS port value (abc) is not valid`, + } { + if !strings.Contains(log, want) { + t.Fatalf("missing %q in output: %s", want, log) + } + } + }) + + t.Run("deduplicates entries and enforces maximum", func(t *testing.T) { + var out bytes.Buffer + var items []string + for i := 1; i <= netdataDNSMaxPorts+2; i++ { + items = append(items, strconv.Itoa(i)) + } + items = append(items, "2") + + ports := parseDNSPortList(&out, strings.Join(items, ","), nil) + if len(ports) != netdataDNSMaxPorts { + t.Fatalf("unexpected number of ports: got %d want %d", len(ports), netdataDNSMaxPorts) + } + if ports[0] != 1 || ports[len(ports)-1] != netdataDNSMaxPorts { + t.Fatalf("unexpected port ordering: %v", ports) + } + if !strings.Contains(out.String(), `Maximum number of DNS ports`) { + t.Fatalf("missing max-ports warning: %s", out.String()) + } + }) +} + +func TestKernelSelectionHelpers(t *testing.T) { + parseCases := []struct { + name string + release string + want int + }{ + {name: "plain release", release: "6.8.12", want: 6*65536 + 8*256 + 12}, + {name: "debian release", release: "6.12.74+deb13-amd64", want: 6*65536 + 12*256 + 74}, + {name: "debian plus suffix chain", release: "6.12.74+deb13+1-amd64", want: 6*65536 + 12*256 + 74}, + {name: "dash suffix", release: "5.15.0-101-generic", want: 5*65536 + 15*256}, + {name: "clamps patch", release: "6.12.999-custom", want: 6*65536 + 12*256 + 255}, + {name: "invalid release", release: "not-a-kernel", want: -1}, + } + + for _, tc := range parseCases { + t.Run("parse "+tc.name, func(t *testing.T) { + if got := parseKernelRelease(tc.release); got != tc.want { + t.Fatalf("unexpected parsed kernel release for %q: got %d want %d", tc.release, got, tc.want) + } + }) + } + + rhCases := []struct { + name string + release string + want int + }{ + {name: "rh release", release: "Red Hat Enterprise Linux release 9.4 (Plow)\n", want: 9*256 + 4}, + {name: "centos release", release: "CentOS Linux release 7.9.2009 (Core)\n", want: 7*256 + 9}, + {name: "alma release", release: "AlmaLinux release 9.5 (Teal Serval)\n", want: 9*256 + 5}, + {name: "invalid release", release: "Debian GNU/Linux\n", want: -1}, + } + + for _, tc := range rhCases { + t.Run("rh "+tc.name, func(t *testing.T) { + if got := parseRedHatRelease(tc.release); got != tc.want { + t.Fatalf("unexpected parsed redhat release for %q: got %d want %d", tc.release, got, tc.want) + } + }) + } + + leadingCases := []struct { + input string + want int + }{ + {input: "74+deb13+1", want: 74}, + {input: "9 ", want: 9}, + {input: "+12beta", want: 12}, + {input: "-5rc1", want: -5}, + } + + for _, tc := range leadingCases { + t.Run("leading "+tc.input, func(t *testing.T) { + if got := parseLeadingLong(tc.input); got != tc.want { + t.Fatalf("unexpected parsed leading integer for %q: got %d want %d", tc.input, got, tc.want) + } + }) + } + + maxCases := []struct { + name string + rhfVersion int + kernelVersion int + want uint32 + }{ + {name: "rhf 5.14", rhfVersion: 1, kernelVersion: netdataEBPFKernel514, want: 7}, + {name: "rhf 5.4", rhfVersion: 1, kernelVersion: netdataEBPFKernel54, want: 4}, + {name: "generic 6.8", rhfVersion: 0, kernelVersion: netdataEBPFKernel68, want: 10}, + {name: "generic 4.15", rhfVersion: 0, kernelVersion: netdataEBPFKernel415, want: 2}, + {name: "too old", rhfVersion: 0, kernelVersion: netdataMinimumEBPFKernel - 1, want: 0}, + } + + for _, tc := range maxCases { + t.Run(tc.name, func(t *testing.T) { + if got := selectMaxIndex(tc.rhfVersion, tc.kernelVersion); got != tc.want { + t.Fatalf("unexpected max index: got %d want %d", got, tc.want) + } + }) + } + + if got := selectKernelName(10); got != "6.8" { + t.Fatalf("unexpected kernel name: %q", got) + } + + if got := selectIndex(netdataV514, -1, netdataEBPFKernel514); got != 0 { + t.Fatalf("expected 5.14-only selector to be masked for non-RHF, got %d", got) + } + + if got := selectIndex(netdataV514|netdataV510, -1, netdataEBPFKernel514); got != 5 { + t.Fatalf("expected fallback to 5.10 selector, got %d", got) + } +} + +func TestCandidateSelectionHelpers(t *testing.T) { + t.Run("matches expected module, version, family, and probe type", func(t *testing.T) { + cases := []struct { + name string + filename string + module string + isReturn bool + version string + rhf int + wantMatch bool + }{ + {name: "exact rhf entry", filename: "pnetdata_ebpf_swap.3.10.rhf.o", module: "swap", version: "3.10", rhf: 1, wantMatch: true}, + {name: "rhf variant", filename: "pnetdata_ebpf_swap.3.10.variant.rhf.o", module: "swap", version: "3.10", rhf: 1, wantMatch: true}, + {name: "wrong probe type", filename: "rnetdata_ebpf_swap.3.10.rhf.o", module: "swap", version: "3.10", rhf: 1, wantMatch: false}, + {name: "wrong version", filename: "pnetdata_ebpf_swap.4.14.rhf.o", module: "swap", version: "3.10", rhf: 1, wantMatch: false}, + {name: "wrong family", filename: "pnetdata_ebpf_swap.3.10.o", module: "swap", version: "3.10", rhf: 1, wantMatch: false}, + {name: "non-rhf exact", filename: "pnetdata_ebpf_swap.5.14.o", module: "swap", version: "5.14", rhf: -1, wantMatch: true}, + {name: "non-rhf rejects rhf", filename: "pnetdata_ebpf_swap.5.14.rhf.o", module: "swap", version: "5.14", rhf: -1, wantMatch: false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := candidateMatches(tc.filename, tc.module, tc.isReturn, tc.version, tc.rhf); got != tc.wantMatch { + t.Fatalf("unexpected match result for %q: got %v want %v", tc.filename, got, tc.wantMatch) + } + }) + } + }) + + t.Run("discovers and sorts matching candidates", func(t *testing.T) { + dir := t.TempDir() + files := []string{ + "pnetdata_ebpf_swap.3.10.variant.rhf.o", + "pnetdata_ebpf_swap.3.10.rhf.o", + "pnetdata_ebpf_swap.3.10.o", + "rnetdata_ebpf_swap.3.10.rhf.o", + "pnetdata_ebpf_process.3.10.rhf.o", + } + for _, name := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte("x"), 0o644); err != nil { + t.Fatalf("cannot create %s: %v", name, err) + } + } + + got := discoverCandidates("swap", false, "3.10", 1, dir) + want := []string{ + filepath.Join(dir, "pnetdata_ebpf_swap.3.10.rhf.o"), + filepath.Join(dir, "pnetdata_ebpf_swap.3.10.variant.rhf.o"), + } + + if len(got) != len(want) { + t.Fatalf("unexpected candidate count: got %v want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("unexpected candidate ordering: got %v want %v", got, want) + } + } + }) + + t.Run("returns first unsupported map type", func(t *testing.T) { + supported := map[uint32]bool{ + bpfMapTypeHash: true, + bpfMapTypeArray: true, + bpfMapTypePerCPUHash: true, + bpfMapTypePerCPUArray: false, + } + + mapType, ok := firstUnsupportedMapType([]uint32{bpfMapTypeHash, bpfMapTypePerCPUArray}, supported) + if !ok { + t.Fatal("expected unsupported map type") + } + if mapType != bpfMapTypePerCPUArray { + t.Fatalf("unexpected unsupported map type: got %d want %d", mapType, bpfMapTypePerCPUArray) + } + }) + + t.Run("falls back to non-percpu support on rhf 3.10", func(t *testing.T) { + if fallbackPerCPUMapSupport(1, netdataMinimumEBPFKernel-1) { + t.Fatal("expected percpu fallback support to be disabled for old RH kernels") + } + if !fallbackPerCPUMapSupport(-1, netdataMinimumEBPFKernel) { + t.Fatal("expected percpu fallback support for supported generic kernels") + } + }) +} + +func TestBinaryAndIPHelpers(t *testing.T) { + buf16 := []byte{0, 0} + putUint16(buf16, 0x1234) + if buf16[0] != 0x34 || buf16[1] != 0x12 { + t.Fatalf("unexpected uint16 encoding: %v", buf16) + } + + buf32 := []byte{0, 0, 0, 0} + putUint32(buf32, 0x12345678) + if want := []byte{0x78, 0x56, 0x34, 0x12}; !bytes.Equal(buf32, want) { + t.Fatalf("unexpected uint32 encoding: %v", buf32) + } + + short := []byte{0} + putUint16(short, 0xFFFF) + if short[0] != 0 { + t.Fatalf("short buffer should remain unchanged: %v", short) + } + + if got := boolToInt(true); got != 1 { + t.Fatalf("unexpected boolToInt(true): %d", got) + } + if got := boolToInt(false); got != 0 { + t.Fatalf("unexpected boolToInt(false): %d", got) + } + + if got := dnsFormatIP(syscall.AF_INET, [16]byte{192, 0, 2, 10}); got != "192.0.2.10" { + t.Fatalf("unexpected IPv4 format: %q", got) + } + + ipv6 := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + if got := dnsFormatIP(syscall.AF_INET6, ipv6); got != "2001:db8::1" { + t.Fatalf("unexpected IPv6 format: %q", got) + } +} + +func TestParseArgumentsUnitTest(t *testing.T) { + var log bytes.Buffer + opts, code := parseArguments([]string{"--unit-test"}, netdataEBPFKernel68, &logState{writer: &log}) + if code != 0 { + t.Fatalf("unexpected parse code: %d", code) + } + if !opts.unitTest { + t.Fatal("expected unitTest flag to be enabled") + } + if opts.flags != 0 { + t.Fatalf("expected no runtime flags when unit-test is selected, got %#x", opts.flags) + } + if log.Len() != 0 { + t.Fatalf("expected no parse output, got %q", log.String()) + } +} + +func TestResolveUnitTestDir(t *testing.T) { + t.Run("finds gotests module from repo root cwd", func(t *testing.T) { + root := t.TempDir() + gotestsDir := filepath.Join(root, "gotests") + if err := os.MkdirAll(gotestsDir, 0o755); err != nil { + t.Fatalf("cannot create gotests dir: %v", err) + } + if err := os.WriteFile(filepath.Join(gotestsDir, "go.mod"), []byte("module "+gotestsModuleName+"\n"), 0o644); err != nil { + t.Fatalf("cannot write go.mod: %v", err) + } + + dir, err := resolveUnitTestDir(root, filepath.Join(t.TempDir(), "go_tester")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dir != gotestsDir { + t.Fatalf("unexpected dir: got %q want %q", dir, gotestsDir) + } + }) + + t.Run("finds gotests module from executable directory", func(t *testing.T) { + root := t.TempDir() + gotestsDir := filepath.Join(root, "bin", "gotests") + if err := os.MkdirAll(gotestsDir, 0o755); err != nil { + t.Fatalf("cannot create gotests dir: %v", err) + } + if err := os.WriteFile(filepath.Join(gotestsDir, "go.mod"), []byte("module "+gotestsModuleName+"\n"), 0o644); err != nil { + t.Fatalf("cannot write go.mod: %v", err) + } + + dir, err := resolveUnitTestDir("", filepath.Join(gotestsDir, "go_tester")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dir != gotestsDir { + t.Fatalf("unexpected dir: got %q want %q", dir, gotestsDir) + } + }) + + t.Run("returns error when module cannot be found", func(t *testing.T) { + if _, err := resolveUnitTestDir(t.TempDir(), filepath.Join(t.TempDir(), "go_tester")); err == nil { + t.Fatal("expected resolveUnitTestDir to fail") + } + }) +} + +func TestUnitTestEnv(t *testing.T) { + t.Run("preserves explicit gocache", func(t *testing.T) { + env := unitTestEnv([]string{"PATH=/bin", "GOCACHE=/custom/cache"}) + if len(env) != 2 || env[1] != "GOCACHE=/custom/cache" { + t.Fatalf("unexpected env: %v", env) + } + }) + + t.Run("adds temp gocache when missing", func(t *testing.T) { + env := unitTestEnv([]string{"PATH=/bin"}) + if len(env) != 2 { + t.Fatalf("unexpected env length: %d", len(env)) + } + if env[1] != "GOCACHE="+filepath.Join(os.TempDir(), "kernel-collector-gocache") { + t.Fatalf("unexpected gocache entry: %q", env[1]) + } + }) +} diff --git a/includes/netdata_disk.h b/includes/netdata_disk.h index 8b9d9d19..01bece1e 100644 --- a/includes/netdata_disk.h +++ b/includes/netdata_disk.h @@ -50,6 +50,7 @@ struct netdata_block_rq_complete { typedef struct netdata_disk_key { dev_t dev; + __u32 pad; // Verifier-visible padding must be initialized. sector_t sector; } netdata_disk_key_t; @@ -59,4 +60,3 @@ typedef struct block_key { } block_key_t; #endif /* _NETDATA_DISK_H_ */ - diff --git a/kernel/DEVELOPER.md b/kernel/DEVELOPER.md index 67d96098..2943a080 100644 --- a/kernel/DEVELOPER.md +++ b/kernel/DEVELOPER.md @@ -4,8 +4,8 @@ This MD file was added to help developers starting with eBPF development. In this repo we are using the same [pattern](https://elixir.bootlin.com/linux/v4.20.17/source/samples/bpf) that was used before [BTF](https://docs.kernel.org/bpf/btf.html) was released. All source files ending with `_kern.c` are eBPF codes -loaded inside `kernel ring`. We do no have a `_user.c` for each one of `_kern.c` files, because we have a common loader for -all (`tester_user.c`). +loaded inside `kernel ring`. We do no have a `_user.c` for each one of `_kern.c` files, because we have common loaders for +all (`tests/tester_user.c` for the legacy C tester and `gotests/` for the Go tester). ## Libbpf @@ -93,16 +93,21 @@ make dev ## Tests -The tester is not compiled by default. To compile it and run all common tests run: +The testers are not compiled by default. To compile them and run all common tests run: ```sh make dev -for j in `seq 0 2`; do for i in `ls *.o`; do ./kernel/legacy_test --content --pid $j --load-binary $i --log-path $i_pid$i.txt; 2>> err >> out; done; done +make tester +for j in `seq 0 2`; do for i in `ls *.o`; do ./tests/legacy_test --content --pid $j --load-binary $i --log-path $i_pid$i.txt; 2>> err >> out; done; done +for j in `seq 0 2`; do for i in `ls *.o`; do ./gotests/go_tester --content --pid $j --load-binary $i --log-path $i_pid$i.go.txt; 2>> err.go >> out.go; done; done ``` +The `legacy_test` and `go_tester` binaries are compiled with debug symbols to +make local debugging easier. + You can take a look in all options available for tests running: ```sh -./kernel/legacy_test --help +./tests/legacy_test --help +./gotests/go_tester --help ``` - diff --git a/kernel/Makefile b/kernel/Makefile index 1eef800b..a17e51b8 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -153,7 +153,7 @@ $(NETDATA_APPS): %: %_kern.o ${NETDATA_ALL_APPS}: %: %_kern.o tester: libbpf - $(CC) -I../.local_libbpf -I$(LIBBPF)/src -I$(LIBBPF)/include -I$(LIBBPF)/include/uapi -L../.local_libbpf -o legacy_test tester_user.c tester_dns.c -lbpf -lz -lelf + cd ../tests && $(MAKE) tester clean: cd $(LIBBPF)/src && make clean diff --git a/kernel/disk_kern.c b/kernel/disk_kern.c index d84764dd..c7811f2a 100644 --- a/kernel/disk_kern.c +++ b/kernel/disk_kern.c @@ -32,6 +32,7 @@ static __always_inline netdata_disk_key_t netdata_disk_key(void *ptr) struct netdata_block_rq_issue *issue = ptr; netdata_disk_key_t key = { .dev = issue->dev, + .pad = 0, .sector = (issue->sector < 0) ? 0 : issue->sector }; return key; diff --git a/kernel/vfs_kern.c b/kernel/vfs_kern.c index 3d621b19..b375752e 100644 --- a/kernel/vfs_kern.c +++ b/kernel/vfs_kern.c @@ -48,40 +48,24 @@ static __always_inline void netdata_init_vfs_data(struct netdata_vfs_stat_t *dat #endif } -static __always_inline void netdata_update_vfs_entry(struct netdata_vfs_stat_t *fill, - struct netdata_vfs_stat_t *data, - __u32 *key, - __u32 tgid, - __u32 *call_field, - __u32 *err_field, - __u64 *byte_field, - __u64 tot, - int has_error, - int is_error, - int has_bytes) +static __always_inline void netdata_store_vfs_entry(struct netdata_vfs_stat_t *data, + __u32 *key, + __u32 tgid) { - if (fill) { - libnetdata_update_u32(call_field, 1); - if (has_error && is_error) - libnetdata_update_u32(err_field, 1); - if (has_bytes) - libnetdata_update_u64(byte_field, tot); - } else { - netdata_init_vfs_data(data, tgid); - data->write_call = 0; - data->writev_call = 0; - data->read_call = 0; - data->readv_call = 0; - data->unlink_call = 0; - data->fsync_call = 0; - data->open_call = 0; - data->create_call = 0; - libnetdata_update_u32(call_field, 1); - netdata_update_vfs_err(err_field, is_error); - netdata_update_vfs_bytes(byte_field, tot, has_bytes); - bpf_map_update_elem(&tbl_vfs_pid, key, data, BPF_ANY); - libnetdata_update_global(&vfs_ctrl, NETDATA_CONTROLLER_PID_TABLE_ADD, 1); - } + netdata_init_vfs_data(data, tgid); + bpf_map_update_elem(&tbl_vfs_pid, key, data, BPF_ANY); + libnetdata_update_global(&vfs_ctrl, NETDATA_CONTROLLER_PID_TABLE_ADD, 1); +} + +/* + * Keep the VFS PID-table lookup local and typed so older 5.14 verifiers + * retain the map-value pointer across the update path. + */ +static __always_inline struct netdata_vfs_stat_t *netdata_get_vfs_structure(__u32 *key, __u32 *tgid) +{ + *key = netdata_get_pid(&vfs_ctrl, tgid); + + return bpf_map_lookup_elem(&tbl_vfs_pid, key); } #if NETDATASEL < 2 @@ -112,10 +96,17 @@ int netdata_sys_write(struct pt_regs* ctx) if (!monitor_apps(&vfs_ctrl)) return 0; - struct netdata_vfs_stat_t *fill = netdata_get_pid_structure(&key, &tgid, &vfs_ctrl, &tbl_vfs_pid); - netdata_update_vfs_entry(fill, &data, &key, tgid, - &fill->write_call, &fill->write_err, &fill->write_bytes, tot, - 1, ret < 0, 1); + struct netdata_vfs_stat_t *fill = netdata_get_vfs_structure(&key, &tgid); + if (fill) { + libnetdata_update_u32(&fill->write_call, 1); + netdata_update_vfs_err(&fill->write_err, ret < 0); + netdata_update_vfs_bytes(&fill->write_bytes, tot, 1); + } else { + libnetdata_update_u32(&data.write_call, 1); + netdata_update_vfs_err(&data.write_err, ret < 0); + netdata_update_vfs_bytes(&data.write_bytes, tot, 1); + netdata_store_vfs_entry(&data, &key, tgid); + } return 0; } @@ -147,10 +138,17 @@ int netdata_sys_writev(struct pt_regs* ctx) if (!monitor_apps(&vfs_ctrl)) return 0; - struct netdata_vfs_stat_t *fill = netdata_get_pid_structure(&key, &tgid, &vfs_ctrl, &tbl_vfs_pid); - netdata_update_vfs_entry(fill, &data, &key, tgid, - &fill->writev_call, &fill->writev_err, &fill->writev_bytes, tot, - 1, ret < 0, 1); + struct netdata_vfs_stat_t *fill = netdata_get_vfs_structure(&key, &tgid); + if (fill) { + libnetdata_update_u32(&fill->writev_call, 1); + netdata_update_vfs_err(&fill->writev_err, ret < 0); + netdata_update_vfs_bytes(&fill->writev_bytes, tot, 1); + } else { + libnetdata_update_u32(&data.writev_call, 1); + netdata_update_vfs_err(&data.writev_err, ret < 0); + netdata_update_vfs_bytes(&data.writev_bytes, tot, 1); + netdata_store_vfs_entry(&data, &key, tgid); + } return 0; } @@ -182,10 +180,17 @@ int netdata_sys_read(struct pt_regs* ctx) if (!monitor_apps(&vfs_ctrl)) return 0; - struct netdata_vfs_stat_t *fill = netdata_get_pid_structure(&key, &tgid, &vfs_ctrl, &tbl_vfs_pid); - netdata_update_vfs_entry(fill, &data, &key, tgid, - &fill->read_call, &fill->read_err, &fill->read_bytes, tot, - 1, ret < 0, 1); + struct netdata_vfs_stat_t *fill = netdata_get_vfs_structure(&key, &tgid); + if (fill) { + libnetdata_update_u32(&fill->read_call, 1); + netdata_update_vfs_err(&fill->read_err, ret < 0); + netdata_update_vfs_bytes(&fill->read_bytes, tot, 1); + } else { + libnetdata_update_u32(&data.read_call, 1); + netdata_update_vfs_err(&data.read_err, ret < 0); + netdata_update_vfs_bytes(&data.read_bytes, tot, 1); + netdata_store_vfs_entry(&data, &key, tgid); + } return 0; } @@ -217,10 +222,17 @@ int netdata_sys_readv(struct pt_regs* ctx) if (!monitor_apps(&vfs_ctrl)) return 0; - struct netdata_vfs_stat_t *fill = netdata_get_pid_structure(&key, &tgid, &vfs_ctrl, &tbl_vfs_pid); - netdata_update_vfs_entry(fill, &data, &key, tgid, - &fill->readv_call, &fill->readv_err, &fill->readv_bytes, tot, - 1, ret < 0, 1); + struct netdata_vfs_stat_t *fill = netdata_get_vfs_structure(&key, &tgid); + if (fill) { + libnetdata_update_u32(&fill->readv_call, 1); + netdata_update_vfs_err(&fill->readv_err, ret < 0); + netdata_update_vfs_bytes(&fill->readv_bytes, tot, 1); + } else { + libnetdata_update_u32(&data.readv_call, 1); + netdata_update_vfs_err(&data.readv_err, ret < 0); + netdata_update_vfs_bytes(&data.readv_bytes, tot, 1); + netdata_store_vfs_entry(&data, &key, tgid); + } return 0; } @@ -248,10 +260,15 @@ int netdata_sys_unlink(struct pt_regs* ctx) if (!monitor_apps(&vfs_ctrl)) return 0; - struct netdata_vfs_stat_t *fill = netdata_get_pid_structure(&key, &tgid, &vfs_ctrl, &tbl_vfs_pid); - netdata_update_vfs_entry(fill, &data, &key, tgid, - &fill->unlink_call, &fill->unlink_err, NULL, 0, - 1, ret < 0, 0); + struct netdata_vfs_stat_t *fill = netdata_get_vfs_structure(&key, &tgid); + if (fill) { + libnetdata_update_u32(&fill->unlink_call, 1); + netdata_update_vfs_err(&fill->unlink_err, ret < 0); + } else { + libnetdata_update_u32(&data.unlink_call, 1); + netdata_update_vfs_err(&data.unlink_err, ret < 0); + netdata_store_vfs_entry(&data, &key, tgid); + } return 0; } @@ -279,10 +296,15 @@ int netdata_vfs_fsync(struct pt_regs* ctx) if (!monitor_apps(&vfs_ctrl)) return 0; - struct netdata_vfs_stat_t *fill = netdata_get_pid_structure(&key, &tgid, &vfs_ctrl, &tbl_vfs_pid); - netdata_update_vfs_entry(fill, &data, &key, tgid, - &fill->fsync_call, &fill->fsync_err, NULL, 0, - 1, ret < 0, 0); + struct netdata_vfs_stat_t *fill = netdata_get_vfs_structure(&key, &tgid); + if (fill) { + libnetdata_update_u32(&fill->fsync_call, 1); + netdata_update_vfs_err(&fill->fsync_err, ret < 0); + } else { + libnetdata_update_u32(&data.fsync_call, 1); + netdata_update_vfs_err(&data.fsync_err, ret < 0); + netdata_store_vfs_entry(&data, &key, tgid); + } return 0; } @@ -310,10 +332,15 @@ int netdata_vfs_open(struct pt_regs* ctx) if (!monitor_apps(&vfs_ctrl)) return 0; - struct netdata_vfs_stat_t *fill = netdata_get_pid_structure(&key, &tgid, &vfs_ctrl, &tbl_vfs_pid); - netdata_update_vfs_entry(fill, &data, &key, tgid, - &fill->open_call, &fill->open_err, NULL, 0, - 1, ret < 0, 0); + struct netdata_vfs_stat_t *fill = netdata_get_vfs_structure(&key, &tgid); + if (fill) { + libnetdata_update_u32(&fill->open_call, 1); + netdata_update_vfs_err(&fill->open_err, ret < 0); + } else { + libnetdata_update_u32(&data.open_call, 1); + netdata_update_vfs_err(&data.open_err, ret < 0); + netdata_store_vfs_entry(&data, &key, tgid); + } return 0; } @@ -341,10 +368,15 @@ int netdata_vfs_create(struct pt_regs* ctx) if (!monitor_apps(&vfs_ctrl)) return 0; - struct netdata_vfs_stat_t *fill = netdata_get_pid_structure(&key, &tgid, &vfs_ctrl, &tbl_vfs_pid); - netdata_update_vfs_entry(fill, &data, &key, tgid, - &fill->create_call, &fill->create_err, NULL, 0, - 1, ret < 0, 0); + struct netdata_vfs_stat_t *fill = netdata_get_vfs_structure(&key, &tgid); + if (fill) { + libnetdata_update_u32(&fill->create_call, 1); + netdata_update_vfs_err(&fill->create_err, ret < 0); + } else { + libnetdata_update_u32(&data.create_call, 1); + netdata_update_vfs_err(&data.create_err, ret < 0); + netdata_store_vfs_entry(&data, &key, tgid); + } return 0; } diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 00000000..7324bc7e --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,34 @@ +CC ?= gcc +GO ?= go + +LIBBPF = ../libbpf +LIBBPF_READY = ../.local_libbpf/libbpf.a +TESTER_BIN = legacy_test +GO_TESTER_BIN = ../gotests/go_tester +TESTER_SRCS = tester_user.c tester_dns.c +GO_TESTER_SRCS = $(wildcard ../gotests/*.go ../gotests/go.mod ../gotests/go.sum) +TESTER_DEBUG_CFLAGS ?= -g +GO_TESTER_GCFLAGS ?= all=-dwarf=true +GO_TESTER_CGO_CFLAGS ?= -g + +.PHONY: all tester legacy-tester go-tester clean + +all: tester + +tester: legacy-tester go-tester + +legacy-tester: $(TESTER_BIN) + +go-tester: $(GO_TESTER_BIN) + +$(LIBBPF_READY): + $(MAKE) -C ../kernel libbpf + +$(TESTER_BIN): $(TESTER_SRCS) $(LIBBPF_READY) + $(CC) $(CFLAGS) $(TESTER_DEBUG_CFLAGS) -I../.local_libbpf -I$(LIBBPF)/src -I$(LIBBPF)/include -I$(LIBBPF)/include/uapi -L../.local_libbpf -o $@ $(TESTER_SRCS) -lbpf -lz -lelf + +$(GO_TESTER_BIN): $(GO_TESTER_SRCS) $(LIBBPF_READY) + cd ../gotests && env GOCACHE=/tmp/go-cache CGO_ENABLED=1 CGO_CFLAGS="$(strip $(CGO_CFLAGS) $(GO_TESTER_CGO_CFLAGS))" $(GO) build -buildvcs=false -gcflags="$(GO_TESTER_GCFLAGS)" -o go_tester . + +clean: + rm -f $(TESTER_BIN) $(GO_TESTER_BIN) diff --git a/kernel/tester_dns.c b/tests/tester_dns.c similarity index 100% rename from kernel/tester_dns.c rename to tests/tester_dns.c diff --git a/kernel/tester_dns.h b/tests/tester_dns.h similarity index 100% rename from kernel/tester_dns.h rename to tests/tester_dns.h diff --git a/kernel/tester_user.c b/tests/tester_user.c similarity index 71% rename from kernel/tester_user.c rename to tests/tester_user.c index 82e84e2b..df9fe44a 100644 --- a/kernel/tester_user.c +++ b/tests/tester_user.c @@ -5,6 +5,7 @@ #include #include #include +#include // Syscalls #include @@ -21,10 +22,28 @@ static ebpf_specify_name_t dc_optional_name[] = { {.program_name = "netdata_lookup_fast", .function_to_attach = "lookup_fast", + .fallback_function_to_attach = NULL, .optional = NULL, .retprobe = 0}, {.program_name = NULL, .function_to_attach = NULL, + .fallback_function_to_attach = NULL, + .optional = NULL, + .retprobe = 0}}; + +static ebpf_specify_name_t swap_optional_name[] = { {.program_name = "netdata_swap_readpage", + .function_to_attach = "swap_read_folio", + .fallback_function_to_attach = "swap_readpage", + .optional = NULL, + .retprobe = 0}, + {.program_name = "netdata_swap_writepage", + .function_to_attach = "__swap_writepage", + .fallback_function_to_attach = "swap_writepage", + .optional = NULL, + .retprobe = 0}, + {.program_name = NULL, + .function_to_attach = NULL, + .fallback_function_to_attach = NULL, .optional = NULL, .retprobe = 0}}; @@ -79,7 +98,7 @@ ebpf_module_t ebpf_modules[] = { { .kernels = NETDATA_V3_10 | NETDATA_V4_14 | NETDATA_V4_16 | NETDATA_V4_18 | NETDATA_V5_4 | NETDATA_V5_14, .flags = NETDATA_FLAG_SYNC, .name = "sync_file_range", .update_names = NULL, .ctrl_table = NULL }, { .kernels = NETDATA_V3_10 | NETDATA_V4_14 | NETDATA_V4_16 | NETDATA_V4_18 | NETDATA_V5_4 | NETDATA_V5_14 | NETDATA_V6_8, - .flags = NETDATA_FLAG_SWAP, .name = "swap", .update_names = NULL, .ctrl_table = "swap_ctrl" }, + .flags = NETDATA_FLAG_SWAP, .name = "swap", .update_names = swap_optional_name, .ctrl_table = "swap_ctrl" }, { .kernels = NETDATA_V3_10 | NETDATA_V4_14 | NETDATA_V4_16 | NETDATA_V4_18 | NETDATA_V5_4 | NETDATA_V5_14, .flags = NETDATA_FLAG_VFS, .name = "vfs", .update_names = NULL, .ctrl_table = "vfs_ctrl" }, { .kernels = NETDATA_V3_10 | NETDATA_V4_14 | NETDATA_V4_16 | NETDATA_V4_18 | NETDATA_V5_4 | NETDATA_V5_14, @@ -104,6 +123,73 @@ static uint16_t dns_ports[NETDATA_DNS_MAX_PORTS] = { NETDATA_DNS_DEFAULT_PORT }; static size_t dns_port_count = 1; static int dns_ports_overridden = 0; +typedef struct ebpf_map_support { + int hash; + int array; + int percpu_array; + int percpu_hash; +} ebpf_map_support_t; + +typedef struct ebpf_candidate_list { + char **files; + size_t size; + size_t capacity; +} ebpf_candidate_list_t; + +static size_t ebpf_round_up_size(size_t value, size_t align) +{ + return ((value + align - 1) / align) * align; +} + +static int ebpf_possible_cpu_count(void) +{ + int count = libbpf_num_possible_cpus(); + + if (count > 0) + return count; + + if (nprocesses > 0 && nprocesses <= INT_MAX) + return (int)nprocesses; + + return 1; +} + +static int ebpf_map_is_percpu(uint32_t type) +{ + return type == BPF_MAP_TYPE_PERCPU_ARRAY || type == BPF_MAP_TYPE_PERCPU_HASH; +} + +static size_t ebpf_map_value_stride(uint32_t type, size_t value_size) +{ + if (!ebpf_map_is_percpu(type)) + return value_size; + + return ebpf_round_up_size(value_size, 8); +} + +static size_t ebpf_map_value_buffer_length(uint32_t type, size_t value_size) +{ + if (!ebpf_map_is_percpu(type)) + return value_size; + + return ebpf_map_value_stride(type, value_size) * (size_t)ebpf_possible_cpu_count(); +} + +static void ebpf_store_scalar_value(void *buffer, size_t value_size, uint64_t value) +{ + if (value_size >= sizeof(value)) { + memcpy(buffer, &value, sizeof(value)); + return; + } + + { + uint32_t truncated = (uint32_t)value; + size_t copy = value_size < sizeof(truncated) ? value_size : sizeof(truncated); + + memcpy(buffer, &truncated, copy); + } +} + static void ebpf_add_dns_port(uint16_t port) { size_t i; @@ -230,6 +316,335 @@ static void ebpf_write_failure_debug(struct bpf_object *obj, const char *stage, fprintf(stdlog, "\n }\n }\n"); } +static char *ebpf_strdup_string(const char *src) +{ + size_t len = strlen(src) + 1; + char *ret = malloc(len); + if (!ret) + return NULL; + + memcpy(ret, src, len); + return ret; +} + +static char *ebpf_resolve_binary_directory(void) +{ + char *resolved; + + if (!netdata_path) + return getcwd(NULL, 0); + + resolved = realpath(netdata_path, NULL); + if (resolved) + return resolved; + + return ebpf_strdup_string(netdata_path); +} + +static int ebpf_append_candidate(ebpf_candidate_list_t *list, const char *path) +{ + if (list->size == list->capacity) { + size_t new_capacity = (list->capacity) ? list->capacity * 2 : 4; + char **files = realloc(list->files, new_capacity * sizeof(char *)); + if (!files) + return -1; + + list->files = files; + list->capacity = new_capacity; + } + + list->files[list->size] = ebpf_strdup_string(path); + if (!list->files[list->size]) + return -1; + + list->size++; + return 0; +} + +static int ebpf_compare_candidates(const void *lhs, const void *rhs) +{ + const char * const *left = lhs; + const char * const *right = rhs; + return strcmp(*left, *right); +} + +static void ebpf_free_candidate_list(ebpf_candidate_list_t *list) +{ + size_t i; + + if (!list) + return; + + for (i = 0; i < list->size; i++) + free(list->files[i]); + + free(list->files); + memset(list, 0, sizeof(*list)); +} + +static int ebpf_candidate_matches(const char *filename, const char *name, int is_return, + const char *version, int rhf_version) +{ + char prefix[128]; + size_t prefix_len; + size_t version_len = strlen(version); + size_t filename_len = strlen(filename); + const char *rest; + int has_rhf; + + snprintf(prefix, sizeof(prefix), "%cnetdata_ebpf_%s.", (is_return) ? 'r' : 'p', name); + prefix_len = strlen(prefix); + if (filename_len <= prefix_len + 2) + return 0; + + if (strncmp(filename, prefix, prefix_len) || strcmp(filename + filename_len - 2, ".o")) + return 0; + + rest = filename + prefix_len; + if (strncmp(rest, version, version_len)) + return 0; + + if (rest[version_len] && rest[version_len] != '.') + return 0; + + has_rhf = (strstr(rest, ".rhf") != NULL); + if (rhf_version != -1) + return has_rhf; + + return !has_rhf; +} + +static void ebpf_discover_candidates(ebpf_candidate_list_t *list, const char *name, int is_return, + const char *version, int rhf_version) +{ + char *path = ebpf_resolve_binary_directory(); + DIR *dir; + struct dirent *entry; + + memset(list, 0, sizeof(*list)); + if (!path) + return; + + dir = opendir(path); + if (!dir) { + free(path); + return; + } + + while ((entry = readdir(dir))) { + char fullpath[PATH_MAX]; + + if (entry->d_name[0] == '.') + continue; + + if (!ebpf_candidate_matches(entry->d_name, name, is_return, version, rhf_version)) + continue; + + snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name); + if (ebpf_append_candidate(list, fullpath)) + break; + } + + closedir(dir); + free(path); + + if (list->size > 1) + qsort(list->files, list->size, sizeof(char *), ebpf_compare_candidates); +} + +#ifdef LIBBPF_MAJOR_VERSION +static int ebpf_probe_map_type_support(int map_type) +{ + return libbpf_probe_bpf_map_type((enum bpf_map_type)map_type, NULL); +} +#else +static int ebpf_probe_map_type_support(int map_type) +{ + (void)map_type; + return -EOPNOTSUPP; +} +#endif + +static int ebpf_should_fallback_percpu_support(int rhf_version, uint32_t kver) +{ + if (rhf_version > 0 && kver < NETDATA_MINIMUM_EBPF_KERNEL) + return 0; + + return (kver >= NETDATA_MINIMUM_EBPF_KERNEL || rhf_version > 0); +} + +static void ebpf_detect_map_support(ebpf_map_support_t *support, int rhf_version, uint32_t kver) +{ + int probe; + + memset(support, 0, sizeof(*support)); + support->hash = (kver >= NETDATA_MINIMUM_EBPF_KERNEL || rhf_version > 0); + support->array = support->hash; + support->percpu_array = ebpf_should_fallback_percpu_support(rhf_version, kver); + support->percpu_hash = support->percpu_array; + + probe = ebpf_probe_map_type_support(BPF_MAP_TYPE_HASH); + if (probe >= 0) + support->hash = probe > 0; + + probe = ebpf_probe_map_type_support(BPF_MAP_TYPE_ARRAY); + if (probe >= 0) + support->array = probe > 0; + + probe = ebpf_probe_map_type_support(BPF_MAP_TYPE_PERCPU_ARRAY); + if (probe >= 0) + support->percpu_array = probe > 0; + + probe = ebpf_probe_map_type_support(BPF_MAP_TYPE_PERCPU_HASH); + if (probe >= 0) + support->percpu_hash = probe > 0; +} + +static const char *ebpf_map_type_name(int map_type) +{ + switch (map_type) { + case BPF_MAP_TYPE_HASH: + return "hash"; + case BPF_MAP_TYPE_ARRAY: + return "array"; + case BPF_MAP_TYPE_PERCPU_HASH: + return "percpu_hash"; + case BPF_MAP_TYPE_PERCPU_ARRAY: + return "percpu_array"; + default: + return "unknown"; + } +} + +static int ebpf_is_supported_map_type(const ebpf_map_support_t *support, int map_type) +{ + switch (map_type) { + case BPF_MAP_TYPE_HASH: + return support->hash; + case BPF_MAP_TYPE_ARRAY: + return support->array; + case BPF_MAP_TYPE_PERCPU_HASH: + return support->percpu_hash; + case BPF_MAP_TYPE_PERCPU_ARRAY: + return support->percpu_array; + default: + return 1; + } +} + +static int ebpf_find_unsupported_map_type(struct bpf_object *obj, const ebpf_map_support_t *support, + int *unsupported_type) +{ + struct bpf_map *map; + + bpf_object__for_each_map(map, obj) { + int type; +#ifdef LIBBPF_MAJOR_VERSION + type = bpf_map__type(map); +#else + { + const struct bpf_map_def *def = bpf_map__def(map); + type = def->type; + } +#endif + if (!ebpf_is_supported_map_type(support, type)) { + *unsupported_type = type; + return 1; + } + } + + return 0; +} + +static void ebpf_write_supported_map_types_json(const ebpf_map_support_t *support) +{ + int first = 1; + + fprintf(stdlog, "["); + if (support->hash) { + fprintf(stdlog, "\"hash\""); + first = 0; + } + if (support->array) { + fprintf(stdlog, "%s\"array\"", first ? "" : ", "); + first = 0; + } + if (support->percpu_hash) { + fprintf(stdlog, "%s\"percpu_hash\"", first ? "" : ", "); + first = 0; + } + if (support->percpu_array) + fprintf(stdlog, "%s\"percpu_array\"", first ? "" : ", "); + + fprintf(stdlog, "]"); +} + +static void ebpf_write_object_map_types(struct bpf_object *obj) +{ + struct bpf_map *map; + int seen_hash = 0, seen_array = 0, seen_percpu_hash = 0, seen_percpu_array = 0; + int first = 1; + + fprintf(stdlog, " \"Map Types Used\" : ["); + if (obj) { + bpf_object__for_each_map(map, obj) { + int type; +#ifdef LIBBPF_MAJOR_VERSION + type = bpf_map__type(map); +#else + { + const struct bpf_map_def *def = bpf_map__def(map); + type = def->type; + } +#endif + + if ((type == BPF_MAP_TYPE_HASH && seen_hash) || + (type == BPF_MAP_TYPE_ARRAY && seen_array) || + (type == BPF_MAP_TYPE_PERCPU_HASH && seen_percpu_hash) || + (type == BPF_MAP_TYPE_PERCPU_ARRAY && seen_percpu_array)) + continue; + + if (!first) + fprintf(stdlog, ", "); + + fprintf(stdlog, "\"%s\"", ebpf_map_type_name(type)); + first = 0; + + if (type == BPF_MAP_TYPE_HASH) + seen_hash = 1; + else if (type == BPF_MAP_TYPE_ARRAY) + seen_array = 1; + else if (type == BPF_MAP_TYPE_PERCPU_HASH) + seen_percpu_hash = 1; + else if (type == BPF_MAP_TYPE_PERCPU_ARRAY) + seen_percpu_array = 1; + } + } + + fprintf(stdlog, "],\n"); +} + +static void ebpf_write_map_compatibility_debug(int unsupported_type, const ebpf_map_support_t *support) +{ + char error_buffer[128]; + const char *error_message = ebpf_describe_error(-EOPNOTSUPP, error_buffer, sizeof(error_buffer)); + + fprintf(stdlog, + " \"Debug\" : {\n" + " \"Info\" : { \"Stage\" : \"map_compatibility\",\n" + " \"Error Code\" : %d,\n" + " \"Error Message\" : \"%s\",\n" + " \"Unsupported Map Type\" : \"%s\",\n" + " \"Supported Map Types\" : ", + -EOPNOTSUPP, error_message, ebpf_map_type_name(unsupported_type)); + ebpf_write_supported_map_types_json(support); + fprintf(stdlog, + ",\n" + " \"Programs\" : []\n" + " }\n" + " }\n"); +} + /**************************************************************************************************** * * KERNEL VERSION @@ -249,16 +664,17 @@ int ebpf_get_kernel_version() char ver[VERSION_STRING_LEN]; char *version = ver; - int fd = open("/proc/sys/kernel/osrelease", O_RDONLY); + int fd = open("/proc/sys/kernel/osrelease", O_RDONLY | O_CLOEXEC); if (fd < 0) return -1; - ssize_t len = read(fd, ver, sizeof(ver)); + ssize_t len = read(fd, ver, sizeof(ver) - 1); if (len < 0) { close(fd); return -1; } + ver[len] = '\0'; close(fd); char *move = major; @@ -282,7 +698,19 @@ int ebpf_get_kernel_version() *move++ = *version++; *move = '\0'; - return ((int)(strtol(major, NULL, 10) * 65536) + (int)(strtol(minor, NULL, 10) * 256) + (int)strtol(patch, NULL, 10)); + long major_val = strtol(major, NULL, 10); + long minor_val = strtol(minor, NULL, 10); + if (major_val < 0 || minor_val < 0) + return -1; + + int ipatch = (int)strtol(patch, NULL, 10); + if (ipatch < 0) + return -1; + + if (ipatch > 255) + ipatch = 255; + + return ((int)(major_val * 65536) + (int)(minor_val * 256) + ipatch); } /** @@ -310,7 +738,7 @@ int ebpf_get_redhat_release() char *end = strchr(buffer, '.'); char *start; if (end) { - *end = 0x0; + *end = '\0'; if (end > buffer) { start = end - 1; @@ -318,9 +746,9 @@ int ebpf_get_redhat_release() major = strtol(start, NULL, 10); start = ++end; - end++; - if (end) { - end = 0x00; + char *minor_end = strchr(start, ' '); + if (minor_end) { + *minor_end = '\0'; minor = strtol(start, NULL, 10); } else { minor = -1; @@ -482,7 +910,10 @@ static void ebpf_start_netdata_json(char *filename, int is_return) static void ebpf_mount_name(char *out, size_t len, uint32_t kver, char *name, int is_return, int rhf_version) { char *version = ebpf_select_kernel_name(kver); - char *path = (!netdata_path) ? getcwd(NULL, 0) : realpath(netdata_path, NULL); + char *path = ebpf_resolve_binary_directory(); + if (!path) + path = ebpf_strdup_string("."); + snprintf(out, len, "%s/%cnetdata_ebpf_%s.%s%s.o", path, (is_return) ? 'r' : 'p', @@ -577,8 +1008,9 @@ static int ebpf_attach_programs(ebpf_attach_t *load, struct bpf_object *obj, siz if (w) { enum bpf_prog_type type = bpf_program__get_type(prog); + const char *target = w->optional ? w->optional : w->function_to_attach; if (type == BPF_PROG_TYPE_KPROBE) - links[i] = bpf_program__attach_kprobe(prog, w->retprobe, w->optional); + links[i] = bpf_program__attach_kprobe(prog, w->retprobe, target); } else links[i] = bpf_program__attach(prog); @@ -607,34 +1039,58 @@ static int ebpf_attach_programs(ebpf_attach_t *load, struct bpf_object *obj, siz */ static void ebpf_update_names(ebpf_specify_name_t *names) { - if (names->optional) - return; + int i = 0; + while (names[i].function_to_attach) { + if (names[i].optional) { + i++; + continue; + } - char line[256]; - FILE *fp = fopen("/proc/kallsyms", "r"); - if (!fp) - return; + char line[256]; + FILE *fp = fopen("/proc/kallsyms", "r"); + if (!fp) + return; + + char *data; + while ((data = fgets(line, 255, fp))) { + const char *candidates[] = { + names[i].function_to_attach, + names[i].fallback_function_to_attach + }; + size_t j; + + data += 19; + for (j = 0; j < 2; j++) { + const char *cmp = candidates[j]; + size_t len; + char *end; + + if (!cmp) + continue; + + len = strlen(cmp); + if (strncmp(cmp, data, len)) + continue; + + end = strchr(data, ' '); + if (!end) + end = strchr(data, '\n'); + + if (!end) + continue; - char *data; - char *cmp = names->function_to_attach; - size_t len = strlen(cmp); - while ( (data = fgets(line, 255, fp))) { - data += 19; - ebpf_specify_name_t *move = names; - if (!strncmp(cmp, data, len)) { - char *end = strchr(data, ' '); - if (!end) - end = strchr(data, '\n'); - - if (end) *end = '\0'; + names[i].optional = strdup(data); + break; + } - names->optional = strdup(data); - break; + if (names[i].optional) + break; } - } - fclose(fp); + fclose(fp); + i++; + } } /** @@ -691,11 +1147,9 @@ static void ebpf_cleanup_tables(ebpf_table_data_t *out) * @param key the size of the key. * @param value the size of the values. */ -static ebpf_table_data_t *ebpf_allocate_tables(const char *name, size_t key, size_t value) +static ebpf_table_data_t *ebpf_allocate_tables(const char *name, uint32_t type, size_t key, size_t value) { - // We multiply value by number of proccess to avoid problems when data is stored - // per process - value *= nprocesses; + value = ebpf_map_value_buffer_length(type, value); ebpf_table_data_t *ret = calloc(1, sizeof(ebpf_table_data_t)); if (!ret) @@ -752,14 +1206,23 @@ static inline void ebpf_values_accumulator(ebpf_table_data_t *values) * @param filled pointer to filled counter. * @param zero pointer to zero counter. */ -static inline void ebpf_check_and_update_counter(int fd, uint32_t key, uint32_t *filled, uint32_t *zero) +static inline void ebpf_check_and_update_counter(int fd, uint32_t key, size_t value_length, + uint32_t *filled, uint32_t *zero) { - uint32_t value[NETDATA_CONTROLLER_END]; + void *value = calloc(1, value_length); + + if (!value) { + (*zero)++; + return; + } + if (bpf_map_lookup_elem(fd, &key, value)) { (*zero)++; } else { (*filled)++; } + + free(value); } /** @@ -772,16 +1235,17 @@ static inline void ebpf_check_and_update_counter(int fd, uint32_t key, uint32_t */ static void ebpf_read_generic_table(ebpf_table_data_t *values, int fd) { - size_t zero = 0; - size_t filled = 0; - // Reset completely the keys memset(values->key, 0, values->key_length); memset(values->next_key, 0, values->key_length); memset(values->value, 0, values->value_length); + // Passing a NULL key retrieves the first entry and preserves key 0 for arrays. + if (bpf_map_get_next_key(fd, NULL, values->next_key)) + return; + // Go trough all keys stored inside the eBPF maps - while (!bpf_map_get_next_key(fd, values->key, values->next_key)) { + while (1) { if (!bpf_map_lookup_elem(fd, values->next_key, values->value)) { ebpf_values_accumulator(values); } @@ -789,11 +1253,10 @@ static void ebpf_read_generic_table(ebpf_table_data_t *values, int fd) // Copy the next key for the current key memcpy(values->key, values->next_key, values->key_length); - memset(values->value, 0, values->value_length); - } + if (bpf_map_get_next_key(fd, values->key, values->next_key)) + break; - if (!bpf_map_lookup_elem(fd, values->key, values->value)) { - ebpf_values_accumulator(values); + memset(values->value, 0, values->value_length); } } @@ -850,7 +1313,7 @@ static void ebpf_controller_json(ebpf_table_data_t *values, int fd) uint32_t key; for (key = 0; key < NETDATA_CONTROLLER_END; key++) { - ebpf_check_and_update_counter(fd, key, &value, &zero); + ebpf_check_and_update_counter(fd, key, values->value_length, &value, &zero); } fprintf(stdlog, " " @@ -889,7 +1352,7 @@ static void ebpf_test_maps(struct bpf_object *obj, char *ctrl) key_size = def->key_size; value_size = def->value_size; #endif - values = ebpf_allocate_tables(name, key_size, value_size); + values = ebpf_allocate_tables(name, type, key_size, value_size); if (values) { // Write header fprintf(stdlog, @@ -943,18 +1406,51 @@ static void ebpf_fill_ctrl(struct bpf_object *obj, char *ctrl) int fd = bpf_map__fd(map); unsigned int end; + uint32_t type; + uint32_t value_size; #ifdef LIBBPF_MAJOR_VERSION + type = bpf_map__type(map); + value_size = bpf_map__value_size(map); end = bpf_map__max_entries(map); #else const struct bpf_map_def *def = bpf_map__def(map); + type = def->type; + value_size = def->value_size; end = def->max_entries; #endif - uint32_t values[NETDATA_CONTROLLER_END] = { 1, map_level, 0, 0, 0, 0 }; + uint64_t values[NETDATA_CONTROLLER_END] = { 1, (uint64_t)map_level, 0, 0, 0, 0 }; + size_t value_length = ebpf_map_value_buffer_length(type, value_size); + size_t value_stride = ebpf_map_value_stride(type, value_size); + size_t cpu_count = value_stride ? value_length / value_stride : 1; + char *value_buffer = calloc(1, value_length); + unsigned int limit = end; unsigned int i; - for (i = 0; i < end; i++) { - if (bpf_map_update_elem(fd, &i, &values[i], 0)) + + if (!value_buffer) { + fprintf(stdlog, "\"error\" : \"Cannot allocate control buffer for table %s.\",", name); + continue; + } + + if (limit > NETDATA_CONTROLLER_END) { + fprintf(stdlog, + "\"error\" : \"Control table %s has %u entries, limiting writes to %u.\",", + name, end, NETDATA_CONTROLLER_END); + limit = NETDATA_CONTROLLER_END; + } + + for (i = 0; i < limit; i++) { + size_t cpu; + + memset(value_buffer, 0, value_length); + for (cpu = 0; cpu < cpu_count; cpu++) { + ebpf_store_scalar_value(value_buffer + (cpu * value_stride), value_size, values[i]); + } + + if (bpf_map_update_elem(fd, &i, value_buffer, 0)) fprintf(stdlog, "\"error\" : \"Add key(%u) for controller table failed.\",", i); } + + free(value_buffer); } } @@ -991,6 +1487,7 @@ static char *ebpf_tester(char *filename, ebpf_specify_name_t *names, uint32_t ma } total = ebpf_count_programs(obj); + ebpf_write_object_map_types(obj); socket_filter_detected = ebpf_object_has_socket_filter(obj); if (socket_filter_detected) { char *ret = (char *)ebpf_socket_filter_tester(obj, maps, stdlog, end_iteration, dns_ports, dns_port_count); @@ -1049,17 +1546,71 @@ static char *ebpf_tester(char *filename, ebpf_specify_name_t *names, uint32_t ma */ static void ebpf_run_netdata_tests(int rhf_version, uint32_t kver, int is_return, uint64_t flags) { + ebpf_map_support_t map_support; char load[FILENAME_MAX]; int i = 0; + + ebpf_detect_map_support(&map_support, rhf_version, kver); while (ebpf_modules[i].name) { if (flags & ebpf_modules[i].flags) { + ebpf_candidate_list_t candidates; + ebpf_candidate_list_t compatible = { 0 }; + char *first_incompatible = NULL; + int unsupported_type = 0; + size_t j; uint32_t idx = ebpf_select_index(ebpf_modules[i].kernels, rhf_version, kver); - ebpf_mount_name(load, FILENAME_MAX - 1, idx, ebpf_modules[i].name, is_return, rhf_version); + char *version = ebpf_select_kernel_name(idx); + + ebpf_discover_candidates(&candidates, ebpf_modules[i].name, is_return, version, rhf_version); + for (j = 0; j < candidates.size; j++) { + struct bpf_object *obj = bpf_object__open_file(candidates.files[j], NULL); + if (libbpf_get_error(obj)) { + if (obj) + bpf_object__close(obj); + if (ebpf_append_candidate(&compatible, candidates.files[j])) + break; + + continue; + } - ebpf_start_netdata_json(load, is_return); - char *result = ebpf_tester(load, ebpf_modules[i].update_names, flags & NETDATA_FLAG_CONTENT, - ebpf_modules[i].ctrl_table, kver); - fprintf(stdlog, " },\n \"Status\" : \"%s\"\n},\n", result); + if (!ebpf_find_unsupported_map_type(obj, &map_support, &unsupported_type)) { + if (ebpf_append_candidate(&compatible, candidates.files[j])) { + bpf_object__close(obj); + break; + } + } else if (!first_incompatible) { + first_incompatible = ebpf_strdup_string(candidates.files[j]); + } + + bpf_object__close(obj); + } + + if (compatible.size) { + for (j = 0; j < compatible.size; j++) { + ebpf_start_netdata_json(compatible.files[j], is_return); + { + char *result = ebpf_tester(compatible.files[j], ebpf_modules[i].update_names, + flags & NETDATA_FLAG_CONTENT, ebpf_modules[i].ctrl_table, kver); + fprintf(stdlog, " },\n \"Status\" : \"%s\"\n},\n", result); + } + } + } else if (first_incompatible) { + ebpf_start_netdata_json(first_incompatible, is_return); + ebpf_write_map_compatibility_debug(unsupported_type, &map_support); + fprintf(stdlog, " },\n \"Status\" : \"%s\"\n},\n", "Fail"); + } else { + ebpf_mount_name(load, FILENAME_MAX - 1, idx, ebpf_modules[i].name, is_return, rhf_version); + ebpf_start_netdata_json(load, is_return); + { + char *result = ebpf_tester(load, ebpf_modules[i].update_names, flags & NETDATA_FLAG_CONTENT, + ebpf_modules[i].ctrl_table, kver); + fprintf(stdlog, " },\n \"Status\" : \"%s\"\n},\n", result); + } + } + + free(first_incompatible); + ebpf_free_candidate_list(&compatible); + ebpf_free_candidate_list(&candidates); } i++; @@ -1415,6 +1966,7 @@ uint64_t ebpf_parse_arguments(int argc, char **argv, int kver) static void ebpf_fill_names() { ebpf_update_names(dc_optional_name); + ebpf_update_names(swap_optional_name); } /** @@ -1425,6 +1977,7 @@ static void ebpf_fill_names() static void ebpf_clean_name_vectors() { ebpf_clean_optional(dc_optional_name); + ebpf_clean_optional(swap_optional_name); } /** diff --git a/kernel/tester_user.h b/tests/tester_user.h similarity index 99% rename from kernel/tester_user.h rename to tests/tester_user.h index 6fd642e5..505adc1b 100644 --- a/kernel/tester_user.h +++ b/tests/tester_user.h @@ -159,6 +159,7 @@ enum netdata_thread_OPT { typedef struct ebpf_specify_name { char *program_name; char *function_to_attach; + char *fallback_function_to_attach; char *optional; bool retprobe; } ebpf_specify_name_t;