Compare commits

...

6 Commits

6 changed files with 125 additions and 154 deletions

View File

@ -2,87 +2,113 @@
{ lib, ... }:
{
# disable non-required packages like nano, perl, rsync, strace
environment.defaultPackages = [];
# remove a few items from /run/wrappers we don't need.
options.security.wrappers = lib.mkOption {
apply = lib.filterAttrs (name: _: !(builtins.elem name [
# wrappers from <repo:nixos/nixpkgs:nixos/modules/programs/shadow.nix>
"newgidmap"
"newgrp"
"newuidmap"
# "sg"
# "su"
# wrappers from <repo:nixos/nixpkgs:nixos/modules/security/pam.nix>
# may need to patch e.g. `pam` package (pam_unix) to not refer to unix_chkpwd by path
"unix_chkpwd"
]));
};
# remove all the non-existent default directories from XDG_DATA_DIRS, XDG_CONFIG_DIRS to simplify debugging.
# this is defaulted in <repo:nixos/nixpkgs:nixos/modules/programs/environment.nix>,
# without being gated by any higher config.
environment.profiles = lib.mkForce [
"/etc/profiles/per-user/$USER"
"/run/current-system/sw"
];
config = {
nixpkgs.overlays = [(self: super: {
pam = super.pam.overrideAttrs (upstream: {
postPatch = (if upstream.postPatch != null then upstream.postPatch else "") + ''
substituteInPlace modules/pam_unix/Makefile.am --replace-fail \
"/run/wrappers/bin/unix_chkpwd" "$out"
'';
});
})];
# NIXPKGS_CONFIG defaults to "/etc/nix/nixpkgs-config.nix" in <nixos/modules/programs/environment.nix>.
# that's never existed on my system and everything does fine without it set empty (no nixpkgs API to forcibly *unset* it).
environment.variables.NIXPKGS_CONFIG = lib.mkForce "";
# XDG_CONFIG_DIRS defaults to "/etc/xdg", which doesn't exist.
# in practice, pam appends the values i want to XDG_CONFIG_DIRS, though this approach causes an extra leading `:`
environment.sessionVariables.XDG_CONFIG_DIRS = lib.mkForce [];
# XCURSOR_PATH: defaults to `[ "$HOME/.icons" "$HOME/.local/share/icons" ]`, neither of which i use, just adding noise.
# see: <repo:nixos/nixpkgs:nixos/modules/config/xdg/icons.nix>
environment.sessionVariables.XCURSOR_PATH = lib.mkForce [];
# disable non-required packages like nano, perl, rsync, strace
environment.defaultPackages = [];
# disable nixos' portal module, otherwise /share/applications gets linked into the system and complicates things (sandboxing).
# instead, i manage portals myself via the sane.programs API (e.g. sane.programs.xdg-desktop-portal).
xdg.portal.enable = false;
xdg.menus.enable = false; #< links /share/applications, and a bunch of other empty (i.e. unused) dirs
# remove all the non-existent default directories from XDG_DATA_DIRS, XDG_CONFIG_DIRS to simplify debugging.
# this is defaulted in <repo:nixos/nixpkgs:nixos/modules/programs/environment.nix>,
# without being gated by any higher config.
environment.profiles = lib.mkForce [
"/etc/profiles/per-user/$USER"
"/run/current-system/sw"
];
# xdg.autostart.enable defaults to true, and links /etc/xdg/autostart into the environment, populated with .desktop files.
# see: <repo:nixos/nixpkgs:nixos/modules/config/xdg/autostart.nix>
# .desktop files are a questionable way to autostart things: i generally prefer a service manager for that.
xdg.autostart.enable = false;
# NIXPKGS_CONFIG defaults to "/etc/nix/nixpkgs-config.nix" in <nixos/modules/programs/environment.nix>.
# that's never existed on my system and everything does fine without it set empty (no nixpkgs API to forcibly *unset* it).
environment.variables.NIXPKGS_CONFIG = lib.mkForce "";
# XDG_CONFIG_DIRS defaults to "/etc/xdg", which doesn't exist.
# in practice, pam appends the values i want to XDG_CONFIG_DIRS, though this approach causes an extra leading `:`
environment.sessionVariables.XDG_CONFIG_DIRS = lib.mkForce [];
# XCURSOR_PATH: defaults to `[ "$HOME/.icons" "$HOME/.local/share/icons" ]`, neither of which i use, just adding noise.
# see: <repo:nixos/nixpkgs:nixos/modules/config/xdg/icons.nix>
environment.sessionVariables.XCURSOR_PATH = lib.mkForce [];
# nix.channel.enable: populates `/nix/var/nix/profiles/per-user/root/channels`, `/root/.nix-channels`, `$HOME/.nix-defexpr/channels`
# <repo:nixos/nixpkgs:nixos/modules/config/nix-channel.nix>
# TODO: may want to recreate NIX_PATH, nix.settings.nix-path
nix.channel.enable = false;
# disable nixos' portal module, otherwise /share/applications gets linked into the system and complicates things (sandboxing).
# instead, i manage portals myself via the sane.programs API (e.g. sane.programs.xdg-desktop-portal).
xdg.portal.enable = false;
xdg.menus.enable = false; #< links /share/applications, and a bunch of other empty (i.e. unused) dirs
# environment.stub-ld: populate /lib/ld-linux.so with an object that unconditionally errors on launch,
# so as to inform when trying to run a non-nixos binary?
# IMO that's confusing: i thought /lib/ld-linux.so was some file actually required by nix.
environment.stub-ld.enable = false;
# xdg.autostart.enable defaults to true, and links /etc/xdg/autostart into the environment, populated with .desktop files.
# see: <repo:nixos/nixpkgs:nixos/modules/config/xdg/autostart.nix>
# .desktop files are a questionable way to autostart things: i generally prefer a service manager for that.
xdg.autostart.enable = false;
# `less.enable` sets LESSKEYIN_SYSTEM, LESSOPEN, LESSCLOSE env vars, which does confusing "lesspipe" things, so disable that.
# it's enabled by default from `<nixos/modules/programs/environment.nix>`, who also sets `PAGER="less"` and `EDITOR="nano"` (keep).
programs.less.enable = lib.mkForce false;
environment.variables.PAGER = lib.mkOverride 900 ""; # mkDefault sets 1000. non-override is 100. 900 will beat the nixpkgs `mkDefault` but not anyone else.
environment.variables.EDITOR = lib.mkOverride 900 "";
# nix.channel.enable: populates `/nix/var/nix/profiles/per-user/root/channels`, `/root/.nix-channels`, `$HOME/.nix-defexpr/channels`
# <repo:nixos/nixpkgs:nixos/modules/config/nix-channel.nix>
# TODO: may want to recreate NIX_PATH, nix.settings.nix-path
nix.channel.enable = false;
# several packages (dconf, modemmanager, networkmanager, gvfs, polkit, udisks, bluez/blueman, feedbackd, etc)
# will add themselves to the dbus search path.
# i prefer dbus to only search XDG paths (/share/dbus-1) for service files, as that's more introspectable.
# see: <repo:nixos/nixpkgs:nixos/modules/services/system/dbus.nix>
# TODO: sandbox dbus? i pretty explicitly don't want to use it as a launcher.
services.dbus.packages = lib.mkForce [
"/run/current-system/sw"
# config.system.path
# pkgs.dbus
# pkgs.polkit.out
# pkgs.modemmanager
# pkgs.networkmanager
# pkgs.udisks
# pkgs.wpa_supplicant
];
# environment.stub-ld: populate /lib/ld-linux.so with an object that unconditionally errors on launch,
# so as to inform when trying to run a non-nixos binary?
# IMO that's confusing: i thought /lib/ld-linux.so was some file actually required by nix.
environment.stub-ld.enable = false;
# systemd by default forces shitty defaults for e.g. /tmp/.X11-unix.
# nixos propagates those in: <nixos/modules/system/boot/systemd/tmpfiles.nix>
# by overwriting this with an empty file, we can effectively remove it.
environment.etc."tmpfiles.d/x11.conf".text = "# (removed by Colin)";
# `less.enable` sets LESSKEYIN_SYSTEM, LESSOPEN, LESSCLOSE env vars, which does confusing "lesspipe" things, so disable that.
# it's enabled by default from `<nixos/modules/programs/environment.nix>`, who also sets `PAGER="less"` and `EDITOR="nano"` (keep).
programs.less.enable = lib.mkForce false;
environment.variables.PAGER = lib.mkOverride 900 ""; # mkDefault sets 1000. non-override is 100. 900 will beat the nixpkgs `mkDefault` but not anyone else.
environment.variables.EDITOR = lib.mkOverride 900 "";
# see: <nixos/modules/tasks/swraid.nix>
# it was enabled by default before 23.11
boot.swraid.enable = lib.mkDefault false;
# several packages (dconf, modemmanager, networkmanager, gvfs, polkit, udisks, bluez/blueman, feedbackd, etc)
# will add themselves to the dbus search path.
# i prefer dbus to only search XDG paths (/share/dbus-1) for service files, as that's more introspectable.
# see: <repo:nixos/nixpkgs:nixos/modules/services/system/dbus.nix>
# TODO: sandbox dbus? i pretty explicitly don't want to use it as a launcher.
services.dbus.packages = lib.mkForce [
"/run/current-system/sw"
# config.system.path
# pkgs.dbus
# pkgs.polkit.out
# pkgs.modemmanager
# pkgs.networkmanager
# pkgs.udisks
# pkgs.wpa_supplicant
];
# see: <nixos/modules/system/boot/kernel.nix>
# by default, it adds to boot.initrd.availableKernelModules:
# - SATA: "ahci" "sata_nv" "sata_via" "sata_sis" "sata_uli" "ata_piix" "pata_marvell"
# - "nvme"
# - scsi: "sd_mod" "sr_mod"
# - SD/eMMC: "mmc_block"
# - USB keyboards: "uhci_hcd" "ehci_hcd" "ehci_pci" "ohci_hcd" "ohci_pci" "xhci_hcd" "xhci_pci" "usbhid" "hid_generic" "hid_lenovo" "hid_apple" "hid_roccat" "hid_logitech_hidpp" "hid_logitech_dj" "hid_microsoft" "hid_cherry" "hid_corsair"
# - LVM: "dm_mod"
# - on x86 only: more keyboard stuff: "pcips2" "atkbd" "i8042"
# systemd by default forces shitty defaults for e.g. /tmp/.X11-unix.
# nixos propagates those in: <nixos/modules/system/boot/systemd/tmpfiles.nix>
# by overwriting this with an empty file, we can effectively remove it.
environment.etc."tmpfiles.d/x11.conf".text = "# (removed by Colin)";
boot.initrd.includeDefaultModules = lib.mkDefault false;
# see: <nixos/modules/tasks/swraid.nix>
# it was enabled by default before 23.11
boot.swraid.enable = lib.mkDefault false;
# see: <nixos/modules/system/boot/kernel.nix>
# by default, it adds to boot.initrd.availableKernelModules:
# - SATA: "ahci" "sata_nv" "sata_via" "sata_sis" "sata_uli" "ata_piix" "pata_marvell"
# - "nvme"
# - scsi: "sd_mod" "sr_mod"
# - SD/eMMC: "mmc_block"
# - USB keyboards: "uhci_hcd" "ehci_hcd" "ehci_pci" "ohci_hcd" "ohci_pci" "xhci_hcd" "xhci_pci" "usbhid" "hid_generic" "hid_lenovo" "hid_apple" "hid_roccat" "hid_logitech_hidpp" "hid_logitech_dj" "hid_microsoft" "hid_cherry" "hid_corsair"
# - LVM: "dm_mod"
# - on x86 only: more keyboard stuff: "pcips2" "atkbd" "i8042"
boot.initrd.includeDefaultModules = lib.mkDefault false;
};
}

