From 6700a16adc0ce561b564f7dd382a8198117ef36d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:29:57 +0000 Subject: [PATCH] Add real socket activation to the quadlets Each heavy service now has a loopback .socket and a systemd-socket-proxyd service (Requires=/After= the container, --exit-idle-time) on a shifted container port. Container units keep StopWhenUnneeded=true, lose their [Install] section and SecurityLabelType=spc, use Notify=healthy, and the emulator uses AddDevice=/dev/kvm under default container_t confinement. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012AuVk4nEvBXaTjs4G6wdW4 --- .../heretek-android-kvm-adb-proxy.service | 16 ++++++ quadlets/heretek-android-kvm-adb-proxy.socket | 13 +++++ .../heretek-android-kvm-vnc-proxy.service | 16 ++++++ quadlets/heretek-android-kvm-vnc-proxy.socket | 13 +++++ quadlets/heretek-android-kvm.container | 53 ++++++++++++------- quadlets/heretek-blender-mcp-proxy.service | 15 ++++++ quadlets/heretek-blender-mcp-proxy.socket | 14 +++++ quadlets/heretek-blender-mcp.container | 42 +++++++++------ quadlets/heretek-ghidra-mcp-proxy.service | 15 ++++++ quadlets/heretek-ghidra-mcp-proxy.socket | 14 +++++ quadlets/heretek-ghidra-mcp.container | 48 +++++++++++------ 11 files changed, 207 insertions(+), 52 deletions(-) create mode 100644 quadlets/heretek-android-kvm-adb-proxy.service create mode 100644 quadlets/heretek-android-kvm-adb-proxy.socket create mode 100644 quadlets/heretek-android-kvm-vnc-proxy.service create mode 100644 quadlets/heretek-android-kvm-vnc-proxy.socket create mode 100644 quadlets/heretek-blender-mcp-proxy.service create mode 100644 quadlets/heretek-blender-mcp-proxy.socket create mode 100644 quadlets/heretek-ghidra-mcp-proxy.service create mode 100644 quadlets/heretek-ghidra-mcp-proxy.socket diff --git a/quadlets/heretek-android-kvm-adb-proxy.service b/quadlets/heretek-android-kvm-adb-proxy.service new file mode 100644 index 0000000..9501ab0 --- /dev/null +++ b/quadlets/heretek-android-kvm-adb-proxy.service @@ -0,0 +1,16 @@ +# quadlets/heretek-android-kvm-adb-proxy.service — ADB socket proxy for heretek-android-kvm +# +# Started by heretek-android-kvm-adb-proxy.socket. Pulls up the emulator unit +# (Requires=/After=; Notify=healthy there delays "started" until noVNC +# answers) and forwards connections to the container's loopback port. It exits +# after 15 idle minutes (--exit-idle-time, systemd-socket-proxyd's idle +# timeout); once both android proxies are gone, StopWhenUnneeded=true stops +# the emulator. + +[Unit] +Description=Heretek Android KVM ADB socket proxy (exits when idle) +Requires=heretek-android-kvm.service heretek-android-kvm-adb-proxy.socket +After=heretek-android-kvm.service heretek-android-kvm-adb-proxy.socket + +[Service] +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=15min 127.0.0.1:15555 diff --git a/quadlets/heretek-android-kvm-adb-proxy.socket b/quadlets/heretek-android-kvm-adb-proxy.socket new file mode 100644 index 0000000..b98bd86 --- /dev/null +++ b/quadlets/heretek-android-kvm-adb-proxy.socket @@ -0,0 +1,13 @@ +# quadlets/heretek-android-kvm-adb-proxy.socket — ADB activation socket for heretek-android-kvm +# +# Listens on the loopback-only port compose also uses (127.0.0.1:5555) and +# activates heretek-android-kvm-adb-proxy.service on the first connection. + +[Unit] +Description=Heretek Android KVM ADB activation socket (127.0.0.1:5555) + +[Socket] +ListenStream=127.0.0.1:5555 + +[Install] +WantedBy=sockets.target diff --git a/quadlets/heretek-android-kvm-vnc-proxy.service b/quadlets/heretek-android-kvm-vnc-proxy.service new file mode 100644 index 0000000..700eebd --- /dev/null +++ b/quadlets/heretek-android-kvm-vnc-proxy.service @@ -0,0 +1,16 @@ +# quadlets/heretek-android-kvm-vnc-proxy.service — noVNC socket proxy for heretek-android-kvm +# +# Started by heretek-android-kvm-vnc-proxy.socket. Pulls up the emulator unit +# (Requires=/After=; Notify=healthy there delays "started" until noVNC +# answers) and forwards connections to the container's loopback port. It exits +# after 15 idle minutes (--exit-idle-time, systemd-socket-proxyd's idle +# timeout); once both android proxies are gone, StopWhenUnneeded=true stops +# the emulator. + +[Unit] +Description=Heretek Android KVM noVNC socket proxy (exits when idle) +Requires=heretek-android-kvm.service heretek-android-kvm-vnc-proxy.socket +After=heretek-android-kvm.service heretek-android-kvm-vnc-proxy.socket + +[Service] +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=15min 127.0.0.1:16080 diff --git a/quadlets/heretek-android-kvm-vnc-proxy.socket b/quadlets/heretek-android-kvm-vnc-proxy.socket new file mode 100644 index 0000000..45d8c40 --- /dev/null +++ b/quadlets/heretek-android-kvm-vnc-proxy.socket @@ -0,0 +1,13 @@ +# quadlets/heretek-android-kvm-vnc-proxy.socket — noVNC activation socket for heretek-android-kvm +# +# Listens on the loopback-only port compose also uses (127.0.0.1:6080) and +# activates heretek-android-kvm-vnc-proxy.service on the first connection. + +[Unit] +Description=Heretek Android KVM noVNC activation socket (127.0.0.1:6080) + +[Socket] +ListenStream=127.0.0.1:6080 + +[Install] +WantedBy=sockets.target diff --git a/quadlets/heretek-android-kvm.container b/quadlets/heretek-android-kvm.container index f8b66c8..27ce7ed 100644 --- a/quadlets/heretek-android-kvm.container +++ b/quadlets/heretek-android-kvm.container @@ -1,39 +1,52 @@ -# quadlets/heretek-android-kvm.container — socket-activated Android emulator (Wave 3) +# quadlets/heretek-android-kvm.container — on-demand Android emulator (rootless Podman quadlet) # -# Heavy KVM emulator from compose/profile-android.yml, expressed as a rootless -# Podman systemd quadlet. StopWhenUnneeded=true keeps it at 0 MB idle RAM -# until socket activation pulls it up. Image and loopback ports mirror the -# compose profile exactly: budtmo/docker-android:emulator_14.0 on -# 127.0.0.1:6080 (VNC) and 127.0.0.1:5555 (ADB). +# Socket-activated through two loopback sockets, one per published port: +# heretek-android-kvm-vnc-proxy.socket 127.0.0.1:6080 (noVNC) +# heretek-android-kvm-adb-proxy.socket 127.0.0.1:5555 (ADB) +# The first connection to either starts its systemd-socket-proxyd service, +# which Requires= this unit, so the emulator is started on demand and proxied +# to its container-only ports (127.0.0.1:16080 / 127.0.0.1:15555). When both +# proxies have exited after --exit-idle-time, StopWhenUnneeded=true stops the +# emulator: 0 MB RAM while idle. No [Install] section on purpose — it must +# never start at login. # -# Enable + start (user systemd): +# Image: budtmo/docker-android:emulator_14.0_v3.8.0-p1, the same immutable +# release tag as compose/profile-android.yml. +# +# KVM: AddDevice=/dev/kvm grants only the KVM device node. The container keeps +# the default container_t SELinux confinement (no SecurityLabelType / +# SecurityLabelDisable). If an enforcing host denies /dev/kvm access, allow +# device use for containers rather than disabling labelling: +# sudo setsebool -P container_use_devices=true +# +# Install (see README "Quadlets"): +# cp quadlets/heretek-android-kvm.container ~/.config/containers/systemd/ +# cp quadlets/heretek-android-kvm-*-proxy.socket quadlets/heretek-android-kvm-*-proxy.service ~/.config/systemd/user/ # systemctl --user daemon-reload -# systemctl --user enable --now heretek-android-kvm.service -# KVM passthrough comes from the Device= mapping below (a device node grant, -# not a capability and not a volume mount — no SELinux label rule applies). +# systemctl --user enable --now heretek-android-kvm-vnc-proxy.socket heretek-android-kvm-adb-proxy.socket [Unit] -Description=Heretek Android KVM emulator (socket-activated, 0 MB idle RAM) +Description=Heretek Android KVM emulator (socket-activated, stops when idle) After=network-online.target Wants=network-online.target StopWhenUnneeded=true [Container] -Image=budtmo/docker-android:emulator_14.0 +Image=budtmo/docker-android:emulator_14.0_v3.8.0-p1 ContainerName=heretek-android-kvm User=1000:1000 -Device=/dev/kvm -PublishPort=127.0.0.1:6080:6080 -PublishPort=127.0.0.1:5555:5555 +AddDevice=/dev/kvm +PublishPort=127.0.0.1:16080:6080 +PublishPort=127.0.0.1:15555:5555 Volume=heretek-android-data:/data:Z Environment=EMULATOR_DEVICE=Samsung Galaxy S10 DropCapability=ALL NoNewPrivileges=true -SecurityLabelType=spc +HealthCmd=bash -c 'exec 3<>/dev/tcp/127.0.0.1/6080' +HealthInterval=10s +HealthStartPeriod=180s +Notify=healthy [Service] Restart=on-failure -TimeoutStartSec=300 - -[Install] -WantedBy=default.target +TimeoutStartSec=600 diff --git a/quadlets/heretek-blender-mcp-proxy.service b/quadlets/heretek-blender-mcp-proxy.service new file mode 100644 index 0000000..8acbfa5 --- /dev/null +++ b/quadlets/heretek-blender-mcp-proxy.service @@ -0,0 +1,15 @@ +# quadlets/heretek-blender-mcp-proxy.service — socket proxy for heretek-blender-mcp +# +# Started by heretek-blender-mcp-proxy.socket. Pulls up the container unit +# (Requires=/After=; Notify=healthy there delays "started" until the server +# answers) and forwards connections to the container's loopback port. It exits +# after 15 idle minutes (--exit-idle-time, systemd-socket-proxyd's idle +# timeout), which lets StopWhenUnneeded=true stop the container. + +[Unit] +Description=Heretek Blender MCP socket proxy (exits when idle) +Requires=heretek-blender-mcp.service heretek-blender-mcp-proxy.socket +After=heretek-blender-mcp.service heretek-blender-mcp-proxy.socket + +[Service] +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=15min 127.0.0.1:18781 diff --git a/quadlets/heretek-blender-mcp-proxy.socket b/quadlets/heretek-blender-mcp-proxy.socket new file mode 100644 index 0000000..69a1a35 --- /dev/null +++ b/quadlets/heretek-blender-mcp-proxy.socket @@ -0,0 +1,14 @@ +# quadlets/heretek-blender-mcp-proxy.socket — activation socket for heretek-blender-mcp +# +# Listens on the loopback-only port compose also uses (127.0.0.1:8781) and +# activates heretek-blender-mcp-proxy.service on the first connection. +# This is the only Blender unit that is enabled (WantedBy=sockets.target). + +[Unit] +Description=Heretek Blender MCP activation socket (127.0.0.1:8781) + +[Socket] +ListenStream=127.0.0.1:8781 + +[Install] +WantedBy=sockets.target diff --git a/quadlets/heretek-blender-mcp.container b/quadlets/heretek-blender-mcp.container index a7e6540..46f6d90 100644 --- a/quadlets/heretek-blender-mcp.container +++ b/quadlets/heretek-blender-mcp.container @@ -1,34 +1,46 @@ -# quadlets/heretek-blender-mcp.container — socket-activated Blender MCP server (Wave 3) +# quadlets/heretek-blender-mcp.container — on-demand Blender MCP server (rootless Podman quadlet) # -# Headless Blender FastMCP server from compose/profile-game.yml, expressed as -# a rootless Podman systemd quadlet. StopWhenUnneeded=true keeps it at 0 MB -# idle RAM until socket activation pulls it up. Image and loopback port mirror -# the compose profile exactly: ghcr.io/sandraschi/blender-mcp:v1.2.0 on -# 127.0.0.1:8781 (MIT-licensed FastMCP 3.2 stack, blender --background). +# Socket-activated: heretek-blender-mcp-proxy.socket owns 127.0.0.1:8781. The +# first connection starts heretek-blender-mcp-proxy.service (systemd-socket-proxyd), +# which Requires= this unit, so the container is started on demand and proxied +# to its container-only port 127.0.0.1:18781. When the proxy exits after +# --exit-idle-time with no connections, nothing needs this unit any more and +# StopWhenUnneeded=true stops the container: 0 MB RAM while idle. This unit has +# no [Install] section on purpose — it must never start at login. # -# Enable + start (user systemd): +# Image: localhost/heretek/blender-mcp:0.11.1 (MIT FastMCP server + Blender +# 4.2.3 LTS), the same pinned local build as compose/profile-game.yml. Build it +# once before enabling the socket: +# podman build -t localhost/heretek/blender-mcp:0.11.1 images/blender-mcp +# +# Install (see README "Quadlets"): +# cp quadlets/heretek-blender-mcp.container ~/.config/containers/systemd/ +# cp quadlets/heretek-blender-mcp-proxy.socket quadlets/heretek-blender-mcp-proxy.service ~/.config/systemd/user/ # systemctl --user daemon-reload -# systemctl --user enable --now heretek-blender-mcp.service +# systemctl --user enable --now heretek-blender-mcp-proxy.socket +# +# SELinux: default container_t confinement (no SecurityLabelType override). [Unit] -Description=Heretek Blender MCP background server (socket-activated, 0 MB idle RAM) +Description=Heretek Blender MCP background server (socket-activated, stops when idle) After=network-online.target Wants=network-online.target StopWhenUnneeded=true [Container] -Image=ghcr.io/sandraschi/blender-mcp:v1.2.0 +Image=localhost/heretek/blender-mcp:0.11.1 +Pull=never ContainerName=heretek-blender-mcp User=1000:1000 -PublishPort=127.0.0.1:8781:8781 +PublishPort=127.0.0.1:18781:8781 Volume=heretek-blender-data:/data:Z DropCapability=ALL NoNewPrivileges=true -SecurityLabelType=spc +HealthCmd=bash -c 'exec 3<>/dev/tcp/127.0.0.1/8781' +HealthInterval=10s +HealthStartPeriod=60s +Notify=healthy [Service] Restart=on-failure TimeoutStartSec=300 - -[Install] -WantedBy=default.target diff --git a/quadlets/heretek-ghidra-mcp-proxy.service b/quadlets/heretek-ghidra-mcp-proxy.service new file mode 100644 index 0000000..958c9ee --- /dev/null +++ b/quadlets/heretek-ghidra-mcp-proxy.service @@ -0,0 +1,15 @@ +# quadlets/heretek-ghidra-mcp-proxy.service — socket proxy for heretek-ghidra-mcp +# +# Started by heretek-ghidra-mcp-proxy.socket. Pulls up the container unit +# (Requires=/After=; Notify=healthy there delays "started" until the server +# answers) and forwards connections to the container's loopback port. It exits +# after 15 idle minutes (--exit-idle-time, systemd-socket-proxyd's idle +# timeout), which lets StopWhenUnneeded=true stop the container. + +[Unit] +Description=Heretek Ghidra MCP socket proxy (exits when idle) +Requires=heretek-ghidra-mcp.service heretek-ghidra-mcp-proxy.socket +After=heretek-ghidra-mcp.service heretek-ghidra-mcp-proxy.socket + +[Service] +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=15min 127.0.0.1:18089 diff --git a/quadlets/heretek-ghidra-mcp-proxy.socket b/quadlets/heretek-ghidra-mcp-proxy.socket new file mode 100644 index 0000000..f15420a --- /dev/null +++ b/quadlets/heretek-ghidra-mcp-proxy.socket @@ -0,0 +1,14 @@ +# quadlets/heretek-ghidra-mcp-proxy.socket — activation socket for heretek-ghidra-mcp +# +# Listens on the loopback-only port compose also uses (127.0.0.1:8089) and +# activates heretek-ghidra-mcp-proxy.service on the first connection. +# This is the only Ghidra unit that is enabled (WantedBy=sockets.target). + +[Unit] +Description=Heretek Ghidra MCP activation socket (127.0.0.1:8089) + +[Socket] +ListenStream=127.0.0.1:8089 + +[Install] +WantedBy=sockets.target diff --git a/quadlets/heretek-ghidra-mcp.container b/quadlets/heretek-ghidra-mcp.container index 32c61d3..daf2a16 100644 --- a/quadlets/heretek-ghidra-mcp.container +++ b/quadlets/heretek-ghidra-mcp.container @@ -1,38 +1,52 @@ -# quadlets/heretek-ghidra-mcp.container — socket-activated Ghidra MCP server (Wave 3) +# quadlets/heretek-ghidra-mcp.container — on-demand Ghidra MCP server (rootless Podman quadlet) # -# Headless Ghidra MCP server from compose/profile-appsec.yml, expressed as a -# rootless Podman systemd quadlet. StopWhenUnneeded=true keeps it at 0 MB idle -# RAM until socket activation pulls it up. Image and loopback port mirror the -# compose profile exactly: bethington/ghidra-mcp-headless:v6.0.0 on -# 127.0.0.1:8089. +# Socket-activated: heretek-ghidra-mcp-proxy.socket owns 127.0.0.1:8089. The +# first connection starts heretek-ghidra-mcp-proxy.service (systemd-socket-proxyd), +# which Requires= this unit, so the container is started on demand and proxied +# to its container-only port 127.0.0.1:18089. When the proxy exits after +# --exit-idle-time with no connections, nothing needs this unit any more and +# StopWhenUnneeded=true stops the container: 0 MB RAM while idle. This unit has +# no [Install] section on purpose — it must never start at login. +# +# Image: localhost/heretek/ghidra-mcp:6.0.0, the same pinned local build as +# compose/profile-appsec.yml. Build it once before enabling the socket: +# podman build -t localhost/heretek/ghidra-mcp:6.0.0 images/ghidra-mcp # # The mandatory bearer token is NOT stored here. Provide it via a user-owned -# environment file before starting (fail closed — the server refuses -# unauthenticated requests without it): +# environment file (the container refuses to start without it): # printf 'GHIDRA_MCP_AUTH_TOKEN=\n' > ~/.config/heretek/ghidra-mcp.env # chmod 600 ~/.config/heretek/ghidra-mcp.env -# systemctl --user enable --now heretek-ghidra-mcp.service +# +# Install (see README "Quadlets"): +# cp quadlets/heretek-ghidra-mcp.container ~/.config/containers/systemd/ +# cp quadlets/heretek-ghidra-mcp-proxy.socket quadlets/heretek-ghidra-mcp-proxy.service ~/.config/systemd/user/ +# systemctl --user daemon-reload +# systemctl --user enable --now heretek-ghidra-mcp-proxy.socket +# +# SELinux: default container_t confinement (no SecurityLabelType override). [Unit] -Description=Heretek Ghidra MCP headless server (socket-activated, 0 MB idle RAM) +Description=Heretek Ghidra MCP headless server (socket-activated, stops when idle) After=network-online.target Wants=network-online.target StopWhenUnneeded=true [Container] -Image=bethington/ghidra-mcp-headless:v6.0.0 +Image=localhost/heretek/ghidra-mcp:6.0.0 +Pull=never ContainerName=heretek-ghidra-mcp User=1000:1000 EnvironmentFile=%h/.config/heretek/ghidra-mcp.env -PublishPort=127.0.0.1:8089:8089 +Environment=GHIDRA_MCP_ALLOWED_HOSTS=localhost,127.0.0.1 +PublishPort=127.0.0.1:18089:8089 Volume=heretek-ghidra-data:/data:Z DropCapability=ALL NoNewPrivileges=true -SecurityLabelType=spc +HealthCmd=bash -c 'exec 3<>/dev/tcp/127.0.0.1/8089' +HealthInterval=10s +HealthStartPeriod=120s +Notify=healthy [Service] Restart=on-failure -TimeoutStartSec=300 - -[Install] -WantedBy=default.target +TimeoutStartSec=600