Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/irchaosclub/FANGS/llms.txt

Use this file to discover all available pages before exploring further.

The FANGS sensor is a CO-RE (Compile Once – Run Everywhere) eBPF program loaded by fangs-runner at startup. All probes attach globally to the host kernel; a per-CPU hash map called CGMAP gates every probe so that only processes whose cgroupv2 ancestry matches a registered run ID produce events. There is no agent, sidecar, or library injected into the monitored container — observation happens entirely from the host kernel.

Probe inventory

The sensor loads 10 probes in three categories. Tracepoints are mandatory; kprobes and the uprobe are best-effort (attach failures produce a warning and the runner continues with the remaining probes active).
TracepointHandlerEvent type
syscalls/sys_enter_openatHandleOpenatFileAccessEvent
syscalls/sys_enter_execveHandleExecveExecEvent
syscalls/sys_enter_connectHandleConnectNetConnectEvent (source = NetSourceSyscall)
syscalls/sys_enter_sendtoHandleSendtoDNSQueryEvent or TLSSniEvent (tcp_clienthello)
syscalls/sys_enter_sendmmsgHandleSendmmsgDNSQueryEvent (parallel A+AAAA queries)
syscalls/sys_enter_sendmsgHandleSendmsgDNSQueryEvent (single-message resolvers)
syscalls/sys_enter_writeHandleWriteTLSSniEvent (tcp_clienthello, Node BoringSSL path)
Tracepoints attach via link.Tracepoint from cilium/ebpf. EnsureTracefs: true in sensor.Options will auto-mount /sys/kernel/tracing if it is not already mounted.

Event types

Every event starts with a common 72-byte header (EventHeader) carrying timestamp, cgroup ID, run ID (16-byte ULID), PID, TID, PPID, UID, GID, and process comm string. The type byte at offset 68 tells the userspace decoder which struct follows.
const (
    EventTypeFileAccess EventType = 1 // openat
    EventTypeExec       EventType = 2 // execve
    EventTypeNetConnect EventType = 3 // connect / tcp_v{4,6}_connect
    EventTypeDNSQuery   EventType = 4 // sendto/sendmsg/sendmmsg to port 53
    EventTypeTLSSNI     EventType = 5 // SSL_ctrl uprobe or ClientHello parse
)

FileAccessEvent

Emitted by HandleOpenat for every openat(2) call inside the monitored cgroup where the opened path matches a path_filter entry. Fields beyond the header:
FieldTypeDescription
PathNamestringParsed null-terminated path (up to 256 bytes)
Flagsint32openat flags argument (O_RDONLY, O_WRONLY, O_CREAT, …)
Truncateduint81 if the path was longer than 256 bytes and was truncated
Tagsuint8Bitmask: EventTagInteresting (bit 0), EventTagCredAccess (bit 1)
Paths that match a cred_tagged: true watched-path entry have both tag bits set, driving the red row in the UI.

ExecEvent

Emitted by HandleExecve for every execve(2) inside the cgroup. Exec events bypass the path_filter — all execs are emitted regardless of path. Captures up to 8 argv slots (64 bytes each) and 5 levels of process ancestry.

NetConnectEvent

Emitted by HandleConnect (tracepoint, source = NetSourceSyscall) and by HandleTcpV4Connect / HandleTcpV6Connect (kprobes, source = NetSourceKprobe). Only AF_INET and AF_INET6 families are captured; port = 0 connects (glibc getaddrinfo source-address selection probes) are discarded in-kernel.

DNSQueryEvent

Emitted by HandleSendto, HandleSendmmsg, and HandleSendmsg when the destination port resolves to 53. The raw DNS wire bytes (up to 200 bytes) are captured and parsed in userspace — keeping the label-walk out of BPF avoids verifier headaches. HandleSendmmsg handles glibc 2.30+ and curl’s parallel A + AAAA queries (two mmsghdr entries per call).

TLSSniEvent

TLS SNI events are tagged with one of three source constants (two currently active; one planned):
Source constantValueMechanism
TLSSourceLibSSL1SSL_ctrl uprobe — SNI pre-populated in SNI[]
TLSSourceNodeInternal2Uprobe in Node binary’s bundled TLS (planned, not yet active) — SNI pre-populated in SNI[]
TLSSourceTCPClientHello3sendto/write tracepoint — raw ClientHello in RawPayload[], SNI parsed userspace
HandleWrite covers Node.js, which bundles BoringSSL statically and writes TLS handshake bytes via plain write(2) on a TCP socket — bypassing both the sendto path and the libssl uprobe. The 6-byte ClientHello signature check (0x16 0x03 ?? ?? ?? 0x01) rejects non-TLS writes in a few instructions before any further work.

