Skip to content

Commit d0bd657

Browse files
committed
Add support for proxying and recording cross network namespace traffic
Signed-off-by: Vyom Yadav <jackhammervyom@gmail.com>
1 parent 251132f commit d0bd657

31 files changed

Lines changed: 2821 additions & 902 deletions

attestation/networktrace/bpf/bpf.go

Lines changed: 183 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"fmt"
2222
"io"
2323
"net"
24+
"os"
25+
"strconv"
2426

2527
"github.com/cilium/ebpf"
2628
"github.com/cilium/ebpf/link"
@@ -70,6 +72,14 @@ func GetCurrentNetns() (uint32, error) {
7072
return uint32(stat.Ino), nil
7173
}
7274

75+
func GetCurrentPidNs() (uint32, error) {
76+
var stat unix.Stat_t
77+
if err := unix.Stat("/proc/self/ns/pid", &stat); err != nil {
78+
return 0, fmt.Errorf("stat /proc/self/ns/pid: %w", err)
79+
}
80+
return uint32(stat.Ino), nil
81+
}
82+
7383
func ipToUint32(ipStr string) (uint32, error) {
7484
ip := net.ParseIP(ipStr)
7585
if ip == nil {
@@ -123,8 +133,13 @@ func Load(cfg LoadConfig) (*State, error) {
123133
if err := connectSpec.Variables["proxy_ip"].Set(proxyIPUint32); err != nil {
124134
return nil, fmt.Errorf("set proxy_ip in connect: %w", err)
125135
}
126-
if err := connectSpec.Variables["host_netns_inum"].Set(hostNetnsInum); err != nil {
127-
return nil, fmt.Errorf("set host_netns_inum in connect: %w", err)
136+
137+
witnessPidNsInum, err := GetCurrentPidNs()
138+
if err != nil {
139+
return nil, fmt.Errorf("get current pid ns: %w", err)
140+
}
141+
if err := connectSpec.Variables["witness_pid_ns_inum"].Set(witnessPidNsInum); err != nil {
142+
return nil, fmt.Errorf("set witness_pid_ns_inum in connect: %w", err)
128143
}
129144

130145
var connectObjs connectObjects
@@ -138,20 +153,26 @@ func Load(cfg LoadConfig) (*State, error) {
138153
return nil, fmt.Errorf("load sockops spec: %w", err)
139154
}
140155

141-
if err := sockopsSpec.Variables["host_netns_inum"].Set(hostNetnsInum); err != nil {
156+
if err := sockopsSpec.Variables["witness_pid_ns_inum"].Set(witnessPidNsInum); err != nil {
142157
connectObjs.Close()
143-
return nil, fmt.Errorf("set host_netns_inum in sockops: %w", err)
158+
return nil, fmt.Errorf("set witness_pid_ns_inum in sockops: %w", err)
144159
}
145160

146161
sockopsOpts := ebpf.CollectionOptions{
147162
MapReplacements: map[string]*ebpf.Map{
148-
"orig_dst_map": connectObjs.OrigDstMap,
149-
"orig_dst_map_v6": connectObjs.OrigDstMapV6,
150-
"tid_allowlist": connectObjs.TidAllowlist,
151-
"comm_allowlist": connectObjs.CommAllowlist,
152-
"cgroup_allowlist": connectObjs.CgroupAllowlist,
153-
"tuple_to_cookie_map": connectObjs.TupleToCookieMap,
154-
"tuple_to_cookie_map_v6": connectObjs.TupleToCookieMapV6,
163+
"orig_dst_map": connectObjs.OrigDstMap,
164+
"orig_dst_map_v6": connectObjs.OrigDstMapV6,
165+
"witness_pid_ns_tid_allowlist": connectObjs.WitnessPidNsTidAllowlist,
166+
"comm_allowlist": connectObjs.CommAllowlist,
167+
"cgroup_allowlist": connectObjs.CgroupAllowlist,
168+
"tuple_to_cookie_map": connectObjs.TupleToCookieMap,
169+
"tuple_to_cookie_map_v6": connectObjs.TupleToCookieMapV6,
170+
"control_map": connectObjs.ControlMap,
171+
"tracked_pid_ns_map": connectObjs.TrackedPidNsMap,
172+
"proxy_state_map": connectObjs.ProxyStateMap,
173+
"gate_map": connectObjs.GateMap,
174+
"pending_execs": connectObjs.PendingExecs,
175+
"witness_pid_ns_level_map": connectObjs.WitnessPidNsLevelMap,
155176
},
156177
}
157178

@@ -168,9 +189,29 @@ func Load(cfg LoadConfig) (*State, error) {
168189
return nil, fmt.Errorf("load task_tracker spec: %w", err)
169190
}
170191

192+
if err := taskTrackerSpec.Variables["witness_pid_ns_inum"].Set(witnessPidNsInum); err != nil {
193+
connectObjs.Close()
194+
sockopsObjs.Close()
195+
return nil, fmt.Errorf("set witness_pid_ns_inum in task_tracker: %w", err)
196+
}
197+
198+
// All maps are not used by every bpf program, but all import the same header
199+
// so replacing all maps to avoid redundant duplication of maps.
171200
taskTrackerOpts := ebpf.CollectionOptions{
172201
MapReplacements: map[string]*ebpf.Map{
173-
"tid_allowlist": connectObjs.TidAllowlist,
202+
"witness_pid_ns_tid_allowlist": connectObjs.WitnessPidNsTidAllowlist,
203+
"control_map": connectObjs.ControlMap,
204+
"tracked_pid_ns_map": connectObjs.TrackedPidNsMap,
205+
"proxy_state_map": connectObjs.ProxyStateMap,
206+
"gate_map": connectObjs.GateMap,
207+
"pending_execs": connectObjs.PendingExecs,
208+
"witness_pid_ns_level_map": connectObjs.WitnessPidNsLevelMap,
209+
"cgroup_allowlist": connectObjs.CgroupAllowlist,
210+
"comm_allowlist": connectObjs.CommAllowlist,
211+
"orig_dst_map": connectObjs.OrigDstMap,
212+
"orig_dst_map_v6": connectObjs.OrigDstMapV6,
213+
"tuple_to_cookie_map": connectObjs.TupleToCookieMap,
214+
"tuple_to_cookie_map_v6": connectObjs.TupleToCookieMapV6,
174215
},
175216
}
176217

@@ -197,13 +238,19 @@ func Load(cfg LoadConfig) (*State, error) {
197238
// Close() can tear them down in reverse (links first, then progs/maps).
198239
state := &State{
199240
Maps: &Maps{
200-
OrigDstMap: connectObjs.OrigDstMap,
201-
OrigDstMapV6: connectObjs.OrigDstMapV6,
202-
TIDAllowlist: connectObjs.TidAllowlist,
203-
CommAllowlist: connectObjs.CommAllowlist,
204-
CgroupAllowlist: connectObjs.CgroupAllowlist,
205-
TupleCookieMap: connectObjs.TupleToCookieMap,
206-
TupleCookieMapV6: connectObjs.TupleToCookieMapV6,
241+
OrigDstMap: connectObjs.OrigDstMap,
242+
OrigDstMapV6: connectObjs.OrigDstMapV6,
243+
WitnessPidNsTIDAllowlist: connectObjs.WitnessPidNsTidAllowlist,
244+
CommAllowlist: connectObjs.CommAllowlist,
245+
CgroupAllowlist: connectObjs.CgroupAllowlist,
246+
TupleCookieMap: connectObjs.TupleToCookieMap,
247+
TupleCookieMapV6: connectObjs.TupleToCookieMapV6,
248+
GateMap: taskTrackerObjs.GateMap,
249+
ProxyStateMap: taskTrackerObjs.ProxyStateMap,
250+
ControlMap: taskTrackerObjs.ControlMap,
251+
TrackedPidNsMap: connectObjs.TrackedPidNsMap,
252+
WitnessPidNsLevelMap: taskTrackerObjs.WitnessPidNsLevelMap,
253+
HostNetnsInum: hostNetnsInum,
207254
},
208255
}
209256

@@ -281,18 +328,62 @@ func Load(cfg LoadConfig) (*State, error) {
281328
}
282329
state.closers = append(state.closers, execveatExitLink)
283330

331+
// Network-namespace gate anchors
332+
cloneExitLink, err := link.Tracepoint("syscalls", "sys_exit_clone", taskTrackerObjs.SysExitClone, nil)
333+
if err != nil {
334+
state.Close()
335+
return nil, fmt.Errorf("attach sys_exit_clone tracepoint: %w", err)
336+
}
337+
state.closers = append(state.closers, cloneExitLink)
338+
339+
clone3ExitLink, err := link.Tracepoint("syscalls", "sys_exit_clone3", taskTrackerObjs.SysExitClone3, nil)
340+
if err != nil {
341+
state.Close()
342+
return nil, fmt.Errorf("attach sys_exit_clone3 tracepoint: %w", err)
343+
}
344+
state.closers = append(state.closers, clone3ExitLink)
345+
346+
unshareExitLink, err := link.Tracepoint("syscalls", "sys_exit_unshare", taskTrackerObjs.SysExitUnshare, nil)
347+
if err != nil {
348+
state.Close()
349+
return nil, fmt.Errorf("attach sys_exit_unshare tracepoint: %w", err)
350+
}
351+
state.closers = append(state.closers, unshareExitLink)
352+
353+
setnsExitLink, err := link.Tracepoint("syscalls", "sys_exit_setns", taskTrackerObjs.SysExitSetns, nil)
354+
if err != nil {
355+
state.Close()
356+
return nil, fmt.Errorf("attach sys_exit_setns tracepoint: %w", err)
357+
}
358+
state.closers = append(state.closers, setnsExitLink)
359+
284360
return state, nil
285361
}
286362

287363
// Maps holds references to BPF maps with typed accessor methods.
288364
type Maps struct {
289-
OrigDstMap *ebpf.Map
290-
OrigDstMapV6 *ebpf.Map
291-
TIDAllowlist *ebpf.Map
292-
CommAllowlist *ebpf.Map
293-
CgroupAllowlist *ebpf.Map
294-
TupleCookieMap *ebpf.Map
295-
TupleCookieMapV6 *ebpf.Map
365+
OrigDstMap *ebpf.Map
366+
OrigDstMapV6 *ebpf.Map
367+
WitnessPidNsTIDAllowlist *ebpf.Map
368+
CommAllowlist *ebpf.Map
369+
CgroupAllowlist *ebpf.Map
370+
TupleCookieMap *ebpf.Map
371+
TupleCookieMapV6 *ebpf.Map
372+
373+
// Execve-gate coordination maps (owned by task_tracker).
374+
GateMap *ebpf.Map
375+
ProxyStateMap *ebpf.Map
376+
ControlMap *ebpf.Map
377+
378+
// PID namespaces created by tracked processes.
379+
TrackedPidNsMap *ebpf.Map
380+
381+
// Cached absolute level of the witness PID namespace (populated by BPF).
382+
WitnessPidNsLevelMap *ebpf.Map
383+
384+
// HostNetnsInum is the inode of the network namespace the daemon runs in.
385+
// Seed allowlist entries (host PIDs from user config) are keyed against it.
386+
HostNetnsInum uint32
296387
}
297388

298389
// LookupOrigDst looks up original destination metadata for an IPv4 connection by socket cookie.
@@ -315,23 +406,72 @@ func (m *Maps) LookupOrigDstV6(sockCookie uint64) (*ConnectionMetadata, error) {
315406
return val.ToConnectionMetadata(sockCookie), nil
316407
}
317408

409+
// ConsumeOrigDst atomically looks up and removes IPv4 original destination
410+
// metadata for a socket cookie. Using LookupAndDelete avoids the lookup-then-
411+
// delete TOCTOU and prevents a recycled cookie from reading stale metadata.
412+
func (m *Maps) ConsumeOrigDst(sockCookie uint64) (*ConnectionMetadata, error) {
413+
key := connectOrigDstKey{SockCookie: sockCookie}
414+
var val connectOrigDstVal
415+
if err := m.OrigDstMap.LookupAndDelete(&key, &val); err != nil {
416+
return nil, err
417+
}
418+
return val.ToConnectionMetadata(sockCookie), nil
419+
}
420+
421+
// ConsumeOrigDstV6 atomically looks up and removes IPv6 original destination
422+
// metadata for a socket cookie.
423+
func (m *Maps) ConsumeOrigDstV6(sockCookie uint64) (*ConnectionMetadata, error) {
424+
key := connectOrigDstKeyV6{SockCookie: sockCookie}
425+
var val connectOrigDstValV6
426+
if err := m.OrigDstMapV6.LookupAndDelete(&key, &val); err != nil {
427+
return nil, err
428+
}
429+
return val.ToConnectionMetadata(sockCookie), nil
430+
}
431+
318432
// LoadUserConfig loads the user configuration into the BPF maps.
319-
// PIDs are added to tid_allowlist, and child processes are automatically
320-
// tracked via sched_process_fork tracepoint.
433+
// PIDs are added to witness_pid_ns_tid_allowlist, and child processes are
434+
// automatically tracked via sched_process_fork tracepoint.
321435
func (m *Maps) LoadUserConfig(config types.Config) error {
322436
for _, pid := range config.ObservePIDs {
323-
key := connectTidAllowlistKey{
437+
key := connectWitnessPidNsTidKey{
324438
Tid: pid,
325439
}
326-
val := connectTidAllowlistVal{
440+
val := connectWitnessPidNsTidVal{
327441
NestedAllowed: 0,
328442
}
329443
if config.ObserveChildTree {
330444
val.NestedAllowed = 1
331445
}
332446

333-
if err := m.TIDAllowlist.Put(&key, &val); err != nil {
334-
return fmt.Errorf("put pid %d in tid_allowlist: %w", pid, err)
447+
if err := m.WitnessPidNsTIDAllowlist.Put(&key, &val); err != nil {
448+
return fmt.Errorf("put pid %d in witness_pid_ns_tid_allowlist: %w", pid, err)
449+
}
450+
451+
// Daemons like dockerd and containerd are multi-threaded:
452+
// worker threads fork children, not the group leader itself.
453+
// Enumerate all threads under /proc/<pid>/task/ and add
454+
// each TID so the fork handler propagates child-tree tracking
455+
// from any thread.
456+
// It is assumed that the background daemon wouldn't spawn more threads
457+
// before the command execution which is being traced, so this is a one-time enumeration.
458+
taskDir := fmt.Sprintf("/proc/%d/task", pid)
459+
entries, err := os.ReadDir(taskDir)
460+
if err != nil {
461+
continue
462+
}
463+
for _, entry := range entries {
464+
if !entry.IsDir() {
465+
continue
466+
}
467+
tid, err := strconv.Atoi(entry.Name())
468+
if err != nil || uint32(tid) == pid {
469+
continue
470+
}
471+
key.Tid = uint32(tid)
472+
if err := m.WitnessPidNsTIDAllowlist.Put(&key, &val); err != nil {
473+
return fmt.Errorf("put tid %d (thread of pid %d) in witness_pid_ns_tid_allowlist: %w", tid, pid, err)
474+
}
335475
}
336476
}
337477

@@ -393,3 +533,14 @@ func GetBootTimeNs() (uint64, error) {
393533
}
394534
return uint64(ts.Sec)*1e9 + uint64(ts.Nsec), nil
395535
}
536+
537+
// GetMonotonicNs returns CLOCK_MONOTONIC in nanoseconds. This matches the clock
538+
// used by bpf_ktime_get_ns(), so it must be used to age gate entries whose
539+
// stop_ts_ns was stamped in eBPF.
540+
func GetMonotonicNs() (uint64, error) {
541+
var ts unix.Timespec
542+
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts); err != nil {
543+
return 0, fmt.Errorf("clock_gettime CLOCK_MONOTONIC: %w", err)
544+
}
545+
return uint64(ts.Sec)*1e9 + uint64(ts.Nsec), nil
546+
}

0 commit comments

Comments
 (0)