Skip to content

Instantly share code, notes, and snippets.

@andrewhamon
Last active January 1, 2023 05:28
Show Gist options
  • Save andrewhamon/ed6b2e2b76a527a82e9cd80f4a4f75ca to your computer and use it in GitHub Desktop.
Save andrewhamon/ed6b2e2b76a527a82e9cd80f4a4f75ca to your computer and use it in GitHub Desktop.
Demonstration of how to run a nix service in an isolated network namespace with internet access only via VPN.

Execute the following to confirm everything

NET_NS=seedbox # Or whichever name you chose in your config

Ensure wireguard is working

ip netns exec $NET_NS wg

image

Check that the interfaces look right

ip netns exec $NET_NS ifconfig

image

Ensure that the expected processes are running in the namespace

ps $(ip netns pids $NET_NS)

image

{ config, pkgs, ... }:
{
imports = [
./hardware-configuration.nix
./namespaced-wg.nix
];
# Set up wireguard + a new network namespace using the module defined in hardware-configuration.nix
services.namespaced-wg.enable = true;
services.namespaced-wg.name = "seedbox"; # Name this whatever, but keep it short
services.namespaced-wg.ips = ["mulvad ipv4" "mulvad ipv6"];
services.namespaced-wg.peerPublicKey = "mulvad public key";
services.namespaced-wg.peerEndpoint = "mulvad.wireguard.endpoint:51820";
services.namespaced-wg.privateKeyFile = "/etc/secrets/wireguard_mullvad_key";
services.namespaced-wg.hostPortalIp = "10.69.44.1"; # Use a different subnet than your LAN
services.namespaced-wg.guestPortalIp = "10.69.44.2";
# Enable any service like normal, in this case we use transmission
services.transmission = {
enable = true;
settings = {
rpc-bind-address = "0.0.0.0";
rpc-port = 8080;
rpc-host-whitelist-enabled = false;
rpc-whitelist-enabled = false;
rpc-authentication-required = false;
watch-dir-enabled = true;
# peer-port = 1234; # Provision a port in mulvad UI and set it here
};
};
# This is the magic. Modify the transmission systemd config so that it runs under the seedbox network namespace
# It can only access the inernet via the wireguard VPN.
#
# To do this right, you need to correctly guess the name of the systemd service that corresponds to the nixos service.
# Usually its the same name, but to be sure you can check the source code in nixpkgs. If you are already running the
# service, you can also try `sudo systemctl status <name>.service` to see if you guessed right.
systemd.services.transmission = config.services.namespaced-wg.systemdMods;
}
# This file is filly parameterized. Feel free to copy it verbatim, add it to your imports list, and configure
# it. See configuration.nix for example configuration.
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.namespaced-wg;
in
{
options.services.namespaced-wg = {
enable = mkEnableOption "Namespaced Wireguard";
# Name should be relatively short. It is used for interface names, which
# seem to break if they exceed 15 characters.
name = mkOption {
type = types.str;
};
ips = mkOption {
type = types.listOf types.str;
};
peerPublicKey = mkOption {
type = types.str;
};
peerEndpoint = mkOption {
type = types.str;
};
peerAllowedIps = mkOption {
type = types.listOf types.str;
default = [ "0.0.0.0/0" "::/0" ];
};
privateKeyFile = mkOption {
type = types.str;
};
guestPortalIp = mkOption {
type = types.str;
};
hostPortalIp = mkOption {
type = types.str;
};
# This isn't exactly config, but I couldn't think of a better way to easily
# let us reference this attrset elsewhere. This is used to mod the systemd
# config of any existing service. These changes will cause the services to
# come up in the network namespace
systemdMods = mkOption {
type = types.anything;
default = {
after = ["network.target" "netns_${config.services.namespaced-wg.name}.service"];
bindsTo = ["netns_${config.services.namespaced-wg.name}.service"];
partOf = ["netns_${config.services.namespaced-wg.name}.service"];
serviceConfig.NetworkNamespacePath = "/var/run/netns/${config.services.namespaced-wg.name}";
};
};
};
config = mkIf cfg.enable {
networking.wireguard.interfaces."${cfg.name}" = {
ips = cfg.ips;
privateKeyFile = cfg.privateKeyFile;
interfaceNamespace = cfg.name;
peers = [
{
publicKey = cfg.peerPublicKey;
allowedIPs = cfg.peerAllowedIps;
endpoint = cfg.peerEndpoint;
}
];
};
# Modify the wireguard systemd service (implicitly defined using the wireguard
# module above) to wait for the netns_${cfg.name} service (defined below)
# to be active. This ensures that the network namespace has already been set
# up before creating the wireguard interface.
systemd.services."wireguard-${cfg.name}" = {
after = ["network.target" "network-online.target" "netns_${cfg.name}.service"];
bindsTo = ["netns_${cfg.name}.service"];
partOf = ["netns_${cfg.name}.service"];
};
# Create a systemd service that does the following:
# - creates a new netowrk namespace
# - creates an pair of veth interfaces and IP addresses that allow
# communication with processes inside the namespace. This is essential to
# be able to view UIs and such. Only very specific IP routes are added,
# these IPs and interfaces will not enable any communication beyond the
# host.
#
# The naming convention is that on the host outside of any network
# namespace, there is an interface named ${cfg.name}_portal. This connects
# directly to an interface that is moved inside of the network namespace,
# named "${cfg.name}_hportal". Software running inside the namesapce can
# bind to ${cfg.name}_hportal (or 0.0.0.0 to bind to all interfaces) and
# become accessible from outside the namespace only through the
# ${cfg.name}_portal interface.
systemd.services."netns_${cfg.name}" = {
description = "${cfg.name} network namespace";
before = [ "network.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "ip netns del ${cfg.name}";
};
script = ''
ipCmd="${pkgs.iproute}/bin/ip"
set -x
# Delete the ns if it already exists. Mostly handy for developemt, in
# case this setup fails partway through and leaves things in an odd
# state.
($ipCmd netns list | grep ${cfg.name}) && $ipCmd netns delete ${cfg.name}
$ipCmd netns add ${cfg.name}
# It seems like netns delete doesn't immediately clean up all the
# related resources. If we are too fast, recreating interfaces with
# the same name will fail. RIP.
sleep 3
$ipCmd link add ${cfg.name}_portal type veth peer ${cfg.name}_hportal
$ipCmd link set dev ${cfg.name}_hportal netns ${cfg.name}
$ipCmd addr add ${cfg.hostPortalIp}/32 dev ${cfg.name}_portal
$ipCmd netns exec ${cfg.name} $ipCmd addr add ${cfg.guestPortalIp}/32 dev ${cfg.name}_hportal
$ipCmd link set dev ${cfg.name}_portal up
$ipCmd route add ${cfg.guestPortalIp}/32 dev ${cfg.name}_portal
$ipCmd netns exec ${cfg.name} $ipCmd link set dev ${cfg.name}_hportal up
$ipCmd netns exec ${cfg.name} $ipCmd route add ${cfg.hostPortalIp}/32 dev ${cfg.name}_hportal
'';
};
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment