@xshieldai/agent-kernel 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +31 -0
- package/README.md +130 -0
- package/bin/kavachos +5 -0
- package/dist/apply-seccomp.py +625 -0
- package/dist/cgroup-egress.py +432 -0
- package/dist/kavachos.js +2687 -0
- package/package.json +52 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
# Copyright (c) 2026 Capt. Anil Sharma (rocketlang). All rights reserved.
|
|
4
|
+
# @rule:KOS-040 cgroup BPF CONNECT4/6 — egress denied before socket established
|
|
5
|
+
# @rule:KOS-041 per-session cgroup under /sys/fs/cgroup/kavachos/{session_id}/
|
|
6
|
+
# @rule:KOS-043 BPF map populated at launch — no runtime expansion permitted
|
|
7
|
+
# @rule:KOS-045 cgroup cleanup mandatory at session end
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
cgroup-egress.py — KavachOS Phase 1E: cgroup BPF egress firewall
|
|
11
|
+
|
|
12
|
+
Architecture:
|
|
13
|
+
1. Compile BPF program (clang -target bpf) — or use pre-compiled .o
|
|
14
|
+
2. Create per-session cgroup
|
|
15
|
+
3. bpftool prog load → pin to /sys/fs/bpf/kavachos/{session_id}/
|
|
16
|
+
4. bpftool cgroup attach → connect4/connect6 on session cgroup
|
|
17
|
+
5. bpftool map update → populate IP:port allowlist from egress policy JSON
|
|
18
|
+
6. Move agent PID into cgroup
|
|
19
|
+
7. Wait for agent exit → bpftool cgroup detach → cleanup
|
|
20
|
+
|
|
21
|
+
Uses clang + bpftool (available on this host). More stable than bcc for
|
|
22
|
+
cgroup_sock_addr programs due to expected_attach_type API differences across
|
|
23
|
+
bcc versions. (KOS-YK-006)
|
|
24
|
+
|
|
25
|
+
Usage: python3 cgroup-egress.py <session_id> <policy.json> <agent_pid>
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import shutil
|
|
32
|
+
import socket
|
|
33
|
+
import struct
|
|
34
|
+
import subprocess
|
|
35
|
+
import sys
|
|
36
|
+
import tempfile
|
|
37
|
+
import time
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import List, Optional, Tuple
|
|
40
|
+
|
|
41
|
+
CGROUP_ROOT = "/sys/fs/cgroup/kavachos"
|
|
42
|
+
BPF_PIN_ROOT = "/sys/fs/bpf/kavachos"
|
|
43
|
+
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
44
|
+
|
|
45
|
+
# ── BPF C source ─────────────────────────────────────────────────────────────
|
|
46
|
+
# Compiled once per session; deterministic for same key schema.
|
|
47
|
+
|
|
48
|
+
_BPF_CONNECT4_C = r"""
|
|
49
|
+
// @rule:KOS-040 cgroup BPF connect4 — deny before socket established
|
|
50
|
+
// @rule:INF-KOS-009 empty map = deny-all
|
|
51
|
+
#include <linux/bpf.h>
|
|
52
|
+
#include <bpf/bpf_helpers.h>
|
|
53
|
+
#include <bpf/bpf_endian.h>
|
|
54
|
+
|
|
55
|
+
// Key: ((ip_be32) << 32) | port_host
|
|
56
|
+
// ip_be32 is as returned by ctx->user_ip4 (big-endian u32)
|
|
57
|
+
// port_host is host-byte-order port number
|
|
58
|
+
struct {
|
|
59
|
+
__uint(type, BPF_MAP_TYPE_HASH);
|
|
60
|
+
__uint(max_entries, 512);
|
|
61
|
+
__type(key, __u64);
|
|
62
|
+
__type(value, __u8);
|
|
63
|
+
} egress_allow_v4 SEC(".maps");
|
|
64
|
+
|
|
65
|
+
SEC("cgroup/connect4")
|
|
66
|
+
int connect4(struct bpf_sock_addr *ctx) {
|
|
67
|
+
// user_ip4 is NBO; bpf_ntohl converts to host order matching Python's key
|
|
68
|
+
__u32 dst_ip = bpf_ntohl(ctx->user_ip4);
|
|
69
|
+
// user_port stores the BE16 port in the *lower* 16 bits (not upper)
|
|
70
|
+
__u16 dst_port = bpf_ntohs((__u16)ctx->user_port);
|
|
71
|
+
|
|
72
|
+
// Exact match: ip + port
|
|
73
|
+
__u64 key = ((__u64)dst_ip << 32) | dst_port;
|
|
74
|
+
__u8 *v = bpf_map_lookup_elem(&egress_allow_v4, &key);
|
|
75
|
+
if (v && *v) return 1;
|
|
76
|
+
|
|
77
|
+
// Wildcard: any port for this ip (port=0 key)
|
|
78
|
+
__u64 wildcard = (__u64)dst_ip << 32;
|
|
79
|
+
v = bpf_map_lookup_elem(&egress_allow_v4, &wildcard);
|
|
80
|
+
if (v && *v) return 1;
|
|
81
|
+
|
|
82
|
+
return 0; // deny
|
|
83
|
+
}
|
|
84
|
+
char _license[] SEC("license") = "GPL";
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
_BPF_CONNECT6_C = r"""
|
|
88
|
+
// @rule:KOS-040 cgroup BPF connect6
|
|
89
|
+
#include <linux/bpf.h>
|
|
90
|
+
#include <bpf/bpf_helpers.h>
|
|
91
|
+
#include <bpf/bpf_endian.h>
|
|
92
|
+
|
|
93
|
+
// Key: last 32 bits of IPv6 address (BE) << 32 | port_host
|
|
94
|
+
// Sufficient for ::1 loopback matching
|
|
95
|
+
struct {
|
|
96
|
+
__uint(type, BPF_MAP_TYPE_HASH);
|
|
97
|
+
__uint(max_entries, 64);
|
|
98
|
+
__type(key, __u64);
|
|
99
|
+
__type(value, __u8);
|
|
100
|
+
} egress_allow_v6 SEC(".maps");
|
|
101
|
+
|
|
102
|
+
SEC("cgroup/connect6")
|
|
103
|
+
int connect6(struct bpf_sock_addr *ctx) {
|
|
104
|
+
// user_ip6[3] is NBO; bpf_ntohl converts to host order matching Python's key
|
|
105
|
+
__u32 addr_last = bpf_ntohl(ctx->user_ip6[3]);
|
|
106
|
+
// user_port stores the BE16 port in the *lower* 16 bits (not upper)
|
|
107
|
+
__u16 dst_port = bpf_ntohs((__u16)ctx->user_port);
|
|
108
|
+
|
|
109
|
+
__u64 key = ((__u64)addr_last << 32) | dst_port;
|
|
110
|
+
__u8 *v = bpf_map_lookup_elem(&egress_allow_v6, &key);
|
|
111
|
+
if (v && *v) return 1;
|
|
112
|
+
|
|
113
|
+
__u64 wildcard = (__u64)addr_last << 32;
|
|
114
|
+
v = bpf_map_lookup_elem(&egress_allow_v6, &wildcard);
|
|
115
|
+
if (v && *v) return 1;
|
|
116
|
+
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
char _license[] SEC("license") = "GPL";
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
# ── Availability checks ───────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
def _has_tool(name: str) -> bool:
|
|
125
|
+
return shutil.which(name) is not None
|
|
126
|
+
|
|
127
|
+
def _has_cgroupv2() -> bool:
|
|
128
|
+
try:
|
|
129
|
+
with open("/proc/mounts") as f:
|
|
130
|
+
return any("cgroup2" in l for l in f)
|
|
131
|
+
except Exception:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def _is_available() -> bool:
|
|
135
|
+
ok = True
|
|
136
|
+
if not _has_tool("clang"):
|
|
137
|
+
sys.stderr.write("[kavachos:egress] clang not found — egress firewall disabled\n")
|
|
138
|
+
ok = False
|
|
139
|
+
if not _has_tool("bpftool"):
|
|
140
|
+
sys.stderr.write("[kavachos:egress] bpftool not found — egress firewall disabled\n")
|
|
141
|
+
ok = False
|
|
142
|
+
if not _has_cgroupv2():
|
|
143
|
+
sys.stderr.write("[kavachos:egress] cgroupv2 not available — egress firewall disabled\n")
|
|
144
|
+
ok = False
|
|
145
|
+
return ok
|
|
146
|
+
|
|
147
|
+
# ── Compilation ────────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
def _compile_bpf(src: str, out_path: str) -> bool:
|
|
150
|
+
"""Compile BPF C source to object file via clang."""
|
|
151
|
+
arch = subprocess.check_output(["uname", "-m"], text=True).strip()
|
|
152
|
+
include_dir = f"/usr/include/{arch}-linux-gnu"
|
|
153
|
+
result = subprocess.run(
|
|
154
|
+
["clang", "-O2", "-target", "bpf", "-g", "-c",
|
|
155
|
+
f"-I{include_dir}", "-I/usr/include/bpf",
|
|
156
|
+
"-x", "c", "-", "-o", out_path],
|
|
157
|
+
input=src.encode(),
|
|
158
|
+
capture_output=True,
|
|
159
|
+
)
|
|
160
|
+
if result.returncode != 0:
|
|
161
|
+
sys.stderr.write(f"[kavachos:egress] BPF compile error:\n{result.stderr.decode()[:400]}\n")
|
|
162
|
+
return False
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
# ── DNS resolution ────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
def _resolve(host: str) -> List[str]:
|
|
168
|
+
if re.match(r'^\d+\.\d+\.\d+\.\d+$', host) or ":" in host:
|
|
169
|
+
return [host]
|
|
170
|
+
try:
|
|
171
|
+
return list({r[4][0] for r in socket.getaddrinfo(host, None)})
|
|
172
|
+
except socket.gaierror:
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
def _ip4_be32(ip: str) -> Optional[int]:
|
|
176
|
+
try:
|
|
177
|
+
return struct.unpack(">I", socket.inet_aton(ip))[0]
|
|
178
|
+
except Exception:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def _ip6_last32_be(ip: str) -> Optional[int]:
|
|
182
|
+
try:
|
|
183
|
+
packed = socket.inet_pton(socket.AF_INET6, ip)
|
|
184
|
+
return struct.unpack(">I", packed[12:16])[0]
|
|
185
|
+
except Exception:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
# ── bpftool helpers ───────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
def _bpftool(*args, check: bool = True) -> subprocess.CompletedProcess:
|
|
191
|
+
return subprocess.run(["bpftool"] + list(args), capture_output=True, text=True, check=check)
|
|
192
|
+
|
|
193
|
+
def _prog_load(obj_path: str, pin_path: str) -> Optional[int]:
|
|
194
|
+
"""Load BPF program, pin to bpffs. Returns program ID."""
|
|
195
|
+
r = _bpftool("prog", "load", obj_path, pin_path, "pinmaps",
|
|
196
|
+
os.path.dirname(pin_path), check=False)
|
|
197
|
+
if r.returncode != 0:
|
|
198
|
+
sys.stderr.write(f"[kavachos:egress] prog load failed: {r.stderr[:300]}\n")
|
|
199
|
+
return None
|
|
200
|
+
# Get the prog ID from 'prog show pinned <path>'
|
|
201
|
+
r2 = _bpftool("prog", "show", "pinned", pin_path, "--json", check=False)
|
|
202
|
+
if r2.returncode == 0:
|
|
203
|
+
try:
|
|
204
|
+
data = json.loads(r2.stdout)
|
|
205
|
+
return data[0]["id"] if isinstance(data, list) else data.get("id")
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
def _map_id_for_prog(pin_dir: str, map_name: str) -> Optional[int]:
|
|
211
|
+
"""Get the map ID for a pinned map."""
|
|
212
|
+
pin = os.path.join(pin_dir, map_name)
|
|
213
|
+
r = _bpftool("map", "show", "pinned", pin, "--json", check=False)
|
|
214
|
+
if r.returncode == 0:
|
|
215
|
+
try:
|
|
216
|
+
data = json.loads(r.stdout)
|
|
217
|
+
return (data[0] if isinstance(data, list) else data).get("id")
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def _map_update(map_id: int, key_u64: int, value: int) -> None:
|
|
223
|
+
"""Update BPF hash map: key (u64 little-endian bytes) → value (u8)."""
|
|
224
|
+
# bpftool map update id <id> key hex <8 LE bytes> value hex <1 byte>
|
|
225
|
+
key_bytes = " ".join(f"{(key_u64 >> (i*8)) & 0xff:02x}" for i in range(8))
|
|
226
|
+
val_hex = f"{value:02x}"
|
|
227
|
+
r = _bpftool("map", "update", "id", str(map_id), "key", "hex",
|
|
228
|
+
*key_bytes.split(), "value", "hex", val_hex, check=False)
|
|
229
|
+
if r.returncode != 0:
|
|
230
|
+
sys.stderr.write(f"[kavachos:egress] map update failed for key {key_u64:#018x}: {r.stderr[:100]}\n")
|
|
231
|
+
|
|
232
|
+
def _cgroup_attach(cgroup_path: str, prog_id: int, attach_type: str) -> bool:
|
|
233
|
+
r = _bpftool("cgroup", "attach", cgroup_path, attach_type, "id", str(prog_id), check=False)
|
|
234
|
+
if r.returncode != 0:
|
|
235
|
+
sys.stderr.write(f"[kavachos:egress] cgroup attach {attach_type} failed: {r.stderr[:200]}\n")
|
|
236
|
+
return False
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
def _cgroup_detach(cgroup_path: str, prog_id: int, attach_type: str) -> None:
|
|
240
|
+
_bpftool("cgroup", "detach", cgroup_path, attach_type, "id", str(prog_id), check=False)
|
|
241
|
+
|
|
242
|
+
# ── Cgroup management ─────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
def _create_cgroup(session_id: str) -> str:
|
|
245
|
+
path = os.path.join(CGROUP_ROOT, session_id)
|
|
246
|
+
os.makedirs(path, exist_ok=True)
|
|
247
|
+
return path
|
|
248
|
+
|
|
249
|
+
def _move_pid(cgroup_path: str, pid: int) -> None:
|
|
250
|
+
with open(os.path.join(cgroup_path, "cgroup.procs"), "w") as f:
|
|
251
|
+
f.write(str(pid))
|
|
252
|
+
|
|
253
|
+
def _destroy_cgroup(session_id: str) -> None:
|
|
254
|
+
path = os.path.join(CGROUP_ROOT, session_id)
|
|
255
|
+
if not os.path.exists(path):
|
|
256
|
+
return
|
|
257
|
+
# Evacuate any remaining processes to parent cgroup
|
|
258
|
+
try:
|
|
259
|
+
child_procs = os.path.join(path, "cgroup.procs")
|
|
260
|
+
parent_procs = os.path.join(CGROUP_ROOT, "cgroup.procs")
|
|
261
|
+
with open(child_procs) as f:
|
|
262
|
+
pids = f.read().strip().split()
|
|
263
|
+
for p in pids:
|
|
264
|
+
try:
|
|
265
|
+
with open(parent_procs, "w") as f:
|
|
266
|
+
f.write(p)
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
except Exception:
|
|
270
|
+
pass
|
|
271
|
+
try:
|
|
272
|
+
os.rmdir(path)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
sys.stderr.write(f"[kavachos:egress] cgroup rmdir warning: {e}\n")
|
|
275
|
+
|
|
276
|
+
# ── Main session class ─────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
class EgressSession:
|
|
279
|
+
"""Lifetime: agent session. Load → attach → populate → wait → cleanup."""
|
|
280
|
+
|
|
281
|
+
def __init__(self, session_id: str):
|
|
282
|
+
self.session_id = session_id
|
|
283
|
+
self.cgroup_path = os.path.join(CGROUP_ROOT, session_id)
|
|
284
|
+
self.pin_dir = os.path.join(BPF_PIN_ROOT, session_id)
|
|
285
|
+
self.prog_id_v4 = -1
|
|
286
|
+
self.prog_id_v6 = -1
|
|
287
|
+
self._tmp = [] # temp files to clean up
|
|
288
|
+
|
|
289
|
+
def setup(self, policy: dict, agent_pid: int) -> bool:
|
|
290
|
+
os.makedirs(CGROUP_ROOT, exist_ok=True)
|
|
291
|
+
os.makedirs(BPF_PIN_ROOT, exist_ok=True)
|
|
292
|
+
os.makedirs(self.pin_dir, exist_ok=True)
|
|
293
|
+
_create_cgroup(self.session_id)
|
|
294
|
+
|
|
295
|
+
# Compile BPF programs to /tmp — bpffs doesn't support regular file writes
|
|
296
|
+
v4_obj = f"/tmp/kavachos-{self.session_id}-connect4.o"
|
|
297
|
+
v6_obj = f"/tmp/kavachos-{self.session_id}-connect6.o"
|
|
298
|
+
self._tmp.extend([v4_obj, v6_obj])
|
|
299
|
+
|
|
300
|
+
if not _compile_bpf(_BPF_CONNECT4_C, v4_obj):
|
|
301
|
+
return False
|
|
302
|
+
_compile_bpf(_BPF_CONNECT6_C, v6_obj) # v6 failure is non-fatal
|
|
303
|
+
|
|
304
|
+
# Load + pin
|
|
305
|
+
v4_pin = os.path.join(self.pin_dir, "connect4")
|
|
306
|
+
self.prog_id_v4 = _prog_load(v4_obj, v4_pin)
|
|
307
|
+
if self.prog_id_v4 is None:
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
if os.path.exists(v6_obj):
|
|
311
|
+
v6_pin = os.path.join(self.pin_dir, "connect6")
|
|
312
|
+
self.prog_id_v6 = _prog_load(v6_obj, v6_pin) or -1
|
|
313
|
+
|
|
314
|
+
# Attach to cgroup
|
|
315
|
+
if not _cgroup_attach(self.cgroup_path, self.prog_id_v4, "connect4"):
|
|
316
|
+
return False
|
|
317
|
+
if self.prog_id_v6 > 0:
|
|
318
|
+
_cgroup_attach(self.cgroup_path, self.prog_id_v6, "connect6")
|
|
319
|
+
|
|
320
|
+
# Populate allowlist maps
|
|
321
|
+
self._populate(policy)
|
|
322
|
+
|
|
323
|
+
# Move agent into cgroup
|
|
324
|
+
try:
|
|
325
|
+
_move_pid(self.cgroup_path, agent_pid)
|
|
326
|
+
except Exception as e:
|
|
327
|
+
sys.stderr.write(f"[kavachos:egress] move_pid failed: {e}\n")
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
allow_len = len(policy.get("allow", []))
|
|
331
|
+
sys.stderr.write(
|
|
332
|
+
f"[kavachos:egress] Phase 1E active: session={self.session_id} "
|
|
333
|
+
f"pid={agent_pid} hosts={allow_len}\n"
|
|
334
|
+
)
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
def _populate(self, policy: dict) -> None:
|
|
338
|
+
"""Fill BPF maps with allowlist entries. @rule:KOS-043"""
|
|
339
|
+
map_v4 = _map_id_for_prog(self.pin_dir, "egress_allow_v4")
|
|
340
|
+
map_v6 = _map_id_for_prog(self.pin_dir, "egress_allow_v6") if self.prog_id_v6 > 0 else None
|
|
341
|
+
|
|
342
|
+
for entry in policy.get("allow", []):
|
|
343
|
+
host = entry.get("host", "")
|
|
344
|
+
port = entry.get("port", 0)
|
|
345
|
+
note = entry.get("note", host)
|
|
346
|
+
ips = _resolve(host)
|
|
347
|
+
if not ips:
|
|
348
|
+
sys.stderr.write(f"[kavachos:egress] WARNING: unresolvable host skipped: {host}\n")
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
for ip in ips:
|
|
352
|
+
be32 = _ip4_be32(ip)
|
|
353
|
+
if be32 is not None and map_v4 is not None:
|
|
354
|
+
# Key: ip_be32 << 32 | port_host (stored little-endian for bpftool)
|
|
355
|
+
key = (be32 << 32) | port
|
|
356
|
+
_map_update(map_v4, key, 1)
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
last32 = _ip6_last32_be(ip)
|
|
360
|
+
if last32 is not None and map_v6 is not None:
|
|
361
|
+
key = (last32 << 32) | port
|
|
362
|
+
_map_update(map_v6, key, 1)
|
|
363
|
+
|
|
364
|
+
def cleanup(self) -> None:
|
|
365
|
+
"""Detach BPF, remove cgroup. @rule:KOS-045"""
|
|
366
|
+
if self.prog_id_v4 > 0:
|
|
367
|
+
_cgroup_detach(self.cgroup_path, self.prog_id_v4, "connect4")
|
|
368
|
+
if self.prog_id_v6 > 0:
|
|
369
|
+
_cgroup_detach(self.cgroup_path, self.prog_id_v6, "connect6")
|
|
370
|
+
_destroy_cgroup(self.session_id)
|
|
371
|
+
# Remove pinned programs from bpffs
|
|
372
|
+
for name in ["connect4", "connect6", "egress_allow_v4", "egress_allow_v6"]:
|
|
373
|
+
p = os.path.join(self.pin_dir, name)
|
|
374
|
+
if os.path.exists(p):
|
|
375
|
+
try:
|
|
376
|
+
os.unlink(p)
|
|
377
|
+
except Exception:
|
|
378
|
+
pass
|
|
379
|
+
try:
|
|
380
|
+
os.rmdir(self.pin_dir)
|
|
381
|
+
except Exception:
|
|
382
|
+
pass
|
|
383
|
+
# Remove tmp .o files
|
|
384
|
+
for tmp in self._tmp:
|
|
385
|
+
if os.path.exists(tmp):
|
|
386
|
+
try:
|
|
387
|
+
os.unlink(tmp)
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
sys.stderr.write(f"[kavachos:egress] Session {self.session_id} cleaned up\n")
|
|
391
|
+
|
|
392
|
+
# ── Entry point ────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
def main() -> None:
|
|
395
|
+
if len(sys.argv) < 4:
|
|
396
|
+
sys.stderr.write("Usage: cgroup-egress.py <session_id> <policy.json> <agent_pid>\n")
|
|
397
|
+
sys.exit(1)
|
|
398
|
+
|
|
399
|
+
session_id = sys.argv[1]
|
|
400
|
+
policy_path = sys.argv[2]
|
|
401
|
+
agent_pid = int(sys.argv[3])
|
|
402
|
+
|
|
403
|
+
if not _is_available():
|
|
404
|
+
sys.exit(0) # graceful degradation — log already written above
|
|
405
|
+
|
|
406
|
+
with open(policy_path) as f:
|
|
407
|
+
policy = json.load(f)
|
|
408
|
+
|
|
409
|
+
sess = EgressSession(session_id)
|
|
410
|
+
ok = sess.setup(policy, agent_pid)
|
|
411
|
+
if not ok:
|
|
412
|
+
sess.cleanup()
|
|
413
|
+
sys.exit(0) # graceful degradation
|
|
414
|
+
|
|
415
|
+
# Wait for agent to exit.
|
|
416
|
+
# The agent is typically NOT our direct child (runner.ts spawned it) so
|
|
417
|
+
# waitpid() raises ChildProcessError immediately — fall back to PID-probe loop.
|
|
418
|
+
try:
|
|
419
|
+
os.waitpid(agent_pid, 0)
|
|
420
|
+
except Exception:
|
|
421
|
+
while True:
|
|
422
|
+
try:
|
|
423
|
+
os.kill(agent_pid, 0) # raises ProcessLookupError when PID gone
|
|
424
|
+
time.sleep(0.5)
|
|
425
|
+
except (ProcessLookupError, PermissionError):
|
|
426
|
+
break
|
|
427
|
+
|
|
428
|
+
sess.cleanup()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
if __name__ == "__main__":
|
|
432
|
+
main()
|