CGMAP and path_filter

Two BPF maps control what the sensor observes.

CGMAP

A BPF_MAP_TYPE_HASH keyed by cgroup_id (u64), mapping to a CgmapValue that carries the run_id. Every probe calls lookup_cgroup() first; events from processes not in the map are discarded immediately. The map supports up to 256 concurrent cgroups. Docker sometimes places container processes in a subcgroup of the registered one; lookup_cgroup walks up to 8 ancestor levels to handle that.

path_filter

A BPF_MAP_TYPE_LPM_TRIE (Longest Prefix Match) keyed by (prefix_len_bits, path[256]). File events are discarded unless the opened path starts with a registered prefix. The value is a PathFilterAction: either PathActionKeep (1) or PathActionKeepCredTagged (2). Exec, connect, DNS, and TLS events bypass this filter entirely.
Sensor.AddCgroup must be called before docker start. This pre-attach-then-register pattern means the CGMAP entry exists before the container process’s first syscall fires. AddCgroup is atomic under a mutex — concurrent scans serialize path_filter mutations. RemoveCgroup is idempotent; it cleans up both the CGMAP entry and all path_filter entries for that cgroup.
// Default watched paths used by every auto-submitted scan.
// cred_tagged: true paths set EventTagCredAccess on matching events.
[]proto.WatchedPath{
    {Prefix: "/etc/"},
    {Prefix: "/etc/shadow",        CredTagged: true},
    {Prefix: "/etc/passwd",        CredTagged: true},
    {Prefix: "/root/"},
    {Prefix: "/root/.ssh/",        CredTagged: true},
    {Prefix: "/root/.aws/",        CredTagged: true},
    {Prefix: "/root/.npmrc",       CredTagged: true},
    {Prefix: "/root/.docker/",     CredTagged: true},
    {Prefix: "/root/.kube/",       CredTagged: true},
    {Prefix: "/root/.gnupg/",      CredTagged: true},
    {Prefix: "/proc/self/environ", CredTagged: true},
    {Prefix: "/tmp/"},
    {Prefix: "/usr/"},
    {Prefix: "/dev/"},
}

Deduplication

TLS dedup (5 s window)

The same TLS connection can produce events from multiple sources — for example, a Node.js binary that has libssl dynamically linked and writes raw bytes via write(2) would fire both HandleSslCtrl (libssl uprobe) and HandleWrite (tcp_clienthello). The sensor tracks (pid, sni) tuples in a sliding window; the second event within the window has DuplicateOf set to the name of the first source. Consumers can filter on DuplicateOf != "" to deduplicate. The window duration is set by sensor.Options.DedupWindow (default 5 * time.Second). Zero disables dedup tagging entirely.

Connect dedup (100 ms window)

A single TCP connect(2) fires both sys_enter_connect (tracepoint) and tcp_v4/6_connect (kprobe). The dedup is asymmetric:
  • Syscall event (NetSourceSyscall): always emitted; the (pid, family, ip, port) key is recorded in a pending map.
  • Kprobe event (NetSourceKprobe): if a matching syscall key exists within 100 ms, this event is dropped (it’s the syscall-path duplicate). If no syscall entry exists within the window, the kprobe event is emitted — this is an io_uring-initiated connect with no paired tracepoint.
This preserves io_uring coverage without double-counting ordinary syscall-path connects.

Ringbuf and drops

All events are written to a single BPF_MAP_TYPE_RINGBUF (64 MiB). When the ringbuf is full, bpf_ringbuf_reserve returns NULL and the probe increments a per-CPU drops_counter. The runner sums the counter across CPUs and reports total drops in the final ScanResult. Monitor fangs_runner_events_dropped_total in Prometheus to detect pressure.
The ringbuf reader runs in a dedicated goroutine inside the runner. Events are read, decoded, and forwarded to the Events() channel (buffer depth 256). Back-pressure on the channel does not block the ringbuf reader goroutine — if the channel is full the probe may begin dropping at the ringbuf level.

Build docs developers (and LLMs) love