inspektor-gadget
1// Copyright 2019-2024 The Inspektor Gadget authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//go:build !withoutebpf
16
17package tracer18
19import (20"context"21"fmt"22"net/netip"23"time"24"unsafe"25
26log "github.com/sirupsen/logrus"27
28gadgetcontext "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-context"29"github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets"30"github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/dns/types"31"github.com/inspektor-gadget/inspektor-gadget/pkg/logger"32"github.com/inspektor-gadget/inspektor-gadget/pkg/networktracer"33eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types"34)
35
36//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel -cc clang -cflags ${CFLAGS} -type event_t dns ./bpf/dns.c -- $CLANG_OS_FLAGS -I./bpf/
37
38const (39BPFQueryMapName = "query_map"40MaxAddrAnswers = 8 // Keep aligned with MAX_ADDR_ANSWERS in bpf/dns-common.h41)
42
43// needs to be kept in sync with dnsEventT from dns_bpfel.go (without the Anaddr field)
44type dnsEventTAbbrev struct {45Netns uint3246_ [4]byte47Timestamp uint6448MountNsId uint6449Pid uint3250Tid uint3251Uid uint3252Gid uint3253Task [16]uint854SaddrV6 [16]uint855DaddrV6 [16]uint856Af uint1657Sport uint1658Dport uint1659Proto uint860_ [1]byte61Id uint1662Qtype uint1663Qr uint864PktType uint865Rcode uint866_ [1]byte67LatencyNs uint6468Name [255]uint869_ [1]byte70Ancount uint1671Anaddrcount uint1672}
73
74type Tracer struct {75*networktracer.Tracer[types.Event]76
77ctx context.Context78cancel context.CancelFunc79}
80
81func NewTracer() (*Tracer, error) {82t := &Tracer{}83
84if err := t.install(); err != nil {85t.Close()86return nil, fmt.Errorf("installing tracer: %w", err)87}88
89// timeout not configurable in this case90if err := t.run(context.TODO(), log.StandardLogger(), time.Minute); err != nil {91t.Close()92return nil, fmt.Errorf("running tracer: %w", err)93}94
95return t, nil96}
97
98// pkt_type definitions:
99// https://github.com/torvalds/linux/blob/v5.14-rc7/include/uapi/linux/if_packet.h#L26
100var pktTypeNames = []string{101"HOST",102"BROADCAST",103"MULTICAST",104"OTHERHOST",105"OUTGOING",106"LOOPBACK",107"USER",108"KERNEL",109}
110
111// List taken from:
112// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4
113var qTypeNames = map[uint]string{1141: "A",1152: "NS",1163: "MD",1174: "MF",1185: "CNAME",1196: "SOA",1207: "MB",1218: "MG",1229: "MR",12310: "NULL",12411: "WKS",12512: "PTR",12613: "HINFO",12714: "MINFO",12815: "MX",12916: "TXT",13017: "RP",13118: "AFSDB",13219: "X25",13320: "ISDN",13421: "RT",13522: "NSAP",13623: "NSAP-PTR",13724: "SIG",13825: "KEY",13926: "PX",14027: "GPOS",14128: "AAAA",14229: "LOC",14330: "NXT",14431: "EID",14532: "NIMLOC",14633: "SRV",14734: "ATMA",14835: "NAPTR",14936: "KX",15037: "CERT",15138: "A6",15239: "DNAME",15340: "SINK",15441: "OPT",15542: "APL",15643: "DS",15744: "SSHFP",15845: "IPSECKEY",15946: "RRSIG",16047: "NSEC",16148: "DNSKEY",16249: "DHCID",16350: "NSEC3",16451: "NSEC3PARAM",16552: "TLSA",16653: "SMIMEA",16755: "HIP",16856: "NINFO",16957: "RKEY",17058: "TALINK",17159: "CDS",17260: "CDNSKEY",17361: "OPENPGPKEY",17462: "CSYNC",17563: "ZONEMD",17664: "SVCB",17765: "HTTPS",17899: "SPF",179100: "UINFO",180101: "UID",181102: "GID",182103: "UNSPEC",183104: "NID",184105: "L32",185106: "L64",186107: "LP",187108: "EUI48",188109: "EUI64",189249: "TKEY",190250: "TSIG",191251: "IXFR",192252: "AXFR",193253: "MAILB",194254: "MAILA",195255: "*",196256: "URI",197257: "CAA",198258: "AVC",199259: "DOA",200260: "AMTRELAY",20132768: "TA",20232769: "DLV",203}
204
205const MaxDNSName = int(unsafe.Sizeof(dnsEventT{}.Name))206
207// DNS header RCODE (response code) field.
208// https://datatracker.ietf.org/doc/rfc1035#section-4.1.1
209var rCodeNames = map[uint8]string{2100: "NoError",2111: "FormErr",2122: "ServFail",2133: "NXDomain",2144: "NotImp",2155: "Refused",216}
217
218// parseLabelSequence parses a label sequence into a string with dots.
219// See https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.2
220func parseLabelSequence(sample []byte) (ret string) {221sampleBounded := make([]byte, MaxDNSName)222copy(sampleBounded, sample)223
224for i := 0; i < MaxDNSName; i++ {225length := int(sampleBounded[i])226if length == 0 {227break228}229if i+1+length < MaxDNSName {230ret += string(sampleBounded[i+1:i+1+length]) + "."231}232i += length233}234return ret235}
236
237func bpfEventToDNSEvent(bpfEvent *dnsEventTAbbrev, answers []byte, netns uint64) (*types.Event, error) {238event := types.Event{239Event: eventtypes.Event{240Type: eventtypes.NORMAL,241},242Pid: bpfEvent.Pid,243Tid: bpfEvent.Tid,244Uid: bpfEvent.Uid,245Gid: bpfEvent.Gid,246WithMountNsID: eventtypes.WithMountNsID{MountNsID: bpfEvent.MountNsId},247WithNetNsID: eventtypes.WithNetNsID{NetNsID: netns},248Comm: gadgets.FromCString(bpfEvent.Task[:]),249}250event.Event.Timestamp = gadgets.WallTimeFromBootTime(bpfEvent.Timestamp)251
252event.ID = fmt.Sprintf("%.4x", bpfEvent.Id)253ipversion := gadgets.IPVerFromAF(bpfEvent.Af)254event.SrcIP = gadgets.IPStringFromBytes(bpfEvent.SaddrV6, ipversion)255event.DstIP = gadgets.IPStringFromBytes(bpfEvent.DaddrV6, ipversion)256
257if bpfEvent.Qr == 1 {258event.Qr = types.DNSPktTypeResponse259event.Nameserver = event.SrcIP260} else {261event.Qr = types.DNSPktTypeQuery262event.Nameserver = event.DstIP263}264
265event.SrcPort = bpfEvent.Sport266event.DstPort = bpfEvent.Dport267event.Protocol = gadgets.ProtoString(int(bpfEvent.Proto))268
269// Convert name into a string with dots270event.DNSName = parseLabelSequence(bpfEvent.Name[:])271
272// Parse the packet type273event.PktType = "UNKNOWN"274pktTypeUint := uint(bpfEvent.PktType)275if pktTypeUint < uint(len(pktTypeNames)) {276event.PktType = pktTypeNames[pktTypeUint]277}278
279qTypeUint := uint(bpfEvent.Qtype)280var ok bool281event.QType, ok = qTypeNames[qTypeUint]282if !ok {283event.QType = "UNASSIGNED"284}285
286if bpfEvent.Qr == 1 {287rCodeUint := uint8(bpfEvent.Rcode)288event.Rcode, ok = rCodeNames[rCodeUint]289if !ok {290event.Rcode = "UNKNOWN"291}292
293event.Latency = time.Duration(bpfEvent.LatencyNs)294}295
296// There's a limit on the number of addresses in the BPF event,297// so bpfEvent.AnaddrCount is always less than or equal to bpfEvent.Ancount298event.NumAnswers = int(bpfEvent.Ancount)299for i := uint16(0); i < bpfEvent.Anaddrcount; i++ {300// For A records, the address in the bpf event will be301// IPv4-mapped-IPv6, which netip.Addr.Unmap() converts back to IPv4.302addr := netip.AddrFrom16([16]byte(answers[i*16 : i*16+16])).Unmap().String()303event.Addresses = append(event.Addresses, addr)304}305
306return &event, nil307}
308
309// --- Registry changes
310
311func (g *GadgetDesc) NewInstance() (gadgets.Gadget, error) {312return &Tracer{}, nil313}
314
315func (t *Tracer) Init(gadgetCtx gadgets.GadgetContext) error {316if err := t.install(); err != nil {317t.Close()318return fmt.Errorf("installing tracer: %w", err)319}320
321t.ctx, t.cancel = gadgetcontext.WithTimeoutOrCancel(gadgetCtx.Context(), gadgetCtx.Timeout())322
323return nil324}
325
326func (t *Tracer) install() error {327networkTracer, err := networktracer.NewTracer[types.Event]()328if err != nil {329return fmt.Errorf("creating network tracer: %w", err)330}331t.Tracer = networkTracer332return nil333}
334
335func (t *Tracer) run(ctx context.Context, logger logger.Logger, dnsTimeout time.Duration) error {336spec, err := loadDns()337if err != nil {338return fmt.Errorf("loading asset: %w", err)339}340
341parseDNSEvent := func(rawSample []byte, netns uint64) (*types.Event, error) {342bpfEvent := (*dnsEventTAbbrev)(unsafe.Pointer(&rawSample[0]))343// 4 (padding) + (size of event without addresses) + (address count * 16)344expected := 4 + int(unsafe.Sizeof(*bpfEvent)) + int(bpfEvent.Anaddrcount)*16345if len(rawSample) != expected {346return nil, fmt.Errorf("invalid sample size: received: %d vs expected: %d",347len(rawSample), expected)348}349
350event, err := bpfEventToDNSEvent(bpfEvent, rawSample[unsafe.Offsetof(dnsEventT{}.Anaddr):], netns)351if err != nil {352return nil, err353}354
355return event, nil356}357
358if err := t.Tracer.Run(spec, types.Base, parseDNSEvent); err != nil {359return fmt.Errorf("setting network tracer spec: %w", err)360}361
362// Start a background thread to garbage collect queries without responses363// from the queries map (used to calculate DNS latency).364// The goroutine terminates when t.ctx is done.365queryMap := t.Tracer.GetMap(BPFQueryMapName)366if queryMap == nil {367t.Close()368return fmt.Errorf("got nil retrieving DNS query map")369}370startGarbageCollector(ctx, logger, dnsTimeout, queryMap)371
372return nil373}
374
375func (t *Tracer) Run(gadgetCtx gadgets.GadgetContext) error {376dnsTimeout := gadgetCtx.GadgetParams().Get(ParamDNSTimeout).AsDuration()377
378if err := t.run(t.ctx, gadgetCtx.Logger(), dnsTimeout); err != nil {379return err380}381
382<-t.ctx.Done()383return nil384}
385
386func (t *Tracer) Close() {387if t.cancel != nil {388t.cancel()389}390
391if t.Tracer != nil {392t.Tracer.Close()393}394}
395