The FANGS sensor is a CO-RE (Compile Once – Run Everywhere) eBPF program loaded byDocumentation 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.
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).- Tracepoints (7)
- Kprobes (2)
- Uprobe (1)
| Tracepoint | Handler | Event type |
|---|---|---|
syscalls/sys_enter_openat | HandleOpenat | FileAccessEvent |
syscalls/sys_enter_execve | HandleExecve | ExecEvent |
syscalls/sys_enter_connect | HandleConnect | NetConnectEvent (source = NetSourceSyscall) |
syscalls/sys_enter_sendto | HandleSendto | DNSQueryEvent or TLSSniEvent (tcp_clienthello) |
syscalls/sys_enter_sendmmsg | HandleSendmmsg | DNSQueryEvent (parallel A+AAAA queries) |
syscalls/sys_enter_sendmsg | HandleSendmsg | DNSQueryEvent (single-message resolvers) |
syscalls/sys_enter_write | HandleWrite | TLSSniEvent (tcp_clienthello, Node BoringSSL path) |
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.
FileAccessEvent
Emitted byHandleOpenat for every openat(2) call inside the monitored cgroup where the opened path matches a path_filter entry. Fields beyond the header:
| Field | Type | Description |
|---|---|---|
PathName | string | Parsed null-terminated path (up to 256 bytes) |
Flags | int32 | openat flags argument (O_RDONLY, O_WRONLY, O_CREAT, …) |
Truncated | uint8 | 1 if the path was longer than 256 bytes and was truncated |
Tags | uint8 | Bitmask: EventTagInteresting (bit 0), EventTagCredAccess (bit 1) |
cred_tagged: true watched-path entry have both tag bits set, driving the red row in the UI.
ExecEvent
Emitted byHandleExecve 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 byHandleConnect (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 byHandleSendto, 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 constant | Value | Mechanism |
|---|---|---|
TLSSourceLibSSL | 1 | SSL_ctrl uprobe — SNI pre-populated in SNI[] |
TLSSourceNodeInternal | 2 | Uprobe in Node binary’s bundled TLS (planned, not yet active) — SNI pre-populated in SNI[] |
TLSSourceTCPClientHello | 3 | sendto/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.
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 viawrite(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 TCPconnect(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.
Ringbuf and drops
All events are written to a singleBPF_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.