[PATCH v2] wg-quick: add SocketNamespace for UDP socket netns

Sybil Isabel Dorsett <[email protected]> Fri, 01 May 2026 13:31:08 +0000
Newsgroups com.zx2c4.lists.wireguard
Message-ID <[email protected]>
wg-quick cannot express WireGuard's supported deployment model in
which the UDP socket resides in a different network namespace than
the interface. This model is documented and used in practice for
cases such as keeping routing policy and tunnel endpoints in separate
namespaces (e.g. container or policy isolation setups). Achieving this
today requires reimplementing substantial parts of wg-quick externally,
including interface lifecycle, address assignment, routing, and
teardown.

Add a SocketNamespace option to select the network namespace in which
the WireGuard UDP socket is created, while keeping the interface in
the invoking namespace. Create the interface in the target namespace
so that sockets are bound there, then move it back to the caller's
namespace before executing PreUp hooks.

Limit the scope to selecting the socket namespace only, without
introducing general network namespace management, persistent state,
or changes to default behavior.

Document SocketNamespace in the man page and update highlighters.

Signed-off-by: Sybil Isabel Dorsett <[email protected]>
---
v2:
- Resend for visibility; no functional changes
- Clarify commit message

Reference (earlier discussion on mailing list):
- https://lists.zx2c4.com/pipermail/wireguard/2020-March/005143.html

Implementation notes:
- Limit scope to socket namespace only (no general netns management)
- Return the interface to the caller namespace before hook execution
- Use PID-based netns targeting instead of fixed namespace IDs
- Ensure userspace fallback (wireguard-go) runs in SocketNamespace,
  preserving socket placement
- Prevent namespace errors from triggering kernel/userspace fallback
  paths

 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/gu=
