kento-core 1.6.0.dev1__py3-none-any.whl

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.
kento/hook.sh ADDED
@@ -0,0 +1,525 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ NAME="@@NAME@@"
5
+ CONTAINER_DIR="@@CONTAINER_DIR@@"
6
+ STATE_DIR="@@STATE_DIR@@"
7
+ LAYERS="@@LAYERS@@"
8
+ HOOK_TYPE="${LXC_HOOK_TYPE:-$3}"
9
+
10
+ # ---------------------------------------------------------------------------
11
+ # NAT backend resolution.
12
+ #
13
+ # Port forwarding installs DNAT/masquerade rules; the host may ship either
14
+ # nftables (`nft`) or legacy iptables (`iptables`). Prefer nft when present
15
+ # (kento's historical backend, isolated `ip kento` table), else fall back to
16
+ # iptables. If NEITHER is available we cannot install rules — warn and skip
17
+ # WITHOUT aborting the start-host hook (which runs under `set -eu`; an
18
+ # unguarded missing-binary call would exit 127 and fail instance start).
19
+ #
20
+ # Echoes `nft`, `iptables`, or `` (empty = none). Probes are guarded so a
21
+ # missing binary never trips `set -e`.
22
+ # ---------------------------------------------------------------------------
23
+ kento_nat_backend() {
24
+ if command -v nft >/dev/null 2>&1; then
25
+ echo nft
26
+ elif command -v iptables >/dev/null 2>&1; then
27
+ echo iptables
28
+ else
29
+ echo ""
30
+ fi
31
+ }
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Port forwarding setup — idempotent.
35
+ #
36
+ # Called from `start-host` for both plain LXC and pve-lxc. For pve-lxc,
37
+ # start-host fires via PVE's snippets hookscript at post-start phase (PVE's
38
+ # config parser strips `lxc.hook.start-host:`, so we route it through a
39
+ # snippets wrapper that execs this script with $3="start-host").
40
+ #
41
+ # Guard: once $CONTAINER_DIR/kento-portfwd-active exists the hook has already
42
+ # installed rules for this boot, so subsequent invocations are no-ops.
43
+ # ---------------------------------------------------------------------------
44
+ setup_port_forwarding() {
45
+ CONTAINER_ID_ARG="$1" # container name (plain LXC) or VMID (PVE)
46
+
47
+ PORT_FILE="$CONTAINER_DIR/kento-port"
48
+ [ -f "$PORT_FILE" ] || return 0
49
+
50
+ # Already configured for this boot (another hook point beat us to it)?
51
+ [ -f "$CONTAINER_DIR/kento-portfwd-active" ] && return 0
52
+
53
+ PORT_SPEC=$(cat "$PORT_FILE" | tr -d '[:space:]')
54
+
55
+ # F19: validate PORT_SPEC before feeding into nft. kento's CLI
56
+ # validates --port at create time, but kento-port is a plain file on
57
+ # disk; a tampered/corrupted value here would otherwise flow straight
58
+ # into `nft add rule ... dport "$HOST_PORT"` as shell-expanded text.
59
+ # Require strictly HOST:GUEST where both are integers in [1, 65535].
60
+ _kento_port_valid=1
61
+ case "$PORT_SPEC" in
62
+ *[!0-9:]*) _kento_port_valid=0 ;;
63
+ *:*:*) _kento_port_valid=0 ;;
64
+ :*|*:) _kento_port_valid=0 ;;
65
+ *:*) : ;;
66
+ *) _kento_port_valid=0 ;;
67
+ esac
68
+ if [ "$_kento_port_valid" -eq 0 ]; then
69
+ echo "kento-hook: invalid kento-port $PORT_SPEC (expected HOST:GUEST integers) -- skipping port forwarding" >&2
70
+ return 0
71
+ fi
72
+ HOST_PORT="${PORT_SPEC%%:*}"
73
+ GUEST_PORT="${PORT_SPEC##*:}"
74
+ for _kp in "$HOST_PORT" "$GUEST_PORT"; do
75
+ if ! [ "$_kp" -ge 1 ] 2>/dev/null || ! [ "$_kp" -le 65535 ] 2>/dev/null; then
76
+ echo "kento-hook: port $_kp out of range 1..65535 in kento-port -- skipping port forwarding" >&2
77
+ return 0
78
+ fi
79
+ done
80
+
81
+ # Discover container IP: static (kento-net) fast path, or DHCP
82
+ # discovery via lxc-info in a detached background worker.
83
+ #
84
+ # DEADLOCK NOTE: the start-host hook runs in the LXC monitor process.
85
+ # `lxc-info -iH` opens a control-socket RPC to that same monitor —
86
+ # which is blocked waiting for this hook to return. So calling
87
+ # lxc-info synchronously here hangs forever and kento start never
88
+ # exits. Always detach the DHCP path with setsid + nohup so the
89
+ # worker survives after the hook returns and can talk to the
90
+ # (by then free) monitor.
91
+ CONTAINER_IP=""
92
+ NET_FILE="$CONTAINER_DIR/kento-net"
93
+ if [ -f "$NET_FILE" ]; then
94
+ CONTAINER_IP=$(grep '^ip=' "$NET_FILE" | head -1 | sed 's/^ip=//' | cut -d/ -f1)
95
+ fi
96
+
97
+ # Enable route_localnet so the kernel will route 127.0.0.0/8 traffic
98
+ # out through the bridge interface toward the container. Without this
99
+ # the kernel drops packets with saddr 127.0.0.1 on non-loopback
100
+ # interfaces, so `ssh -p <host_port> localhost` times out even when
101
+ # the DNAT and masquerade rules are correct. Debian/PVE defaults to 0.
102
+ echo 1 > /proc/sys/net/ipv4/conf/all/route_localnet 2>/dev/null || true
103
+
104
+ # Resolve the NAT backend once. If neither nft nor iptables is present we
105
+ # cannot install rules; record an error marker and bail WITHOUT aborting
106
+ # start (the unguarded rule installs below would otherwise hit exit 127
107
+ # under `set -eu` and fail the whole start-host hook).
108
+ BACKEND=$(kento_nat_backend)
109
+ if [ -z "$BACKEND" ]; then
110
+ echo "kento: neither nft nor iptables found; port forwarding for $NAME not active" \
111
+ > "$CONTAINER_DIR/kento-portfwd-error" 2>&1
112
+ echo "kento-hook: neither nft nor iptables available -- skipping port forwarding for $NAME" >&2
113
+ return 0
114
+ fi
115
+
116
+ if [ "$BACKEND" = nft ]; then
117
+ # Ensure kento nftables table and base chains exist (idempotent)
118
+ nft add table ip kento 2>/dev/null || true
119
+ nft 'add chain ip kento prerouting { type nat hook prerouting priority dstnat; policy accept; }' 2>/dev/null || true
120
+ nft 'add chain ip kento output { type nat hook output priority dstnat; policy accept; }' 2>/dev/null || true
121
+ nft 'add chain ip kento postrouting { type nat hook postrouting priority srcnat; policy accept; }' 2>/dev/null || true
122
+ fi
123
+
124
+ if [ -n "$CONTAINER_IP" ]; then
125
+ if [ "$BACKEND" = nft ]; then
126
+ nft add rule ip kento prerouting tcp dport "$HOST_PORT" dnat to "${CONTAINER_IP}:${GUEST_PORT}" comment "\"kento:${NAME}\""
127
+ nft add rule ip kento output tcp dport "$HOST_PORT" dnat to "${CONTAINER_IP}:${GUEST_PORT}" comment "\"kento:${NAME}\""
128
+ nft add rule ip kento postrouting ip saddr 127.0.0.0/8 ip daddr "${CONTAINER_IP}" tcp dport "$GUEST_PORT" masquerade comment "\"kento:${NAME}\""
129
+ else
130
+ # iptables fallback: append to the standard nat-table chains.
131
+ # Rules are comment-tagged "kento:NAME" so post-stop can find and
132
+ # delete exactly the rules this instance installed.
133
+ iptables -t nat -A PREROUTING -p tcp --dport "$HOST_PORT" -j DNAT --to-destination "${CONTAINER_IP}:${GUEST_PORT}" -m comment --comment "kento:${NAME}"
134
+ iptables -t nat -A OUTPUT -p tcp --dport "$HOST_PORT" -j DNAT --to-destination "${CONTAINER_IP}:${GUEST_PORT}" -m comment --comment "kento:${NAME}"
135
+ iptables -t nat -A POSTROUTING -s 127.0.0.0/8 -d "${CONTAINER_IP}" -p tcp --dport "$GUEST_PORT" -j MASQUERADE -m comment --comment "kento:${NAME}"
136
+ fi
137
+ echo "${HOST_PORT}:${GUEST_PORT}:${CONTAINER_IP}" > "$CONTAINER_DIR/kento-portfwd-active"
138
+ echo "$BACKEND" > "$CONTAINER_DIR/kento-portfwd-backend"
139
+ else
140
+ # DHCP path — detach a worker that polls lxc-info and installs
141
+ # the rules once the guest has an address. The resolved backend is
142
+ # baked into the worker (host NAT state is stable within a boot, so
143
+ # the worker need not re-detect).
144
+ CONTAINER_ID="${LXC_NAME:-$CONTAINER_ID_ARG}"
145
+ WORKER="$CONTAINER_DIR/kento-portfwd-worker.sh"
146
+ cat > "$WORKER" <<WORKER_EOF
147
+ #!/bin/sh
148
+ CID="$CONTAINER_ID"
149
+ NAME="$NAME"
150
+ HOST_PORT="$HOST_PORT"
151
+ GUEST_PORT="$GUEST_PORT"
152
+ CONTAINER_DIR="$CONTAINER_DIR"
153
+ BACKEND="$BACKEND"
154
+ # Bail out if rules already installed (e.g. another hook raced ahead).
155
+ [ -f "\$CONTAINER_DIR/kento-portfwd-active" ] && exit 0
156
+ # Bail out if the container was stopped while we were being launched —
157
+ # post-stop drops a cancel sentinel at the very start of its teardown.
158
+ [ -f "\$CONTAINER_DIR/kento-portfwd-cancel" ] && exit 0
159
+ IP=""
160
+ TRIES=0
161
+ # Retry for up to ~30s; DHCP can take a while on slow guests.
162
+ # IPv4-only: our NAT rules live in the ip family, so we must ignore
163
+ # IPv6 addresses (which lxc-info prints interleaved with IPv4 ones).
164
+ while [ "\$TRIES" -lt 30 ]; do
165
+ # If the container stopped mid-discovery, post-stop wrote a cancel
166
+ # sentinel; abandon the poll WITHOUT installing rules or a marker.
167
+ [ -f "\$CONTAINER_DIR/kento-portfwd-cancel" ] && exit 0
168
+ IP=\$(lxc-info -n "\$CID" -iH 2>/dev/null \\
169
+ | grep -E '^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+\$' | head -1)
170
+ [ -n "\$IP" ] && break
171
+ sleep 1
172
+ TRIES=\$((TRIES + 1))
173
+ done
174
+ if [ -z "\$IP" ]; then
175
+ echo "kento: could not determine IPv4 address for \$NAME; port forwarding not active" \\
176
+ > "\$CONTAINER_DIR/kento-portfwd-error" 2>&1
177
+ exit 0
178
+ fi
179
+ # Re-check in case start-host raced ahead while we polled.
180
+ [ -f "\$CONTAINER_DIR/kento-portfwd-active" ] && exit 0
181
+ # Final cancel check immediately BEFORE installing any rule: this closes
182
+ # the race where the container is stopped after we obtained an IP but
183
+ # before we commit rules. Without this, a late worker could install
184
+ # orphan DNAT/masquerade rules into the shared table for a dead container.
185
+ [ -f "\$CONTAINER_DIR/kento-portfwd-cancel" ] && exit 0
186
+ if [ "\$BACKEND" = nft ]; then
187
+ nft add rule ip kento prerouting tcp dport "\$HOST_PORT" \\
188
+ dnat to "\$IP:\$GUEST_PORT" comment "\"kento:\$NAME\""
189
+ nft add rule ip kento output tcp dport "\$HOST_PORT" \\
190
+ dnat to "\$IP:\$GUEST_PORT" comment "\"kento:\$NAME\""
191
+ nft add rule ip kento postrouting ip saddr 127.0.0.0/8 \\
192
+ ip daddr "\$IP" tcp dport "\$GUEST_PORT" \\
193
+ masquerade comment "\"kento:\$NAME\""
194
+ else
195
+ iptables -t nat -A PREROUTING -p tcp --dport "\$HOST_PORT" \\
196
+ -j DNAT --to-destination "\$IP:\$GUEST_PORT" \\
197
+ -m comment --comment "kento:\$NAME"
198
+ iptables -t nat -A OUTPUT -p tcp --dport "\$HOST_PORT" \\
199
+ -j DNAT --to-destination "\$IP:\$GUEST_PORT" \\
200
+ -m comment --comment "kento:\$NAME"
201
+ iptables -t nat -A POSTROUTING -s 127.0.0.0/8 -d "\$IP" \\
202
+ -p tcp --dport "\$GUEST_PORT" -j MASQUERADE \\
203
+ -m comment --comment "kento:\$NAME"
204
+ fi
205
+ echo "\$HOST_PORT:\$GUEST_PORT:\$IP" > "\$CONTAINER_DIR/kento-portfwd-active"
206
+ echo "\$BACKEND" > "\$CONTAINER_DIR/kento-portfwd-backend"
207
+ WORKER_EOF
208
+ chmod +x "$WORKER"
209
+ # A fresh launch supersedes any stale cancel sentinel from a prior
210
+ # boot, so the new worker isn't aborted on its first check.
211
+ rm -f "$CONTAINER_DIR/kento-portfwd-cancel" 2>/dev/null || true
212
+ # setsid + redirection detaches fully from the monitor process
213
+ # group, so lxc-start can reap its children and return. setsid makes
214
+ # the worker its own process-group leader, so its PID is also the
215
+ # PGID — post-stop signals the whole group to reap a still-polling
216
+ # worker promptly.
217
+ setsid sh "$WORKER" </dev/null >/dev/null 2>&1 &
218
+ echo "$!" > "$CONTAINER_DIR/kento-portfwd-pid" 2>/dev/null || true
219
+ fi
220
+ }
221
+
222
+ case "$HOOK_TYPE" in
223
+ pre-start|pre-mount|mount)
224
+ # Validate layer paths still exist (image may have changed)
225
+ IFS=:
226
+ for dir in $LAYERS; do
227
+ if [ ! -d "$dir" ]; then
228
+ echo "kento-hook: error: layer path missing: $dir" >&2
229
+ echo "kento-hook: image may have changed. Run: kento scrub $NAME" >&2
230
+ exit 1
231
+ fi
232
+ done
233
+ unset IFS
234
+
235
+ # pre-mount (PVE privileged): mount at lxc.rootfs.path source so LXC picks it up
236
+ # pre-start (PVE unprivileged): the only hook in the host INITIAL ns as
237
+ # real root — pre-mount/mount run in the child userns where idmapped
238
+ # bind mounts fail with EPERM (run 19). LXC_ROOTFS_PATH is set here.
239
+ # pre-start (plain LXC): mount at $CONTAINER_DIR/rootfs directly
240
+ # All cases use $LXC_ROOTFS_PATH when available, else $CONTAINER_DIR/rootfs
241
+ ROOTFS="${LXC_ROOTFS_PATH:-$CONTAINER_DIR/rootfs}"
242
+
243
+ mkdir -p "$STATE_DIR/upper" "$STATE_DIR/work" "$ROOTFS"
244
+ export LIBMOUNT_FORCE_MOUNT2=always
245
+
246
+ if [ -f "$CONTAINER_DIR/kento-unprivileged" ]; then
247
+ # ---------------------------------------------------------------------------
248
+ # Unprivileged (per-layer idmap) path — mainline kernel 5.19+, util-linux 2.40+
249
+ #
250
+ # For each lowerdir Lᵢ, create an idmapped bind mount so the
251
+ # on-disk uid/gid 0 appears as <BASE> (the container's host uid/gid
252
+ # base) through the overlay. The overlay is then mounted over the
253
+ # idmapped lowers with userxattr,index=off,metacopy=off so
254
+ # container-root (host-<BASE>) can read/write the rootfs.
255
+ #
256
+ # Idmap range acquisition, in precedence order:
257
+ # 1. Parse `lxc.idmap = u 0 <BASE> <COUNT>` from $LXC_CONFIG_FILE.
258
+ # This follows PVE's ACTUAL value (and plain-lxc, where kento
259
+ # emits the line at create). It's authoritative WHEN present.
260
+ # 2. Fall back to the $CONTAINER_DIR/kento-idmap-range state file
261
+ # ("<BASE> <COUNT>"), written at create following PVE's range.
262
+ # For unprivileged pve-lxc the assembly runs in pre-start, where
263
+ # PVE may not have populated lxc.idmap into the runtime config
264
+ # yet — the state file makes us independent of that ordering.
265
+ # Fail closed only if NEITHER source yields a valid range.
266
+ # ---------------------------------------------------------------------------
267
+
268
+ BASE=""
269
+ COUNT=""
270
+
271
+ # Source 1: parse the first `lxc.idmap = u 0 <BASE> <COUNT>` line
272
+ # from the runtime config (format: lxc.idmap = u 0 BASE COUNT).
273
+ if [ -n "${LXC_CONFIG_FILE:-}" ] && [ -r "$LXC_CONFIG_FILE" ]; then
274
+ _idmap_line=$(grep -m1 '^[[:space:]]*lxc\.idmap[[:space:]]*=[[:space:]]*u[[:space:]]' "$LXC_CONFIG_FILE" 2>/dev/null || true)
275
+ if [ -n "$_idmap_line" ]; then
276
+ # Strip to "0 BASE COUNT" after the `u` prefix, skip the
277
+ # container-side id (0), take BASE and COUNT.
278
+ _idmap_rest=$(printf '%s' "$_idmap_line" | sed 's/^[[:space:]]*lxc\.idmap[[:space:]]*=[[:space:]]*u[[:space:]]*//')
279
+ BASE=$(printf '%s' "$_idmap_rest" | awk '{print $2}')
280
+ COUNT=$(printf '%s' "$_idmap_rest" | awk '{print $3}')
281
+ fi
282
+ fi
283
+
284
+ # Source 2 (fallback): the kento-idmap-range state file
285
+ # ("<BASE> <COUNT>"), used when LXC_CONFIG_FILE carries no idmap
286
+ # line yet (PVE pre-start ordering).
287
+ if [ -z "$BASE" ] || [ -z "$COUNT" ]; then
288
+ _idmap_range_file="$CONTAINER_DIR/kento-idmap-range"
289
+ if [ -r "$_idmap_range_file" ]; then
290
+ _idmap_range=$(head -n1 "$_idmap_range_file" 2>/dev/null || true)
291
+ BASE=$(printf '%s' "$_idmap_range" | awk '{print $1}')
292
+ COUNT=$(printf '%s' "$_idmap_range" | awk '{print $2}')
293
+ fi
294
+ fi
295
+
296
+ if [ -z "$BASE" ] || [ -z "$COUNT" ]; then
297
+ echo "kento-hook: error: unprivileged mode could not determine the idmap range" >&2
298
+ echo "kento-hook: no 'lxc.idmap = u 0 BASE COUNT' line in '${LXC_CONFIG_FILE:-<unset>}'" >&2
299
+ echo "kento-hook: and no usable $CONTAINER_DIR/kento-idmap-range state file" >&2
300
+ exit 1
301
+ fi
302
+
303
+ # Validate BASE and COUNT are integers
304
+ case "$BASE" in
305
+ ''|*[!0-9]*) echo "kento-hook: error: idmap BASE is not a non-negative integer: '$BASE'" >&2; exit 1 ;;
306
+ esac
307
+ case "$COUNT" in
308
+ ''|*[!0-9]*) echo "kento-hook: error: idmap COUNT is not a non-negative integer: '$COUNT'" >&2; exit 1 ;;
309
+ esac
310
+
311
+ # Create idmapped bind mounts for each lowerdir, preserving order.
312
+ # $STATE_DIR/idmap/0, /1, /2, ... correspond to $LAYERS order.
313
+ mkdir -p "$STATE_DIR/idmap"
314
+ IDLAYERS=""
315
+ _idx=0
316
+ IFS=:
317
+ for _layer in $LAYERS; do
318
+ _idmap_target="$STATE_DIR/idmap/$_idx"
319
+ mkdir -p "$_idmap_target"
320
+ # Idempotent: skip re-binding if already a mountpoint.
321
+ if ! mountpoint -q "$_idmap_target" 2>/dev/null; then
322
+ mount --bind \
323
+ -o "X-mount.idmap=u:0:${BASE}:${COUNT} g:0:${BASE}:${COUNT}" \
324
+ "$_layer" "$_idmap_target"
325
+ fi
326
+ if [ -z "$IDLAYERS" ]; then
327
+ IDLAYERS="$_idmap_target"
328
+ else
329
+ IDLAYERS="$IDLAYERS:$_idmap_target"
330
+ fi
331
+ _idx=$((_idx + 1))
332
+ done
333
+ unset IFS
334
+
335
+ # chown upper and work so container-root (host-$BASE) can write them.
336
+ chown "${BASE}:${BASE}" "$STATE_DIR/upper" "$STATE_DIR/work"
337
+
338
+ # Mount the overlay over the idmapped lowers.
339
+ # userxattr: required for overlay-on-idmapped-lowers (kernel 5.19+).
340
+ # index=off,metacopy=off: avoid inode-index and metacopy features
341
+ # that are incompatible with the per-layer idmap path.
342
+ # Idempotency / PVE-coexistence guard. A plain `mountpoint -q` check
343
+ # is WRONG under PVE: lxc-pve-prestart-hook runs before this hook and
344
+ # bind-mounts the dir rootfs, so $ROOTFS is already a (non-overlay)
345
+ # mountpoint — a mountpoint guard would skip our overlay and leave the
346
+ # container with an empty rootfs. Skip ONLY when $ROOTFS is already
347
+ # OUR overlay (genuine LXC retry / hook re-entry); otherwise mount the
348
+ # overlay (on plain LXC: over nothing; on PVE: on top of PVE's bind,
349
+ # which PVE's later rootfs handling transparently picks up).
350
+ _rootfs_fstype=$(findmnt -fno FSTYPE "$ROOTFS" 2>/dev/null | head -1 || true)
351
+ if [ "$_rootfs_fstype" != "overlay" ]; then
352
+ mount -t overlay overlay \
353
+ -o "lowerdir=$IDLAYERS,upperdir=$STATE_DIR/upper,workdir=$STATE_DIR/work,userxattr,index=off,metacopy=off" \
354
+ "$ROOTFS"
355
+ fi
356
+ else
357
+ # Privileged path — unchanged.
358
+ mount -t overlay overlay \
359
+ -o "lowerdir=$LAYERS,upperdir=$STATE_DIR/upper,workdir=$STATE_DIR/work" \
360
+ "$ROOTFS"
361
+ fi
362
+
363
+ # Guest config injection — shared with VM / PVE-VM modes.
364
+ sh "$CONTAINER_DIR/kento-inject.sh" "$ROOTFS" "$CONTAINER_DIR"
365
+ ;;
366
+ start-host)
367
+ # Container identifier: plain LXC with hook.version=1 passes args via
368
+ # env vars only ($LXC_NAME); pve-lxc arrives via the snippets wrapper
369
+ # which passes VMID as $1. Handle both safely under `set -u`.
370
+ CONTAINER_ID="${LXC_NAME:-${1:-}}"
371
+
372
+ # pve-lxc only: propagate memory/cores limits into the inner `ns` cgroup
373
+ # so the guest sees its own limit at /sys/fs/cgroup/memory.max instead
374
+ # of "max". PVE nests the container cgroup via
375
+ # `lxc.cgroup.dir.container.inner = ns`, so `lxc.cgroup2.*` keys land
376
+ # on the outer (accounting) cgroup at /sys/fs/cgroup/lxc/<vmid>/ while
377
+ # processes run in /sys/fs/cgroup/lxc/<vmid>/ns/. Plain LXC has no
378
+ # inner nesting; /sys/fs/cgroup/lxc/<name>/ns/ won't exist, so the
379
+ # is-dir check below silently skips.
380
+ NS_CGROUP="${KENTO_TEST_NS_CGROUP:-/sys/fs/cgroup/lxc/$CONTAINER_ID/ns}"
381
+ if [ -d "$NS_CGROUP" ]; then
382
+ if [ -f "$CONTAINER_DIR/kento-memory" ]; then
383
+ MEM_MB=$(cat "$CONTAINER_DIR/kento-memory" | tr -d '[:space:]')
384
+ # Validate before arithmetic: a non-numeric/empty value would
385
+ # make $(( )) fail and, under `set -e`, abort the hook (a
386
+ # start error on pve-lxc). Warn-and-skip instead, mirroring the
387
+ # kento-port guard above.
388
+ case "$MEM_MB" in
389
+ ''|*[!0-9]*)
390
+ echo "kento: warning: invalid kento-memory '$MEM_MB' (expected positive integer MB) -- skipping memory.max" >&2
391
+ ;;
392
+ *)
393
+ MEM_BYTES=$((MEM_MB * 1024 * 1024))
394
+ echo "$MEM_BYTES" > "$NS_CGROUP/memory.max" 2>/dev/null \
395
+ || echo "kento: warning: could not set memory.max on $NS_CGROUP" >&2
396
+ ;;
397
+ esac
398
+ fi
399
+ if [ -f "$CONTAINER_DIR/kento-cores" ]; then
400
+ CORES=$(cat "$CONTAINER_DIR/kento-cores" | tr -d '[:space:]')
401
+ # Validate before arithmetic (see kento-memory above): a
402
+ # corrupted non-numeric value would otherwise abort the hook
403
+ # under `set -e`.
404
+ case "$CORES" in
405
+ ''|*[!0-9]*)
406
+ echo "kento: warning: invalid kento-cores '$CORES' (expected positive integer) -- skipping cpu.max" >&2
407
+ ;;
408
+ *)
409
+ QUOTA=$((CORES * 100000))
410
+ echo "$QUOTA 100000" > "$NS_CGROUP/cpu.max" 2>/dev/null \
411
+ || echo "kento: warning: could not set cpu.max on $NS_CGROUP" >&2
412
+ ;;
413
+ esac
414
+ fi
415
+ fi
416
+
417
+ # Port forwarding via nftables DNAT. For pve-lxc, this branch is
418
+ # reached via the snippets hookscript wrapper (post-start phase).
419
+ setup_port_forwarding "$CONTAINER_ID"
420
+ ;;
421
+ post-stop)
422
+ # Drop a cancel sentinel FIRST, before anything else. A DHCP
423
+ # port-forward worker may still be polling (or about to install
424
+ # rules) for this now-dead container; the sentinel tells it to
425
+ # abort without installing rules or writing the active marker. This
426
+ # closes the race where a worker wakes up after stop and leaves
427
+ # orphan DNAT/masquerade rules in the shared table.
428
+ : > "$CONTAINER_DIR/kento-portfwd-cancel" 2>/dev/null || true
429
+
430
+ # Reap a still-polling worker promptly: it was launched under setsid
431
+ # so its recorded PID is also its process-group ID. TERM the whole
432
+ # group. Tolerate the worker already being gone (kill -> non-zero).
433
+ if [ -f "$CONTAINER_DIR/kento-portfwd-pid" ]; then
434
+ WORKER_PID=$(cat "$CONTAINER_DIR/kento-portfwd-pid" 2>/dev/null | tr -d '[:space:]')
435
+ case "$WORKER_PID" in
436
+ ''|*[!0-9]*) : ;;
437
+ *) kill -TERM -- "-$WORKER_PID" 2>/dev/null || true ;;
438
+ esac
439
+ fi
440
+
441
+ # Tear down port forwarding rules by comment tag, using whichever
442
+ # backend installed them. The backend was recorded at install time in
443
+ # kento-portfwd-backend; default to nft if the marker is absent
444
+ # (pre-1.5.0 containers only ever used nft).
445
+ #
446
+ # Teardown runs UNCONDITIONALLY — NOT gated on kento-portfwd-active.
447
+ # A worker that won the race against the cancel sentinel may have
448
+ # installed rules without (or just before) writing the marker, so
449
+ # always attempt removal of this instance's tagged rules. The
450
+ # anchored greps below match only `kento:${NAME}` exactly, so a
451
+ # no-op teardown on an instance that never installed rules is quiet
452
+ # and harmless.
453
+ BACKEND=nft
454
+ if [ -f "$CONTAINER_DIR/kento-portfwd-backend" ]; then
455
+ BACKEND=$(cat "$CONTAINER_DIR/kento-portfwd-backend" | tr -d '[:space:]')
456
+ fi
457
+ # NAME is interpolated into an ERE below, where a kento name's `.`
458
+ # would otherwise act as a one-char wildcard (so `web.api` would also
459
+ # match a sibling `web1api`'s rule and tear down the wrong instance).
460
+ # Escape the ERE metacharacters that can appear in or around a name
461
+ # before using it in the teardown greps. The install side writes
462
+ # literal comments and is intentionally left untouched.
463
+ NAME_RE=$(printf '%s' "$NAME" | sed 's/[.[\*^$]/\\&/g')
464
+ if [ "$BACKEND" = iptables ]; then
465
+ # iptables: line numbers shift on every delete, so re-list and
466
+ # delete the first matching rule until none remain.
467
+ for chain in PREROUTING OUTPUT POSTROUTING; do
468
+ while :; do
469
+ # iptables renders the tag as `/* kento:NAME */`
470
+ # (space-delimited). Anchor to a trailing boundary so
471
+ # `kento:web` does NOT match `kento:web2`'s rule; NAME is
472
+ # regex-escaped (NAME_RE) so a name's `.` is a literal dot.
473
+ n=$(iptables -t nat -L "$chain" --line-numbers -n 2>/dev/null \
474
+ | grep -E "kento:${NAME_RE}( |\$)" | head -1 | awk '{print $1}')
475
+ [ -n "$n" ] || break
476
+ iptables -t nat -D "$chain" "$n" 2>/dev/null || break
477
+ done
478
+ done
479
+ else
480
+ for chain in prerouting output postrouting; do
481
+ # nft renders the tag as `comment "kento:NAME"`. Match the
482
+ # full quoted token so `kento:web` does NOT also delete
483
+ # `kento:web2`'s rule (prefix collision); NAME is regex-escaped
484
+ # (NAME_RE) so a name's `.` is a literal dot, not a wildcard.
485
+ nft -a list chain ip kento "$chain" 2>/dev/null \
486
+ | grep -E "comment \"kento:${NAME_RE}\"( |\$)" | \
487
+ awk '{print $NF}' | while read -r handle; do
488
+ nft delete rule ip kento "$chain" handle "$handle" 2>/dev/null || true
489
+ done
490
+ done
491
+ fi
492
+ # Remove the worker script and all kento-portfwd-* sentinels
493
+ # (active/backend/cancel/pid/worker.sh). Idempotent and quiet.
494
+ rm -f "$CONTAINER_DIR/kento-portfwd-active" \
495
+ "$CONTAINER_DIR/kento-portfwd-backend" \
496
+ "$CONTAINER_DIR/kento-portfwd-cancel" \
497
+ "$CONTAINER_DIR/kento-portfwd-pid" \
498
+ "$CONTAINER_DIR/kento-portfwd-worker.sh" 2>/dev/null || true
499
+ # Unmount overlayfs
500
+ mountpoint -q "$CONTAINER_DIR/rootfs" 2>/dev/null && umount "$CONTAINER_DIR/rootfs" || true
501
+
502
+ # Unprivileged cleanup: unmount idmapped bind mounts in reverse order
503
+ # and remove the idmap directory. Idempotent and quiet — if the
504
+ # idmap mounts were never created (privileged path) this is a no-op.
505
+ if [ -d "$STATE_DIR/idmap" ]; then
506
+ # Collect mounted targets in forward order, then umount in reverse
507
+ # so overlapping mount namespaces are handled safely.
508
+ _idmap_mounts=""
509
+ for _d in "$STATE_DIR"/idmap/*; do
510
+ [ -d "$_d" ] || continue
511
+ _idmap_mounts="$_idmap_mounts $_d"
512
+ done
513
+ # Reverse the list and unmount
514
+ _idmap_rev=""
515
+ for _d in $_idmap_mounts; do
516
+ _idmap_rev="$_d $_idmap_rev"
517
+ done
518
+ for _d in $_idmap_rev; do
519
+ [ -z "$_d" ] && continue
520
+ mountpoint -q "$_d" 2>/dev/null && umount "$_d" 2>/dev/null || true
521
+ done
522
+ rm -rf "$STATE_DIR/idmap" 2>/dev/null || true
523
+ fi
524
+ ;;
525
+ esac