View File

@ -572,16 +572,16 @@ in
iotop.sandbox.capabilities = [ "net_admin" ];
# provides `ip`, `routel`, `bridge`, others.
# landlock works fine for most of these, but `ip netns exec` uses namespaces internally,
# and that's incompatible with landlock
iproute2.sandbox.method = "bwrap";
iproute2.sandbox.net = "all";
iproute2.sandbox.capabilities = [ "net_admin" ];
iproute2.sandbox.extraPaths = [
"/run/netns" # for `ip netns ...` to work, but maybe not needed anymore?
"/sys/class/net" # for `ip netns ...` to work
"/var/run/netns"
];
# landlock works fine for most of these, but `ip netns exec` wants to attach to an existing namespace
# and that means we can't use ANY sandboxer for it.
iproute2.sandbox.enable = false;
# iproute2.sandbox.net = "all";
# iproute2.sandbox.capabilities = [ "net_admin" ];
# iproute2.sandbox.extraPaths = [
# "/run/netns" # for `ip netns ...` to work, but maybe not needed anymore?
# "/sys/class/net" # for `ip netns ...` to work
# "/var/run/netns"
# ];
iptables.sandbox.method = "landlock";
iptables.sandbox.net = "all";

View File

@ -229,7 +229,6 @@ in
fwmark=${builtins.toString vpnCfg.fwmark}
priorityMain=${builtins.toString vpnCfg.priorityMain}
priorityFwMark=${builtins.toString vpnCfg.priorityFwMark}
bridgeDevice=${vpnCfg.bridgeDevice}
addrV4=${vpnCfg.addrV4}
name=${vpnCfg.name}
dns=(${lib.concatStringsSep " " vpnCfg.dns})

