[PATCH] wg-quick: add support for socket netns separate from interface netns
Sybil Isabel Dorsett
sybdorsett at proton.me
Wed Jan 14 12:08:52 UTC 2026
wg-quick creates and enables the WireGuard interface in a single
network namespace, with no provision to separate the socket netns
from the interface netns. This prevents use of WireGuard’s supported
model where sockets reside in a different netns than the interface,
as documented at https://www.wireguard.com/netns/. This limitation
cannot be addressed via a systemd drop-in, as wg-quick hardcodes
the namespace context used during interface creation and configuration,
forcing users to reimplement wg-quick logic in a custom service,
including interface lifecycle, address assignment, routing, and teardown.
Add support to wg-quick for selecting a network namespace in which
the WireGuard interface is initially created. Create the interface
in the specified netns so that sockets are bound there, then move
the interface into the invoking netns before execution of PreUp hooks.
This aligns wg-quick behavior with existing WireGuard kernel and userspace
capabilities and removes the need for manual service reimplementation.
Signed-off-by: Sybil Isabel Dorsett <sybdorsett at proton.me>
---
contrib/highlighter/gui/highlight.cpp | 1 +
contrib/highlighter/highlight.c | 1 +
contrib/highlighter/highlighter.c | 23 +++++++++++++++++++++++
contrib/highlighter/highlighter.h | 1 +
src/man/wg-quick.8 | 5 +++++
src/wg-quick/linux.bash | 15 ++++++++++++---
6 files changed, 43 insertions(+), 3 deletions(-)
diff --git a/contrib/highlighter/gui/highlight.cpp b/contrib/highlighter/gui/highlight.cpp
index a95857b..7c4e4ac 100644
--- a/contrib/highlighter/gui/highlight.cpp
+++ b/contrib/highlighter/gui/highlight.cpp
@@ -25,6 +25,7 @@ static QColor colormap[] = {
[HighlightDelimiter] = QColor("#7aa6da"),
#ifndef MOBILE_WGQUICK_SUBSET
[HighlightTable] = QColor("#c397d8"),
+ [HighlightSocketNamespace] = QColor("#c397d8"),
[HighlightFwMark] = QColor("#c397d8"),
[HighlightSaveConfig] = QColor("#c397d8"),
[HighlightCmd] = QColor("#969896"),
diff --git a/contrib/highlighter/highlight.c b/contrib/highlighter/highlight.c
index e9034f7..1bd6c07 100644
--- a/contrib/highlighter/highlight.c
+++ b/contrib/highlighter/highlight.c
@@ -51,6 +51,7 @@ static const char *colormap[] = {
[HighlightDelimiter] = TERMINAL_FG_CYAN,
#ifndef MOBILE_WGQUICK_SUBSET
[HighlightTable] = TERMINAL_FG_BLUE,
+ [HighlightSocketNamespace] = TERMINAL_FG_BLUE,
[HighlightFwMark] = TERMINAL_FG_BLUE,
[HighlightSaveConfig] = TERMINAL_FG_BLUE,
[HighlightCmd] = TERMINAL_FG_WHITE,
diff --git a/contrib/highlighter/highlighter.c b/contrib/highlighter/highlighter.c
index d89feda..650cc7f 100644
--- a/contrib/highlighter/highlighter.c
+++ b/contrib/highlighter/highlighter.c
@@ -223,6 +223,24 @@ static bool is_valid_persistentkeepalive(string_span_t s)
#ifndef MOBILE_WGQUICK_SUBSET
+static bool is_valid_filename(string_span_t s)
+{
+ if (s.len > 128 || !s.len)
+ return false;
+ if (s.len == 1 && s.s[0] == '.')
+ return false;
+ if (s.len == 2 && s.s[0] == '.' && s.s[1] == '.')
+ return false;
+ if (s.s[0] == '-')
+ return false;
+ for (size_t i = 0; i < s.len; ++i) {
+ if (!is_alphabet(s.s[i]) && !is_decimal(s.s[i]) &&
+ s.s[i] != '_' && s.s[i] != '-' && s.s[i] != '.')
+ return false;
+ }
+ return true;
+}
+
static bool is_valid_fwmark(string_span_t s)
{
if (is_same(s, "off"))
@@ -345,6 +363,7 @@ enum field {
DNS,
MTU,
#ifndef MOBILE_WGQUICK_SUBSET
+ SocketNamespace,
FwMark,
Table,
PreUp, PostUp, PreDown, PostDown,
@@ -384,6 +403,7 @@ static enum field get_field(string_span_t s)
check_enum(Endpoint);
check_enum(PersistentKeepalive);
#ifndef MOBILE_WGQUICK_SUBSET
+ check_enum(SocketNamespace);
check_enum(FwMark);
check_enum(Table);
check_enum(PreUp);
@@ -526,6 +546,9 @@ static void highlight_value(struct highlight_span_array *ret, const string_span_
case SaveConfig:
append_highlight_span(ret, parent.s, s, is_valid_saveconfig(s) ? HighlightSaveConfig : HighlightError);
break;
+ case SocketNamespace:
+ append_highlight_span(ret, parent.s, s, is_valid_filename(s) ? HighlightSocketNamespace : HighlightError);
+ break;
case FwMark:
append_highlight_span(ret, parent.s, s, is_valid_fwmark(s) ? HighlightFwMark : HighlightError);
break;
diff --git a/contrib/highlighter/highlighter.h b/contrib/highlighter/highlighter.h
index 65cc230..7fe0a6d 100644
--- a/contrib/highlighter/highlighter.h
+++ b/contrib/highlighter/highlighter.h
@@ -21,6 +21,7 @@ enum highlight_type {
HighlightDelimiter,
#ifndef MOBILE_WGQUICK_SUBSET
HighlightTable,
+ HighlightSocketNamespace,
HighlightFwMark,
HighlightSaveConfig,
HighlightCmd,
diff --git a/src/man/wg-quick.8 b/src/man/wg-quick.8
index bc9e145..1a7c9a6 100644
--- a/src/man/wg-quick.8
+++ b/src/man/wg-quick.8
@@ -102,6 +102,11 @@ the commands are executed in order.
SaveConfig \(em if set to `true', the configuration is saved from the current state of the
interface upon shutdown. Any changes made to the configuration file before the
interface is removed will therefore be overwritten.
+.IP \(bu
+SocketNamespace \(em the name of an existing network namespace (netns)
+in which the interface's UDP sockets are created. If specified, the interface
+is first added to that netns, then moved to the invoking process's native netns
+before any other interface settings are applied.
.P
Recommended \fIINTERFACE\fP names include `wg0' or `wgvpn0' or even `wgmgmtlan0'.
diff --git a/src/wg-quick/linux.bash b/src/wg-quick/linux.bash
index 34fa5f9..db32f0d 100755
--- a/src/wg-quick/linux.bash
+++ b/src/wg-quick/linux.bash
@@ -13,6 +13,7 @@ export PATH="${SELF%/*}:$PATH"
WG_CONFIG=""
INTERFACE=""
+SOCKET_NAMESPACE=""
ADDRESSES=( )
MTU=""
DNS=( )
@@ -56,6 +57,7 @@ parse_options() {
[[ $key == "[Interface]" ]] && interface_section=1
if [[ $interface_section -eq 1 ]]; then
case "$key" in
+ SocketNamespace) SOCKET_NAMESPACE="$value"; continue ;;
Address) ADDRESSES+=( ${value//,/ } ); continue ;;
MTU) MTU="$value"; continue ;;
DNS) for v in ${value//,/ }; do
@@ -88,12 +90,14 @@ auto_su() {
add_if() {
local ret
- if ! cmd ip link add dev "$INTERFACE" type wireguard; then
+ trap 'cmd "${netns_exec[@]}" ip link delete dev "$INTERFACE"; exit' INT TERM EXIT
+ if ! cmd "${netns_exec[@]}" ip link add dev "$INTERFACE" type wireguard; then
ret=$?
[[ -e /sys/module/wireguard ]] || ! command -v "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wireguard-go}" >/dev/null && exit $ret
echo "[!] Missing WireGuard kernel module. Falling back to slow userspace implementation." >&2
- cmd "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wireguard-go}" "$INTERFACE"
+ cmd "${netns_exec[@]}" "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wireguard-go}" "$INTERFACE"
fi
+ [[ -z "$SOCKET_NAMESPACE" ]] || cmd "${netns_exec[@]}" ip link set "$INTERFACE" netns $$
}
del_if() {
@@ -256,6 +260,7 @@ save_config() {
local old_umask new_config current_config address cmd
[[ $(ip -all -brief address show dev "$INTERFACE") =~ ^$INTERFACE\ +\ [A-Z]+\ +(.+)$ ]] || true
new_config=$'[Interface]\n'
+ [[ -z "$SOCKET_NAMESPACE" ]] || new_config+="SocketNamespace = $SOCKET_NAMESPACE"$'\n'
for address in ${BASH_REMATCH[1]}; do
new_config+="Address = $address"$'\n'
done
@@ -326,9 +331,13 @@ cmd_usage() {
cmd_up() {
local i
+ local netns_exec=()
+ [[ -z "$SOCKET_NAMESPACE" ]] || netns_exec=(ip netns exec "$SOCKET_NAMESPACE")
+ "${netns_exec[@]}" true || die "Network namespace '${SOCKET_NAMESPACE:-<unset>}' does not exist"
[[ -z $(ip link show dev "$INTERFACE" 2>/dev/null) ]] || die "\`$INTERFACE' already exists"
- trap 'del_if; exit' INT TERM EXIT
+ [[ -z $("${netns_exec[@]}" ip link show dev "$INTERFACE" 2>/dev/null) ]] || die "\`$INTERFACE' already exists in network namespace '${SOCKET_NAMESPACE:-<unset>}'"
add_if
+ trap 'del_if; exit' INT TERM EXIT
execute_hooks "${PRE_UP[@]}"
set_config
for i in "${ADDRESSES[@]}"; do
--
2.39.5
More information about the WireGuard
mailing list