467 lines
16 KiB
Nix
467 lines
16 KiB
Nix
# Browser VPN Isolation Module
|
|
# Provides: Isolated browsers (Firefox, Tor, Thorium, Kitty) running in Podman through VPN
|
|
#
|
|
# Usage:
|
|
# myModules.browserVpn = {
|
|
# enable = true;
|
|
# browsers = [ "firefox" "tor-browser" "thorium" "kitty" ]; # default: all
|
|
# gtkTheme = "Catppuccin-Frappe-Standard-Blue-Dark";
|
|
# repositoryPath = "/home/user/nixos"; # Path to container Dockerfiles
|
|
# };
|
|
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
|
|
let
|
|
cfg = config.myModules.browserVpn;
|
|
|
|
# Helper function for auto-recovery from podman namespace corruption
|
|
# Detects "cannot re-exec process" errors and runs migrate to fix
|
|
podmanRecoveryHelper = ''
|
|
podman_with_recovery() {
|
|
local output
|
|
local exit_code
|
|
|
|
# First attempt
|
|
output=$(podman "$@" 2>&1)
|
|
exit_code=$?
|
|
|
|
# Check for the namespace corruption error
|
|
if echo "$output" | grep -q "cannot re-exec process to join the existing user namespace"; then
|
|
echo "Detected stale podman namespace, running recovery..."
|
|
podman system migrate 2>/dev/null || true
|
|
sleep 1
|
|
|
|
# Retry the command
|
|
output=$(podman "$@" 2>&1)
|
|
exit_code=$?
|
|
fi
|
|
|
|
echo "$output"
|
|
return $exit_code
|
|
}
|
|
'';
|
|
|
|
# Backend script generator for browsers
|
|
# Firefox needs --security-opt=label=disable, others use --cap-drop=ALL
|
|
mkBrowserBackend =
|
|
name: containerName: imageName: dataVol: securityOpts: extraCmd:
|
|
pkgs.writeShellScriptBin "${name}-vpn-backend" ''
|
|
ACTION="$1"
|
|
W_DISPLAY="$2"
|
|
RUNTIME_DIR="$3"
|
|
REPO_DIR="${cfg.repositoryPath}"
|
|
|
|
${podmanRecoveryHelper}
|
|
|
|
case "$ACTION" in
|
|
stop)
|
|
echo "Stopping containers..."
|
|
podman_with_recovery stop ${containerName} 2>/dev/null || true
|
|
systemctl --user stop gluetun.service
|
|
echo "Containers stopped."
|
|
;;
|
|
status)
|
|
echo "=== gluetun ==="
|
|
systemctl --user status gluetun.service --no-pager 2>/dev/null || echo "Not running (or service not found)"
|
|
echo ""
|
|
echo "=== ${containerName} ==="
|
|
podman_with_recovery ps --filter name=${containerName} 2>/dev/null || echo "Not running"
|
|
;;
|
|
build)
|
|
echo "Building ${name} container..."
|
|
podman_with_recovery build -t ${imageName}:latest "$REPO_DIR/containers/${name}-wayland/"
|
|
echo "Build complete."
|
|
;;
|
|
run)
|
|
if ! podman_with_recovery image exists ${imageName}:latest 2>/dev/null; then
|
|
echo "Building ${name} container image..."
|
|
podman_with_recovery build -t ${imageName}:latest "$REPO_DIR/containers/${name}-wayland/"
|
|
fi
|
|
|
|
echo "Starting VPN container (user service)..."
|
|
systemctl --user start gluetun.service
|
|
|
|
echo "Waiting for VPN connection..."
|
|
sleep 10
|
|
|
|
echo "Starting ${name} with native Wayland (Rootless)..."
|
|
podman_with_recovery run --rm -d \
|
|
--name ${containerName} \
|
|
--network=container:gluetun \
|
|
${securityOpts} \
|
|
--userns=keep-id \
|
|
--shm-size=2g \
|
|
--device=/dev/dri \
|
|
-v "$RUNTIME_DIR/$W_DISPLAY:/run/user/1000/$W_DISPLAY:ro" \
|
|
-v "$RUNTIME_DIR/pipewire-0:/tmp/pipewire-0:ro" \
|
|
-v "$RUNTIME_DIR/pulse:/tmp/pulse:ro" \
|
|
-v /etc/machine-id:/etc/machine-id:ro \
|
|
-e "WAYLAND_DISPLAY=$W_DISPLAY" \
|
|
-e "XDG_RUNTIME_DIR=/run/user/1000" \
|
|
-e "PULSE_SERVER=unix:/tmp/pulse/native" \
|
|
-e "MOZ_ENABLE_WAYLAND=1" \
|
|
-e "LIBGL_ALWAYS_SOFTWARE=1" \
|
|
-e "MOZ_WEBRENDER=0" \
|
|
-e "LD_PRELOAD=" \
|
|
-v ${dataVol} \
|
|
-e GTK_THEME=${cfg.gtkTheme} \
|
|
-e "GSETTINGS_BACKEND=keyfile" \
|
|
${imageName}:latest \
|
|
${extraCmd}
|
|
|
|
echo ""
|
|
echo "${name} started! Window should appear on your desktop."
|
|
;;
|
|
*)
|
|
echo "Usage: ${name}-vpn-backend {stop|status|build|run} <DISPLAY> <RUNTIME_DIR>"
|
|
exit 1
|
|
;;
|
|
esac
|
|
'';
|
|
|
|
# Frontend wrapper script
|
|
mkFrontendScript =
|
|
name: backend:
|
|
pkgs.writeShellScriptBin "${name}-vpn-podman" ''
|
|
CMD="run"
|
|
if [ -n "$1" ]; then
|
|
CMD="$1"
|
|
fi
|
|
${backend}/bin/${name}-vpn-backend \
|
|
"$CMD" \
|
|
"$WAYLAND_DISPLAY" \
|
|
"$XDG_RUNTIME_DIR"
|
|
'';
|
|
|
|
# Desktop entry generator
|
|
mkDesktopEntry = name: displayName: icon: category: keywords: ''
|
|
cat > $out/share/applications/${name}-vpn.desktop << 'EOF'
|
|
[Desktop Entry]
|
|
Name=${displayName} (Isolated VPN)
|
|
Comment=${displayName} with network isolation through VPN
|
|
Exec=${name}-vpn-podman
|
|
Icon=${icon}
|
|
Terminal=false
|
|
Type=Application
|
|
Categories=${category};
|
|
Keywords=${keywords};
|
|
EOF
|
|
'';
|
|
|
|
# Firefox policies to disable IPv6 and force fast connections
|
|
firefoxPolicies = pkgs.writeText "policies.json" (
|
|
builtins.toJSON {
|
|
policies = {
|
|
DisableAppUpdate = true;
|
|
DisableTelemetry = true;
|
|
DisablePocket = true;
|
|
DisableFirefoxStudies = true;
|
|
EnableTrackingProtection = {
|
|
Value = true;
|
|
Locked = true;
|
|
Cryptomining = true;
|
|
Fingerprinting = true;
|
|
};
|
|
Preferences = {
|
|
"network.dns.disableIPv6" = true;
|
|
"network.ipv6" = false;
|
|
"network.http.fast-fallback-to-IPv4" = true;
|
|
"network.trr.mode" = 5; # Disable DNS over HTTPS (use system/VPN DNS)
|
|
"ui.systemUsesDarkTheme" = 1;
|
|
"browser.theme.content-theme" = 0;
|
|
"browser.theme.toolbar-theme" = 0;
|
|
"browser.in-content.dark-mode" = true;
|
|
};
|
|
};
|
|
}
|
|
);
|
|
|
|
# Browser configurations
|
|
# Firefox needs label=disable for its internal sandbox to work
|
|
firefoxBackend =
|
|
mkBrowserBackend "firefox" "firefox-vpn" "localhost/firefox-wayland"
|
|
"firefox-vpn-data:/home/firefox-user/.mozilla"
|
|
"--security-opt=label=disable --security-opt=seccomp=unconfined -v ${firefoxPolicies}:/usr/lib/firefox/distribution/policies.json:ro"
|
|
"";
|
|
|
|
# Other browsers use --cap-drop=ALL for enhanced security
|
|
torBrowserBackend =
|
|
mkBrowserBackend "tor-browser" "tor-browser-vpn" "localhost/tor-browser-wayland"
|
|
"tor-browser-vpn-data:/home/tor-user/tor-browser/Browser/TorBrowser/Data"
|
|
"--cap-drop=ALL"
|
|
"";
|
|
|
|
thoriumBackend =
|
|
mkBrowserBackend "thorium" "thorium-vpn" "localhost/thorium-wayland"
|
|
"thorium-vpn-data:/home/thorium-user/.config/thorium"
|
|
"--cap-drop=ALL"
|
|
"thorium-browser --ozone-platform=wayland --enable-features=UseOzonePlatform --enable-gpu-rasterization --enable-zero-copy --no-sandbox";
|
|
|
|
# Thorium Dev backend with custom browser flags for localhost-only access
|
|
thoriumDevBackend = pkgs.writeShellScriptBin "thorium-dev-vpn-backend" ''
|
|
ACTION="$1"
|
|
W_DISPLAY="$2"
|
|
RUNTIME_DIR="$3"
|
|
REPO_DIR="${cfg.repositoryPath}"
|
|
|
|
${podmanRecoveryHelper}
|
|
|
|
case "$ACTION" in
|
|
stop)
|
|
echo "Stopping containers..."
|
|
podman_with_recovery stop thorium-dev-vpn 2>/dev/null || true
|
|
systemctl --user stop gluetun.service
|
|
echo "Containers stopped."
|
|
;;
|
|
status)
|
|
echo "=== gluetun ==="
|
|
systemctl --user status gluetun.service --no-pager 2>/dev/null || echo "Not running (or service not found)"
|
|
echo ""
|
|
echo "=== thorium-dev-vpn ==="
|
|
podman_with_recovery ps --filter name=thorium-dev-vpn 2>/dev/null || echo "Not running"
|
|
;;
|
|
build)
|
|
echo "Building thorium-dev container..."
|
|
podman_with_recovery build -t localhost/thorium-wayland:latest "$REPO_DIR/containers/thorium-wayland/"
|
|
echo "Build complete."
|
|
;;
|
|
run)
|
|
if ! podman_with_recovery image exists localhost/thorium-wayland:latest 2>/dev/null; then
|
|
echo "Building thorium-dev container image..."
|
|
podman_with_recovery build -t localhost/thorium-wayland:latest "$REPO_DIR/containers/thorium-wayland/"
|
|
fi
|
|
|
|
echo "Starting VPN container (user service)..."
|
|
systemctl --user start gluetun.service
|
|
|
|
echo "Waiting for VPN connection..."
|
|
sleep 5
|
|
|
|
echo "Starting thorium-dev with native Wayland (Rootless) and localhost-only restrictions..."
|
|
podman_with_recovery run --rm -d \
|
|
--name thorium-dev-vpn \
|
|
--network=container:gluetun \
|
|
--cap-drop=ALL \
|
|
--userns=keep-id \
|
|
--shm-size=2g \
|
|
--device=/dev/dri \
|
|
-v "$RUNTIME_DIR/$W_DISPLAY:/tmp/$W_DISPLAY:ro" \
|
|
-v "$RUNTIME_DIR/pipewire-0:/tmp/pipewire-0:ro" \
|
|
-v "$RUNTIME_DIR/pulse:/tmp/pulse:ro" \
|
|
-v /etc/machine-id:/etc/machine-id:ro \
|
|
-e "WAYLAND_DISPLAY=$W_DISPLAY" \
|
|
-e "XDG_RUNTIME_DIR=/tmp" \
|
|
-e "PULSE_SERVER=unix:/tmp/pulse/native" \
|
|
-e "MOZ_ENABLE_WAYLAND=1" \
|
|
-v thorium-dev-vpn-data:/home/thorium-user/.config/thorium \
|
|
-e GTK_THEME=${cfg.gtkTheme} \
|
|
-e "GSETTINGS_BACKEND=keyfile" \
|
|
localhost/thorium-wayland:latest \
|
|
thorium-browser \
|
|
--ozone-platform=wayland \
|
|
--enable-features=UseOzonePlatform \
|
|
--enable-gpu-rasterization \
|
|
--enable-zero-copy \
|
|
--no-sandbox \
|
|
--proxy-server="http://127.0.0.1:65535" \
|
|
--proxy-bypass-list="localhost;127.0.0.1;host.containers.internal;*.local"
|
|
|
|
echo ""
|
|
echo "thorium-dev started! Window should appear on your desktop."
|
|
echo "This browser is restricted to localhost and host.containers.internal only."
|
|
;;
|
|
*)
|
|
echo "Usage: thorium-dev-vpn-backend {stop|status|build|run} <DISPLAY> <RUNTIME_DIR>"
|
|
exit 1
|
|
;;
|
|
esac
|
|
'';
|
|
|
|
# Kitty backend (special handling for config mounts)
|
|
kittyBackend = pkgs.writeShellScriptBin "kitty-vpn-backend" ''
|
|
ACTION="$1"
|
|
W_DISPLAY="$2"
|
|
RUNTIME_DIR="$3"
|
|
REPO_DIR="${cfg.repositoryPath}"
|
|
|
|
${podmanRecoveryHelper}
|
|
|
|
resolve_path() {
|
|
realpath "$1"
|
|
}
|
|
|
|
case "$ACTION" in
|
|
stop)
|
|
echo "Stopping containers..."
|
|
podman_with_recovery stop kitty-vpn 2>/dev/null || true
|
|
systemctl --user stop gluetun.service
|
|
echo "Containers stopped."
|
|
;;
|
|
status)
|
|
echo "=== gluetun ==="
|
|
systemctl --user status gluetun.service --no-pager 2>/dev/null || echo "Not running (or service not found)"
|
|
echo ""
|
|
echo "=== kitty-vpn ==="
|
|
podman_with_recovery ps --filter name=kitty-vpn 2>/dev/null || echo "Not running"
|
|
;;
|
|
build)
|
|
echo "Building Arch Kitty container..."
|
|
podman_with_recovery build -t localhost/arch-kitty:latest "$REPO_DIR/containers/arch-kitty/"
|
|
echo "Build complete."
|
|
;;
|
|
run)
|
|
if ! podman_with_recovery image exists localhost/arch-kitty:latest 2>/dev/null; then
|
|
echo "Building Arch Kitty container image..."
|
|
podman_with_recovery build -t localhost/arch-kitty:latest "$REPO_DIR/containers/arch-kitty/"
|
|
fi
|
|
|
|
echo "Starting VPN container (user service)..."
|
|
systemctl --user start gluetun.service
|
|
|
|
echo "Waiting for VPN connection..."
|
|
sleep 5
|
|
|
|
KITTY_CONF_DIR="${cfg.kittyConfigDir}"
|
|
KITTY_CONF_FILE="${cfg.kittyConfigDir}/kitty.conf"
|
|
BASHRC_FILE="${cfg.bashrcPath}"
|
|
|
|
REAL_KITTY_CONF=$(resolve_path "$KITTY_CONF_FILE")
|
|
REAL_BASHRC=$(resolve_path "$BASHRC_FILE")
|
|
|
|
echo "Starting Kitty with native Wayland (Rootless)..."
|
|
podman_with_recovery run --rm -d \
|
|
--name kitty-vpn \
|
|
--network=container:gluetun \
|
|
--cap-drop=ALL \
|
|
--userns=keep-id \
|
|
--shm-size=2g \
|
|
--device=/dev/dri \
|
|
-v "$RUNTIME_DIR/$W_DISPLAY:/tmp/$W_DISPLAY:ro" \
|
|
-v "$RUNTIME_DIR/pipewire-0:/tmp/pipewire-0:ro" \
|
|
-v "$RUNTIME_DIR/pulse:/tmp/pulse:ro" \
|
|
-v /etc/machine-id:/etc/machine-id:ro \
|
|
-v "$KITTY_CONF_DIR:/home/arch-user/.config/kitty:ro" \
|
|
-v "$REAL_KITTY_CONF:/home/arch-user/.config/kitty/kitty.conf:ro" \
|
|
-v "$REAL_BASHRC:/home/arch-user/.bashrc:ro" \
|
|
-v arch-user-home:/home/arch-user \
|
|
-e "WAYLAND_DISPLAY=$W_DISPLAY" \
|
|
-e "XDG_RUNTIME_DIR=/tmp" \
|
|
-e "PULSE_SERVER=unix:/tmp/pulse/native" \
|
|
localhost/arch-kitty:latest
|
|
|
|
echo ""
|
|
echo "Kitty started! Window should appear on your desktop."
|
|
;;
|
|
*)
|
|
echo "Usage: kitty-vpn-backend {stop|status|build|run} <DISPLAY> <RUNTIME_DIR>"
|
|
exit 1
|
|
;;
|
|
esac
|
|
'';
|
|
|
|
# Build list of enabled browsers
|
|
enabledPackages = lib.flatten [
|
|
(lib.optional (builtins.elem "firefox" cfg.browsers) [
|
|
firefoxBackend
|
|
(mkFrontendScript "firefox" firefoxBackend)
|
|
])
|
|
(lib.optional (builtins.elem "tor-browser" cfg.browsers) [
|
|
torBrowserBackend
|
|
(mkFrontendScript "tor-browser" torBrowserBackend)
|
|
])
|
|
(lib.optional (builtins.elem "thorium" cfg.browsers) [
|
|
thoriumBackend
|
|
(mkFrontendScript "thorium" thoriumBackend)
|
|
])
|
|
(lib.optional (builtins.elem "thorium-dev" cfg.browsers) [
|
|
thoriumDevBackend
|
|
(mkFrontendScript "thorium-dev" thoriumDevBackend)
|
|
])
|
|
(lib.optional (builtins.elem "kitty" cfg.browsers) [
|
|
kittyBackend
|
|
(mkFrontendScript "kitty" kittyBackend)
|
|
])
|
|
];
|
|
|
|
desktopEntriesPackage = pkgs.runCommand "browser-vpn-desktop-entries" { } ''
|
|
mkdir -p $out/share/applications
|
|
|
|
${lib.optionalString (builtins.elem "firefox" cfg.browsers) (
|
|
mkDesktopEntry "firefox" "Firefox" "firefox" "Network;WebBrowser" "browser;vpn;isolated;secure"
|
|
)}
|
|
${lib.optionalString (builtins.elem "tor-browser" cfg.browsers) (
|
|
mkDesktopEntry "tor-browser" "Tor Browser" "firefox" "Network;WebBrowser"
|
|
"browser;vpn;isolated;secure;tor;onion"
|
|
)}
|
|
${lib.optionalString (builtins.elem "thorium" cfg.browsers) (
|
|
mkDesktopEntry "thorium" "Thorium" "chromium" "Network;WebBrowser"
|
|
"browser;vpn;isolated;secure;chromium;thorium;privacy"
|
|
)}
|
|
${lib.optionalString (builtins.elem "thorium-dev" cfg.browsers) (
|
|
mkDesktopEntry "thorium-dev" "Thorium (Dev/Local)" "chromium" "Network;WebBrowser"
|
|
"browser;vpn;isolated;secure;chromium;thorium;dev;local"
|
|
)}
|
|
${lib.optionalString (builtins.elem "kitty" cfg.browsers) (
|
|
mkDesktopEntry "kitty" "Kitty" "kitty" "System;TerminalEmulator" "terminal;vpn;isolated;kitty;arch"
|
|
)}
|
|
'';
|
|
|
|
in
|
|
{
|
|
options.myModules.browserVpn = {
|
|
enable = lib.mkEnableOption "VPN-isolated browser containers";
|
|
|
|
browsers = lib.mkOption {
|
|
type = lib.types.listOf (
|
|
lib.types.enum [
|
|
"firefox"
|
|
"tor-browser"
|
|
"thorium"
|
|
"thorium-dev"
|
|
"kitty"
|
|
]
|
|
);
|
|
default = [
|
|
"firefox"
|
|
"tor-browser"
|
|
"thorium"
|
|
"thorium-dev"
|
|
"kitty"
|
|
];
|
|
description = "Which browsers to enable";
|
|
};
|
|
|
|
gtkTheme = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "Catppuccin-Mocha-Standard-Blue-Dark";
|
|
description = "GTK theme for browsers";
|
|
};
|
|
|
|
repositoryPath = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = config.myModules.system.repoPath;
|
|
description = "Path to repository containing container Dockerfiles";
|
|
};
|
|
|
|
kittyConfigDir = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/home/ashie/.config/kitty";
|
|
description = "Path to kitty configuration directory";
|
|
};
|
|
|
|
bashrcPath = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/home/ashie/.bashrc";
|
|
description = "Path to bashrc file for Kitty container";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
environment.systemPackages = enabledPackages ++ [ desktopEntriesPackage ];
|
|
};
|
|
}
|