Compare commits

..

No commits in common. "c461f04c13f4b18df56bad45990e9a4450e27be3" and "644748664312a19f74d40ab7e42902164306e409" have entirely different histories.

9 changed files with 224 additions and 129 deletions

View file

@ -9,18 +9,18 @@
... ...
}: }:
{ {
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
sops.defaultSopsFile = ./secrets/home.yaml; sops.defaultSopsFile = ./secrets/home.yaml;
sops.age.keyFile = "/.persist/root/.config/sops/age/keys.txt"; sops.age.keyFile = "/.persist/root/.config/sops/age/keys.txt";
sops.secrets."root/ssh/desktop" = { sops.secrets."root/ssh/desktop" = {
path = "/root/.ssh/desktop"; path = "/root/.ssh/desktop";
mode = "0600"; mode = "0600";
}; };
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
boot.initrd.systemd.enable = true; boot.initrd.systemd.enable = true;
boot.loader.systemd-boot.enable = true; boot.loader.systemd-boot.enable = true;

View file

@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SSH_TARGET="sandbox@vsock%3"
if [[ "$PWD" == /home/user* ]] || [[ "$PWD" == /etc/nixos* ]]; then
project="${PWD/\/home\/user/\/home\/sandbox}"
else
project="$(ssh "$SSH_TARGET" 'mktemp -d -p /tmp claude-XXXXXX')"
fi
exec ssh -t "$SSH_TARGET" claude-code "$project" "$@"

View file

@ -3,7 +3,7 @@ set -euo pipefail
SSH_TARGET="sandbox@vsock%3" SSH_TARGET="sandbox@vsock%3"
if [[ "$PWD" == /home/user* ]] || [[ "$PWD" == /etc/nixos* ]]; then if [[ "$PWD" == /home/user* ]]; then
project="${PWD/\/home\/user/\/home\/sandbox}" project="${PWD/\/home\/user/\/home\/sandbox}"
else else
project="$(ssh "$SSH_TARGET" 'mktemp -d -p /tmp opencode-XXXXXX')" project="$(ssh "$SSH_TARGET" 'mktemp -d -p /tmp opencode-XXXXXX')"

View file

@ -4,10 +4,59 @@
pkgs, pkgs,
... ...
}: }:
let
agentProfile = pkgs.writeText "agent.profile" ''
whitelist /nix/store
whitelist /nix/var
read-only /nix/store
whitelist ''${HOME}/.cache/nix
whitelist ''${HOME}/.local/state/nix
whitelist /etc/nix
whitelist /etc/nixos
whitelist /nix/var/nix/daemon-socket
whitelist ''${HOME}/dev
whitelist ''${HOME}/work
whitelist ''${HOME}/.config
blacklist ''${HOME}
blacklist /etc/nixos/secrets
blacklist /etc/shadow
blacklist /etc/sudoers
blacklist /etc/sudoers.d
blacklist /root
caps.drop all
nonewprivs
noroot
seccomp
no3d
nodvd
notv
nou2f
novideo
ignore net none
machine-id
disable-mnt
private-dev
private-tmp
private-cache
'';
in
{ {
programs.firejail = { programs.firejail = {
enable = true; enable = true;
wrappedBinaries = { wrappedBinaries = {
claude = {
executable = "${pkgs.claude-code}/bin/claude";
profile = agentProfile;
extraArgs = [
"--private=~/.local/state/claude-home"
];
};
chromium-sandbox = { chromium-sandbox = {
executable = "${pkgs.chromium}/bin/chromium"; executable = "${pkgs.chromium}/bin/chromium";
profile = "${pkgs.firejail}/etc/firejail/chromium-browser.profile"; profile = "${pkgs.firejail}/etc/firejail/chromium-browser.profile";

View file

@ -58,38 +58,25 @@
# "Xft.antialias" = 1; # "Xft.antialias" = 1;
# "Xft.rgba" = "rgb"; # "Xft.rgba" = "rgb";
# }; # };
systemd.user.services.screenshot-latest = {
Unit = {
Description = "Update latest.png symlink";
};
Service = {
Type = "oneshot";
ExecStart =
let
screenshotDir = "${config.home.homeDirectory}/pictures/screenshots";
script = pkgs.writeShellScript "screenshot-latest-update" ''
latest=$(${pkgs.coreutils}/bin/ls -t "${screenshotDir}"/*.png 2>/dev/null | head -1)
if [ -n "$latest" ]; then
${pkgs.coreutils}/bin/ln -sf "$latest" "${screenshotDir}/latest.png"
fi
'';
in
"${script}";
};
};
systemd.user.paths.screenshot-latest = { systemd.user.services.waypipe-socket = {
Unit = { Unit = {
Description = "Watch screenshot directory for new files"; Description = "start waypipe client";
After = [ "niri.service" ];
Requires = [ "niri.service" ];
}; };
Install = { Install = {
WantedBy = [ "default.target" ]; WantedBy = [ "graphical-session.target" ];
}; };
Path = { Service = {
PathChanged = "${config.home.homeDirectory}/pictures/screenshots"; ExecStart = "${pkgs.waypipe}/bin/waypipe --socket /tmp/waypipe.sock client";
ExecStartPost = "${pkgs.acl}/bin/setfacl -m g:sandbox:rw /tmp/waypipe.sock";
ExecStopPost = "${pkgs.coreutils}/bin/rm /tmp/waypipe.sock";
RuntimeDirectory = "waypipe";
Type = "simple";
Restart = "on-failure";
}; };
}; };
sops.secrets."user/gpg/legacy_fnzr" = { }; sops.secrets."user/gpg/legacy_fnzr" = { };
home.activation.importGpgKey = config.lib.dag.entryAfter [ "writeBoundary" ] '' home.activation.importGpgKey = config.lib.dag.entryAfter [ "writeBoundary" ] ''
if [[ -f "${config.sops.secrets."user/gpg/legacy_fnzr".path}" ]]; then if [[ -f "${config.sops.secrets."user/gpg/legacy_fnzr".path}" ]]; then
@ -271,7 +258,7 @@
}; };
programs = { programs = {
# opencode.enable = true; opencode.enable = true;
dank-material-shell.enable = true; dank-material-shell.enable = true;
ssh = { ssh = {
enable = true; enable = true;
@ -377,13 +364,11 @@
home.packages = with pkgs; [ home.packages = with pkgs; [
xrdb xrdb
# (writeShellScriptBin "agent" ''
# machinectl shell agent@ ${waypipe}/bin/waypipe --socket /run/waypipe.sock server fish
# '')
(writeShellApplication { (writeShellApplication {
name = "claude-code"; name = "opencode-sandbox";
runtimeInputs = [ openssh ];
text = builtins.readFile ../executables/claude-code.sh;
})
(writeShellApplication {
name = "opencode";
runtimeInputs = [ openssh ]; runtimeInputs = [ openssh ];
text = builtins.readFile ../executables/opencode.sh; text = builtins.readFile ../executables/opencode.sh;
}) })

View file

@ -56,7 +56,6 @@ in
keepEnv = true; keepEnv = true;
persist = true; persist = true;
setEnv = [ setEnv = [
"HOME=/root"
"EDITOR" "EDITOR"
"TERM" "TERM"
]; ];

View file

@ -26,13 +26,6 @@ in
homeManagerModule homeManagerModule
]; ];
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
nix.nixPath = [ "nixpkgs=${pkgs.path}" ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
sops.defaultSopsFile = ./secrets/sandbox.yaml; sops.defaultSopsFile = ./secrets/sandbox.yaml;
sops.age.keyFile = "/.persist/secrets/age-keys.txt"; sops.age.keyFile = "/.persist/secrets/age-keys.txt";
sops.secrets."root-password-hash" = { sops.secrets."root-password-hash" = {
@ -99,7 +92,6 @@ in
fd fd
ripgrep ripgrep
podman-compose podman-compose
claude-code
]; ];
environment.persistence."/.persist" = { environment.persistence."/.persist" = {
@ -194,17 +186,17 @@ in
{ {
mountPoint = "/var"; mountPoint = "/var";
image = "var.img"; image = "var.img";
size = 2 * 1024; size = 256;
} }
{ {
mountPoint = "/nix/.rw-store"; mountPoint = "/nix/.rw-store";
image = "nix-store-overlay.img"; image = "nix-store-overlay.img";
size = 2 * 1024; size = 512;
} }
{ {
mountPoint = "/.persist"; mountPoint = "/.persist";
image = "persist.img"; image = "persist.img";
size = 2 * 1024; size = 1;
} }
]; ];
shares = [ shares = [
@ -220,12 +212,6 @@ in
source = "/home/user/work"; source = "/home/user/work";
mountPoint = "/home/sandbox/work"; mountPoint = "/home/sandbox/work";
} }
{
proto = "virtiofs";
tag = "nixos";
source = "/etc/nixos";
mountPoint = "/etc/nixos";
}
{ {
proto = "virtiofs"; proto = "virtiofs";
tag = "screenshots"; tag = "screenshots";

View file

@ -5,10 +5,7 @@
... ...
}: }:
{ {
imports = [ imports = [ ./wireguard.nix ];
./wireguard.nix
./tailscale.nix
];
sops.secrets = { sops.secrets = {
"wg0/conf".sopsFile = ./secrets/vpn.yaml; "wg0/conf".sopsFile = ./secrets/vpn.yaml;
"wg-br0/conf".sopsFile = ./secrets/vpn.yaml; "wg-br0/conf".sopsFile = ./secrets/vpn.yaml;
@ -16,13 +13,6 @@
"wg-uk0/conf".sopsFile = ./secrets/vpn.yaml; "wg-uk0/conf".sopsFile = ./secrets/vpn.yaml;
}; };
services.tailscale-netns = {
enable = true;
instances.work = {
extraDaemonFlags = [ "--accept-routes=false" ];
};
};
networking = { networking = {
hostName = hostname; hostName = hostname;
networkmanager.enable = false; networkmanager.enable = false;

View file

@ -10,24 +10,137 @@ with lib;
let let
cfg = config.services.tailscale-netns; cfg = config.services.tailscale-netns;
instanceOptions = { instanceOptions =
options.extraDaemonFlags = mkOption { { name, ... }:
type = types.listOf types.str; {
default = [ ]; options = {
description = "Extra flags passed to `tailscaled`."; authKeyFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to file containing a Tailscale auth key.";
};
hostname = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Tailscale hostname for this instance.
Defaults to "<system-hostname>-<instance-name>".
'';
};
advertiseRoutes = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Subnet routes to advertise (e.g. [ \"10.0.0.0/24\" ]).";
};
dns = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Bootstrap DNS server for the namespace before Tailscale is connected.
Defaults to `1.1.1.1`. Only used for the initial coordination server
connection; once Tailscale is up it manages DNS inside the namespace.
'';
};
advertiseExitNode = mkOption {
type = types.bool;
default = false;
description = "Whether to advertise this node as an exit node.";
};
acceptRoutes = mkOption {
type = types.bool;
default = false;
description = "Whether to accept routes advertised by other nodes.";
};
exitNode = mkOption {
type = types.nullOr types.str;
default = null;
description = "Tailscale IP or hostname of exit node to use.";
};
extraUpFlags = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Extra flags passed to `tailscale up`.";
};
extraDaemonFlags = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Extra flags passed to `tailscaled`.";
};
};
}; };
};
mkUpFlags =
instanceCfg:
let
hostFlag = optional (
instanceCfg.hostname != null
) "--hostname=${escapeShellArg instanceCfg.hostname}";
routeFlags = map (r: "--advertise-routes=${escapeShellArg r}") instanceCfg.advertiseRoutes;
exitFlag = optional instanceCfg.advertiseExitNode "--advertise-exit-node";
acceptFlag = optional instanceCfg.acceptRoutes "--accept-routes";
exitNodeFlag = optional (
instanceCfg.exitNode != null
) "--exit-node=${escapeShellArg instanceCfg.exitNode}";
in
hostFlag ++ routeFlags ++ exitFlag ++ acceptFlag ++ exitNodeFlag ++ instanceCfg.extraUpFlags;
mkAutoconnectScript =
name: instanceCfg:
pkgs.writers.writeBash "tailscale-autoconnect-${name}" ''
set -euo pipefail
TS="${pkgs.tailscale}/bin/tailscale"
SOCKET="/run/tailscale-${name}/tailscaled.sock"
TS_ARGS="--socket $SOCKET"
get_state() {
$TS $TS_ARGS status --json --peers=false 2>/dev/null | ${pkgs.jq}/bin/jq -r '.BackendState // "NoState"'
}
last_state=""
while state="$(get_state)"; do
if [[ "$state" != "$last_state" ]]; then
case "$state" in
NeedsLogin|NeedsMachineAuth|Stopped)
echo "[tailscale-netns:${name}] server needs authentication, sending auth key"
$TS $TS_ARGS up \
--auth-key "$(cat ${instanceCfg.authKeyFile})" \
${escapeShellArgs (mkUpFlags instanceCfg)}
;;
Running)
echo "[tailscale-netns:${name}] tailscale is running"
${pkgs.systemd}/bin/systemd-notify --ready
exit 0
;;
NoState)
echo "[tailscale-netns:${name}] tailscaled not ready yet"
;;
*)
echo "[tailscale-netns:${name}] waiting for Running (state=$state)"
;;
esac
fi
last_state="$state"
sleep 1
done
'';
mkNetnsSetup = mkNetnsSetup =
index: name: instanceCfg: index: name: instanceCfg:
let let
ns = "ts-${name}ns"; ns = "ts-${name}ns";
hostAddr = "10.201.${toString index}.1"; hostIP = "10.201.${toString index}.1/24";
hostIP = "${hostAddr}/24";
nsIP = "10.201.${toString index}.2/24"; nsIP = "10.201.${toString index}.2/24";
vethHost = "veth-ts-${name}"; vethHost = "veth-ts-${name}";
vethNS = "veth-ts-${name}-ns"; vethNS = "veth-ts-${name}-ns";
fallbackLines = concatMapStrings (n: " echo nameserver ${n}\n") config.networking.nameservers;
in in
{ {
"netns@ts-${name}" = { "netns@ts-${name}" = {
@ -40,42 +153,12 @@ let
set -euo pipefail set -euo pipefail
${pkgs.coreutils}/bin/mkdir -p /etc/netns/${ns} ${pkgs.coreutils}/bin/mkdir -p /etc/netns/${ns}
# Build resolv.conf for the netns. Skip loopback addresses
# (systemd-resolved stub at 127.0.0.53 is not reachable from the netns).
NS_DNS="${if instanceCfg.dns != null then instanceCfg.dns else "1.1.1.1"}"
{ {
dns_found=0 ${pkgs.gawk}/bin/awk '$1 == "nameserver" && $2 !~ /^127\./ && $2 != "::1" {print}' /etc/resolv.conf
if ${pkgs.systemd}/bin/resolvectl dns 2>/dev/null; then echo "nameserver $NS_DNS"
saved_ifs="$IFS"
IFS=$'\n'
for line in $(${pkgs.systemd}/bin/resolvectl dns 2>/dev/null); do
case "$line" in
*:*)
server=$(echo "$line" | ${pkgs.gnused}/bin/sed 's/^[^:]*: //')
case "$server" in
127.*|::1|"") ;;
*) echo "nameserver $server"; dns_found=1 ;;
esac
;;
esac
done
IFS="$saved_ifs"
fi
if [ "$dns_found" -eq 0 ]; then
while IFS= read -r rline; do
case "$rline" in
nameserver*)
ip=$(echo "$rline" | ${pkgs.gnused}/bin/sed 's/^nameserver //')
case "$ip" in
127.*|::1) ;;
*) echo "$rline"; dns_found=1 ;;
esac
;;
esac
done < /etc/resolv.conf
fi
${optionalString (fallbackLines != "") ''
if [ "$dns_found" -eq 0 ]; then
${fallbackLines} fi
''}
} > /etc/netns/${ns}/resolv.conf } > /etc/netns/${ns}/resolv.conf
${pkgs.iproute2}/bin/ip netns add ${ns} ${pkgs.iproute2}/bin/ip netns add ${ns}
@ -88,7 +171,6 @@ let
${pkgs.iproute2}/bin/ip -n ${ns} addr add ${nsIP} dev ${vethNS} ${pkgs.iproute2}/bin/ip -n ${ns} addr add ${nsIP} dev ${vethNS}
${pkgs.iproute2}/bin/ip -n ${ns} link set ${vethNS} up ${pkgs.iproute2}/bin/ip -n ${ns} link set ${vethNS} up
${pkgs.iproute2}/bin/ip -n ${ns} link set lo up ${pkgs.iproute2}/bin/ip -n ${ns} link set lo up
${pkgs.iproute2}/bin/ip -n ${ns} route add default via ${hostAddr}
${pkgs.iptables}/bin/iptables -t nat -C POSTROUTING -s ${nsIP} -j MASQUERADE 2>/dev/null \ ${pkgs.iptables}/bin/iptables -t nat -C POSTROUTING -s ${nsIP} -j MASQUERADE 2>/dev/null \
|| ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s ${nsIP} -j MASQUERADE || ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s ${nsIP} -j MASQUERADE
@ -149,7 +231,10 @@ in
description = "Tailscale instances configured in separate network namespaces."; description = "Tailscale instances configured in separate network namespaces.";
example = literalExpression '' example = literalExpression ''
{ {
work = {}; work = {
authKeyFile = "/run/secrets/ts-work/authkey";
acceptRoutes = true;
};
} }
''; '';
}; };
@ -162,12 +247,25 @@ in
index: elem: index: elem:
let let
name = elem.name; name = elem.name;
units = mkNetnsSetup index name elem.value; instanceCfg = elem.value;
units = mkNetnsSetup index name instanceCfg;
in in
[ [
{ name = "netns@ts-${name}"; value = units."netns@ts-${name}"; } units."netns@ts-${name}"
{ name = "tailscaled-${name}"; value = units."tailscaled-${name}"; } units."tailscaled-${name}"
] ]
++ optional (instanceCfg.authKeyFile != null) {
"tailscaled-autoconnect-${name}" = {
description = "Tailscale autoconnect for instance ${name}";
after = [ "tailscaled-${name}.service" ];
requires = [ "tailscaled-${name}.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "notify";
};
script = "${mkAutoconnectScript name instanceCfg}";
};
}
) (attrsToList cfg.instances) ) (attrsToList cfg.instances)
) )
); );