RSS Amplifier

Posts on Tarneo's blog · Oct 3, 2025

Selective wireguard use with a namespace

0
Sign in to vote or save

tarneo.fr

Today I decided to make a clean Wireguard client configuration on my dev computer, that would allow accessing any IP with any program while letting any other traffic out without going through it.

I quickly found a post on the infamous Pro Custodibus blog, which contains many great Wireguard configs I’ve used countless times (Renn.es’s VPS-to-homeserver tunnel couldn’t have been done without it).

Architecture

We will simply create a network namespace wg0 which will contain the Wireguard interface, so that any traffic in this namespace goes through Wireguard. Userland access to this namespace will be touched on later.

Execution

I made a little script that puts everything together (detailed explanations can be found on Custodibus, I won’t repeat these):

#!/usr/bin/env bash

# tunnel: Put wireguard in its own network namespace for selective use.
# Based on [1].
# Used in conjunction with wgx to access the namespace from userspace.
#
# [1]: https://www.procustodibus.com/blog/2023/04/wireguard-netns-for-specific-apps/

set -e

if [ "$1" = "check" ]; then
    ip netns show wg0 | grep -q wg0 || {
        echo "starting tunnel"
        exec doas "$0" up
    }
    exit 0
fi

if [ $EUID -ne 0 ]; then
    echo "Please run this script as root. Exiting."
    exit 1
fi

NAMESPACE=wg0
INTERFACE=wg0

case $1 in
up)
    ip netns add $NAMESPACE         # Create namespace
    ip -n $NAMESPACE link set lo up # Start loopback

    ip link add $INTERFACE type wireguard   # Create empty interface
    ip link set $INTERFACE netns $NAMESPACE # Move into namespace

    # Configure the interface
    ip netns exec $NAMESPACE wg setconf $INTERFACE /etc/wireguard/wg0.conf

    # Set the interface address (matches address in config file)
    ip -n $NAMESPACE address add 10.9.0.2/32 dev $INTERFACE

    ip -n $NAMESPACE link set $INTERFACE up # Start interface

    # Set wg as default route for the namespace
    ip -n $NAMESPACE route add default dev $INTERFACE
    ;;

down)
    ip netns pids $NAMESPACE | xargs -r kill # Kill wireguard
    ip netns delete $NAMESPACE               # Remove namespace
    ;;
esac

Note that /etc/wireguard/wg0.conf needs to be stripped from its Address, MTU, PreUp and most other [Interface] fields, which are already set by the script.

After running doas ./tunnel up, the namespace will be usable like this:

doas ip netns exec wg0 doas -u $(whoami) ssh chorizo

But this isn’t very convenient, I don’t want to have to authenticate locally just to start an SSH session.

Accessing the namespace from userspace

This is where we’ll get our hands dirty, since it requires writing a custom program with the setuid bit set. This is what’s used by privilege escalation tools like sudo, and it simply allows a program run by a user to run as effective root.

It requires additional caution though, as any arbitrary execution that can be done in our program could be used by local bad actors: remember, it’s privilege escalation. I am pretty serene on this though, because a hacker going through the hassle of finding a mistake in this could simply use a software keylogger to get my root password. I’ll still try to make something secure though.

Here’s the program. A more recent version may be available in my dotfiles, along with the nix package definition.

// Allow a non-root user to run an arbitrary command in a network namespace,
// useful for a wireguard namespace.

// compile with: gcc -s -o wgx wgx.c
// then run: sudo chmod u+s wgx

// example usage: wgx ip netns identify

// For a cleaner SSH setup, ProxyCommand can be set as follows :
// ProxyCommand = "sh -c 'tunnel check && wgx nc %h %p'";

#define NAME_OF_NETWORK_NAMESPACE  "wg0"
#define PATH_TO_NAMESPACE  "/run/netns/"  NAME_OF_NETWORK_NAMESPACE

#define _GNU_SOURCE

#include <fcntl.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#define die(s, msg, ...) { \
	printf("%s: " msg "\n", basename(argv[0]) __VA_OPT__(,) __VA_ARGS__); \
	exit(s); \
}

int
main(int argc, char **argv) {
	if(!(geteuid() == 0))
		die(3,
		    "This program needs to be run as effective root."
		    "Is the binary owned by root and is the setuid bit set ?");

	int fd = open(PATH_TO_NAMESPACE, O_RDONLY);
	if(fd == -1)
		die(2,
		    "Namespace " NAME_OF_NETWORK_NAMESPACE
		    " does not exist.");
	if(!(setns(fd, CLONE_NEWNET) == 0))
		die(-1, "Couldn't set namespace.");
	if(!(close(fd) == 0))
		die(-1, "Couldn't close ns fd");

	if(!(setgid(getgid()) == 0 && setuid(getuid()) == 0))
		die(-1, "Couldn't drop effective root or root group");

	if(argc > 1) {
		execvpe(argv[1], argv + 1, environ);
		int errsv = errno;
		const char *errstr = strerrorname_np(errsv);
		die(1, "Got error %d (%s) while running command", errsv,
		    errstr);
	} else
		die(1, "A command is required.");
}

This should be pretty self-explanatory to anyone with basic C knowledge. We just check we have effective root, do the namespace change, deescalate privileges and exec() the command from arguments.

On NixOS, setting suid can be done as follows (on other OSes, just put the binary in /bin after changing owner and setting suid, making sure it is not world-writable):

security.wrappers.wgx = {
  setuid = true;
  owner = "root";
  group = "root";
  source = "${pkgs.wgx}/bin/wgx";
};

This assumes that the script has been compiled into the store as pkgs.wgx.

It can then be used instead of ip netns exec:

wgx ssh chorizo

SSH config

I’m editing this post to add that SSH can be easily configured to use this, using ProxyCommand (which replaces SSH’s socket connection). Here’s the Nix config (the bare SSH config is in wgx’s header):

programs.ssh.matchBlocks."chorizo" = {
  proxyCommand = "sh -c 'tunnel check && wgx nc %h %p'";
};

Multiple hostnames can be specified in the quotes, separated by a space.

Read the original on tarneo.fr

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.