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/__init__.py +461 -0
- kento/attach.py +188 -0
- kento/cloudinit.py +201 -0
- kento/create.py +1205 -0
- kento/defaults.py +207 -0
- kento/destroy.py +131 -0
- kento/diagnose.py +548 -0
- kento/errors.py +48 -0
- kento/exec_cmd.py +50 -0
- kento/hook.py +28 -0
- kento/hook.sh +525 -0
- kento/images.py +210 -0
- kento/info.py +274 -0
- kento/inject.py +28 -0
- kento/inject.sh +282 -0
- kento/layers.py +81 -0
- kento/list.py +146 -0
- kento/locking.py +64 -0
- kento/logs.py +48 -0
- kento/lxc_hook.py +61 -0
- kento/pve.py +619 -0
- kento/reset.py +212 -0
- kento/set_cmd.py +1036 -0
- kento/start.py +65 -0
- kento/stop.py +210 -0
- kento/subprocess_util.py +76 -0
- kento/suspend.py +196 -0
- kento/vm.py +504 -0
- kento/vm_hook.py +256 -0
- kento_core-1.6.0.dev1.dist-info/METADATA +8 -0
- kento_core-1.6.0.dev1.dist-info/RECORD +34 -0
- kento_core-1.6.0.dev1.dist-info/WHEEL +5 -0
- kento_core-1.6.0.dev1.dist-info/licenses/LICENSE.md +594 -0
- kento_core-1.6.0.dev1.dist-info/top_level.txt +1 -0
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
|