View File

@ -51,7 +51,7 @@ let
"/etc" #< especially for /etc/profiles/per-user/$USER/bin
"/run/current-system" #< for basics like `ls`, and all this program's `suggestedPrograms` (/run/current-system/sw/bin)
"/run/wrappers" #< SUID wrappers. TODO: remove!
# "/run/wrappers" #< SUID wrappers. they don't mean much inside a namespace.
# /run/opengl-driver is a symlink into /nix/store; needed by e.g. mpv
"/run/opengl-driver"
"/run/opengl-driver-32" #< XXX: doesn't exist on aarch64?

View File

@ -6,18 +6,21 @@
# - wireguard (arch): <https://wiki.archlinux.org/title/WireGuard>
#
# to route all internet traffic through a VPN endpoint, run `systemctl start vpn-${vpnName}`
# to route an application's traffic through a VPN: `sane-vpn do ${vpnName} ${command[@]}`
# to show the routing table: `ip rule`
# to show the NAT rules used for bridging: `sudo iptables -t nat --list-rules -v`
#
# the rough idea here is:
# 1. each VPN has an IP address: if we originate a packet, and the source address is the VPN's address, then it gets routed over the VPN trivially.
# 2a. create a bridge device for each VPN. traffic exiting that bridge is source-NAT'd to the VPN's address.
# 2b. to route all traffic for a specific application (or container), give it access to only the bridge device.
# 3a. create a separate routing table for each VPN, with table id = ID.
# 3b. if a packet enters the VPN's table then it will be routed via the VPN.
# 3c. to apply a VPN to all internet traffic, system-wide, a rule is added that forces each packet to enter that VPN's routing table.
# 2a. create a separate routing table for each VPN, with table id = ID.
# 2b. if a packet enters the VPN's table then it will be routed via the VPN.
# 2c. to apply a VPN to all internet traffic, system-wide, a rule is added that forces each packet to enter that VPN's routing table.
# - that's done with `systemctl start vpn-$VPN`.
# - the VPN acts as the default route. so traffic destined to e.g. a LAN device do not traverse the VPN in this case. only internet traffic is VPN'd.
# 3. to apply a VPN to internet traffic selectively, just proxy an applications traffic into the VPN device
# 3a. use a network namespace and a userspace TCP stack (e.g. pasta/slirp4netns).
# 3b. attach the VPN device to a bridge device, then connect that to a network namespace by using a veth pair.
# 3c. juse use `sanebox`, which abstracts the above options.
{ config, lib, pkgs, sane-lib, ... }:
let
@ -41,6 +44,7 @@ let
fwmark = mkOption {
type = types.int;
};
# priority*: used externally, by e.g. `sane-vpn`
priorityMain = mkOption {
type = types.int;
};
@ -86,13 +90,6 @@ let
dns servers to use for traffic associated with this VPN.
'';
};
bridgeDevice = mkOption {
type = types.str;
default = "br-${name}";
description = ''
name of the bridge net device which will be created and configured so as to route all its outbound traffic over the VPN.
'';
};
privateKeyFile = mkOption {
type = types.either types.str types.path;
description = ''
@ -111,9 +108,7 @@ let
priorityFwMark = config.id + 300;
};
});
mkVpnConfig = name: { id, dns, endpoint, publicKey, addrV4, privateKeyFile, bridgeDevice, priorityMain, priorityWgTable, priorityFwMark, fwmark, ... }: let
bridgeAddrV4 = "10.20.${builtins.toString id}.1/24";
in {
mkVpnConfig = name: { id, dns, endpoint, publicKey, addrV4, privateKeyFile, priorityMain, priorityWgTable, priorityFwMark, fwmark, ... }: {
assertions = [
{
assertion = (lib.count (c: c.id == id) (builtins.attrValues cfg)) == 1;
@ -153,19 +148,6 @@ let
# networkConfig.DNSDefaultRoute = true;
# Domains = ~.: system DNS queries are sent to this link's DNS server
# networkConfig.Domains = "~.";
routingPolicyRules = [
{
routingPolicyRuleConfig = {
# send traffic from the container bridge out over the VPN.
# the bridge itself does source nat (SNAT) to rewrite the packet source address to that of the VPNs
# -- but that happens in POSTROUTING: *after* the kernel decides which interface to give the packet to next.
# therefore, we have to route here based on the packet's address as it is in PREROUTING, i.e. the bridge address. weird!
Priority = priorityWgTable;
From = bridgeAddrV4;
Table = id;
};
}
];
routes = [{
routeConfig.Table = id;
routeConfig.Scope = "link";
@ -176,35 +158,6 @@ let
linkConfig.RequiredForOnline = false;
};
systemd.network.netdevs."99-${bridgeDevice}" = {
netdevConfig.Kind = "bridge";
netdevConfig.Name = bridgeDevice;
};
systemd.network.networks."51-${bridgeDevice}" = {
matchConfig.Name = bridgeDevice;
networkConfig.Description = "NATs inbound traffic to ${name}, intended for container isolation";
networkConfig.Address = [ bridgeAddrV4 ];
networkConfig.DNS = dns;
# ConfigureWithoutCarrier: a bridge with no attached interface has no carrier (this is to be expected).
# systemd ordinarily waits for a carrier to be present before "configuring" the bridge.
# i tell it to not do that, so that i can assign an IP address to the bridge before i connect it to anything.
# alternative may be to issue the bridge a static MACAddress?
# see: <https://github.com/systemd/systemd/issues/9252#issuecomment-808710185>
networkConfig.ConfigureWithoutCarrier = true;
# networkConfig.DHCPServer = true;
linkConfig.RequiredForOnline = false;
};
networking.localCommands = with pkgs; ''
# view rules with:
# - `sudo iptables -t nat --list-rules -v`
# rewrite the source address of every packet leaving the container so that it's routable by the wg tunnel.
# note that this alone doesn't get it routed *to* the wg device. we can't, since SNAT is POSTROUTING (not PREROUTING).
# that part's done by an `ip rule` entry.
${iptables}/bin/iptables -A POSTROUTING -t nat -s ${bridgeAddrV4} -j SNAT --to-source ${addrV4}
'';
# linux will drop inbound packets if it thinks a reply to that packet wouldn't exit via the same interface (rpfilter).
# wg-quick has a solution via `iptables -j CONNMARK`, and that does work for system-wide VPNs,
# but i couldn't get that to work for netns with SNAT, so set rpfilter to "loose".

View File

@ -24,12 +24,13 @@ vpns=()
defaultVpn=
# loaded from a specific ~/.config/sane-vpn/vpns profile
id=
fwmark=
prioMain=
prioFwMark=
bridgeDevice=
addrV4=
dns=()
fwmark=
id=
name=
priorityFwMark=
priorityMain=
debug() {
if [ -n "$SANE_VPN_DEBUG" ]; then
@ -46,15 +47,7 @@ getVpns() {
# load a specific VPN profile, `"$1"`
sourceVpn() {
# populates:
# - id
# - fwmark
# - prioMain
# - prioFwMark
# - bridgeDevice
# - addrV4
# - name
# - dns
# populates: variables declared above
debug "sourcing: ~/.config/sane-vpn/vpns/$1"
# TODO: don't blindly source this, but parse explicitly as `K=V`
source ~/.config/sane-vpn/vpns/$1