{ config, lib, pkgs, ... }: let cfg = config.services.claudebox; in { options.services.claudebox = { enable = lib.mkEnableOption '' claudebox network isolation. Installs nftables rules that drop egress to Tailscale CGNAT, RFC1918, MagicDNS resolver, and link-local ranges for any process inside the systemd user slice `claude-sandbox.slice`. The claudebox wrapper launches `claude` into this slice via `systemd-run --user --scope --slice=claude-sandbox.slice`. The rules installed here are the structural backstop that Claude Code's built-in `/sandbox` does not provide (it does hostname allowlisting only, not CIDR-level block). ''; cgroupLevel = lib.mkOption { type = lib.types.int; default = 4; description = '' Cgroup level at which `claude-sandbox.slice` appears in the cgroup v2 hierarchy. The default 4 matches modern systemd user-instance layout: ``` / (level 0) user.slice/ (level 1) user-1000.slice/ (level 2) user@1000.service/ (level 3) claude-sandbox.slice/ (level 4) ``` Verify on your system with: ``` systemd-run --user --scope --slice=claude-sandbox.slice -- sleep 5 & cat /proc/$!/cgroup ``` Count `/`-separated components from root to find where `claude-sandbox.slice` sits. ''; }; blockedCidrsV4 = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "100.64.0.0/10" # Tailscale CGNAT "100.100.100.100/32" # Tailscale MagicDNS resolver "10.0.0.0/8" # RFC1918 "172.16.0.0/12" # RFC1918 "192.168.0.0/16" # RFC1918 "169.254.0.0/16" # link-local ]; description = '' IPv4 CIDRs blocked for processes inside `claude-sandbox.slice`. Defaults cover the homelab threat model: no Tailscale, no LAN, no link-local (cloud metadata services). ''; }; blockedCidrsV6 = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "fd7a:115c:a1e0::/48" # Tailscale IPv6 "fc00::/7" # ULA (RFC4193) "fe80::/10" # link-local ]; description = '' IPv6 CIDRs blocked for processes inside `claude-sandbox.slice`. ''; }; extraOutputRules = lib.mkOption { type = lib.types.lines; default = ""; description = '' Extra nftables rules to append to the claudebox `output` chain. Useful for blocking additional internal subnets or specific ports. Rules run after the default CIDR blocks but inside the same chain, so they only fire for sockets in `claude-sandbox.slice`. ''; }; }; config = lib.mkIf cfg.enable { networking.nftables.enable = true; networking.nftables.tables.claudebox = { family = "inet"; content = '' chain output { type filter hook output priority filter; policy accept; # IPv4 CIDR block — only fires for sockets inside claude-sandbox.slice. socket cgroupv2 level ${toString cfg.cgroupLevel} "claude-sandbox.slice" \ ip daddr { ${lib.concatStringsSep ", " cfg.blockedCidrsV4} } drop # IPv6 CIDR block. socket cgroupv2 level ${toString cfg.cgroupLevel} "claude-sandbox.slice" \ ip6 daddr { ${lib.concatStringsSep ", " cfg.blockedCidrsV6} } drop ${cfg.extraOutputRules} } ''; }; assertions = [ { assertion = cfg.cgroupLevel >= 0 && cfg.cgroupLevel <= 16; message = "services.claudebox.cgroupLevel must be 0..16 (cgroup hierarchy depth)."; } ]; }; }