@@ -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+
7383func 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.
288364type 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.
321435func (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