i/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[] =3D {
 =09[HighlightDelimiter] =3D QColor("#7aa6da"),
 #ifndef MOBILE_WGQUICK_SUBSET
 =09[HighlightTable] =3D QColor("#c397d8"),
+=09[HighlightSocketNamespace] =3D QColor("#c397d8"),
 =09[HighlightFwMark] =3D QColor("#c397d8"),
 =09[HighlightSaveConfig] =3D QColor("#c397d8"),
 =09[HighlightCmd] =3D QColor("#969896"),
diff --git a/contrib/highlighter/highlight.c b/contrib/highlighter/highligh=
t.c
index 8dc0d49..a18f039 100644
--- a/contrib/highlighter/highlight.c
+++ b/contrib/highlighter/highlight.c
@@ -51,6 +51,7 @@ static const char *colormap[] =3D {
 =09[HighlightDelimiter] =3D TERMINAL_FG_CYAN,
 #ifndef MOBILE_WGQUICK_SUBSET
 =09[HighlightTable] =3D TERMINAL_FG_BLUE,
+=09[HighlightSocketNamespace] =3D TERMINAL_FG_BLUE,
 =09[HighlightFwMark] =3D TERMINAL_FG_BLUE,
 =09[HighlightSaveConfig] =3D TERMINAL_FG_BLUE,
 =09[HighlightCmd] =3D TERMINAL_FG_WHITE,
diff --git a/contrib/highlighter/highlighter.c b/contrib/highlighter/highli=
ghter.c
index 3c34f1c..5264ac9 100644
--- a/contrib/highlighter/highlighter.c
+++ b/contrib/highlighter/highlighter.c
@@ -223,6 +223,24 @@ static bool is_valid_persistentkeepalive(string_span_t=
 s)
=20
 #ifndef MOBILE_WGQUICK_SUBSET
=20
+static bool is_valid_filename(string_span_t s)
+{
+=09if (s.len > 128 || !s.len)
+=09=09return false;
+=09if (s.len =3D=3D 1 && s.s[0] =3D=3D '.')
+=09=09return false;
+=09if (s.len =3D=3D 2 && s.s[0] =3D=3D '.' && s.s[1] =3D=3D '.')
+=09=09return false;
+=09if (s.s[0] =3D=3D '-')
+=09=09return false;
+=09for (size_t i =3D 0; i < s.len; ++i) {
+=09=09if (!is_alphabet(s.s[i]) && !is_decimal(s.s[i]) &&
+=09=09    s.s[i] !=3D '_' && s.s[i] !=3D '-' && s.s[i] !=3D '.')
+=09=09=09return false;
+=09}
+=09return true;
+}
+
 static bool is_valid_fwmark(string_span_t s)
 {
 =09if (is_same(s, "off"))
@@ -345,6 +363,7 @@ enum field {
 =09DNS,
 =09MTU,
 #ifndef MOBILE_WGQUICK_SUBSET
+=09SocketNamespace,
 =09FwMark,
 =09Table,
 =09PreUp, PostUp, PreDown, PostDown,
@@ -384,6 +403,7 @@ static enum field get_field(string_span_t s)
 =09check_enum(Endpoint);
 =09check_enum(PersistentKeepalive);
 #ifndef MOBILE_WGQUICK_SUBSET
+=09check_enum(SocketNamespace);
 =09check_enum(FwMark);
 =09check_enum(Table);
 =09check_enum(PreUp);
@@ -526,6 +546,9 @@ static void highlight_value(struct highlight_span_array=
 *ret, const string_span_
 =09case SaveConfig:
 =09=09append_highlight_span(ret, parent.s, s, is_valid_saveconfig(s) ? Hig=
hlightSaveConfig : HighlightError);
 =09=09break;
+=09case SocketNamespace:
+=09=09append_highlight_span(ret, parent.s, s, is_valid_filename(s) ? Highl=
ightSocketNamespace : HighlightError);
+=09=09break;
 =09case FwMark:
 =09=09append_highlight_span(ret, parent.s, s, is_valid_fwmark(s) ? Highlig=
htFwMark : HighlightError);
 =09=09break;
diff --git a/contrib/highlighter/highlighter.h b/contrib/highlighter/highli=
ghter.h
index ecbce00..d7d6600 100644
--- a/contrib/highlighter/highlighter.h
+++ b/contrib/highlighter/highlighter.h
@@ -21,6 +21,7 @@ enum highlight_type {
 =09HighlightDelimiter,
 #ifndef MOBILE_WGQUICK_SUBSET
 =09HighlightTable,
+=09HighlightSocketNamespace,
 =09HighlightFwMark,
 =09HighlightSaveConfig,
 =09HighlightCmd,
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 curr=
ent 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 interf=
ace
+is first added to that netns, then moved to the invoking process's native =
netns
+before any other interface settings are applied.
=20
 .P
 Recommended \fIINTERFACE\fP names include `wg0' or `wgvpn0' or even `wgmgm=
tlan0'.
diff --git a/src/wg-quick/linux.bash b/src/wg-quick/linux.bash
index 18e266c..1349d70 100755
--- a/src/wg-quick/linux.bash
+++ b/src/wg-quick/linux.bash
@@ -13,6 +13,7 @@ export PATH=3D"${SELF%/*}:$PATH"
=20
 WG_CONFIG=3D""
 INTERFACE=3D""
+SOCKET_NAMESPACE=3D""
 ADDRESSES=3D( )
 MTU=3D""
 DNS=3D( )
@@ -56,6 +57,7 @@ parse_options() {
 =09=09[[ $key =3D=3D "[Interface]" ]] && interface_section=3D1
 =09=09if [[ $interface_section -eq 1 ]]; then
 =09=09=09case "$key" in
+=09=09=09SocketNamespace) SOCKET_NAMESPACE=3D"$value"; continue ;;
 =09=09=09Address) ADDRESSES+=3D( ${value//,/ } ); continue ;;
 =09=09=09MTU) MTU=3D"$value"; continue ;;
 =09=09=09DNS) for v in ${value//,/ }; do
@@ -88,12 +90,14 @@ auto_su() {
=20
 add_if() {
 =09local ret
-=09if ! cmd ip link add dev "$INTERFACE" type wireguard; then
+=09trap 'cmd "${netns_exec[@]}" ip link delete dev "$INTERFACE"; exit' INT=
 TERM EXIT
+=09if ! cmd "${netns_exec[@]}" ip link add dev "$INTERFACE" type wireguard=
; then
 =09=09ret=3D$?
 =09=09[[ -e /sys/module/wireguard ]] || ! command -v "${WG_QUICK_USERSPACE=
_IMPLEMENTATION:-wireguard-go}" >/dev/null && exit $ret
 =09=09echo "[!] Missing WireGuard kernel module. Falling back to slow user=
space implementation." >&2
-=09=09cmd "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wireguard-go}" "$INTERFACE=
"
+=09=09cmd "${netns_exec[@]}" "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wiregua=
rd-go}" "$INTERFACE"
 =09fi
+=09[[ -z "$SOCKET_NAMESPACE" ]] || cmd "${netns_exec[@]}" ip link set "$IN=
TERFACE" netns $$
 }
=20
 del_if() {
@@ -256,6 +260,7 @@ save_config() {
 =09local old_umask new_config current_config address cmd
 =09[[ $(ip -all -brief address show dev "$INTERFACE") =3D~ ^$INTERFACE\ +\=
 [A-Z]+\ +(.+)$ ]] || true
 =09new_config=3D$'[Interface]\n'
+=09[[ -z "$SOCKET_NAMESPACE" ]] || new_config+=3D"SocketNamespace =3D $SOC=
KET_NAMESPACE"$'\n'
 =09for address in ${BASH_REMATCH[1]}; do
 =09=09new_config+=3D"Address =3D $address"$'\n'
 =09done
@@ -326,9 +331,13 @@ cmd_usage() {
=20
 cmd_up() {
 =09local i
+=09local netns_exec=3D()
+=09[[ -z "$SOCKET_NAMESPACE" ]] || netns_exec=3D(ip netns exec "$SOCKET_NA=
MESPACE")
+=09"${netns_exec[@]}" true || die "Network namespace '${SOCKET_NAMESPACE:-=
<unset>}' does not exist"
 =09[[ -z $(ip link show dev "$INTERFACE" 2>/dev/null) ]] || die "\`$INTERF=
ACE' already exists"
-=09trap 'del_if; exit' INT TERM EXIT
+=09[[ -z $("${netns_exec[@]}" ip link show dev "$INTERFACE" 2>/dev/null) ]=
] || die "\`$INTERFACE' already exists in network namespace '${SOCKET_NAMES=
PACE:-<unset>}'"
 =09add_if
+=09trap 'del_if; exit' INT TERM EXIT
 =09execute_hooks "${PRE_UP[@]}"
 =09set_config
 =09for i in "${ADDRESSES[@]}"; do
--=20
2.39.5