From patchwork Wed Mar 25 12:40:38 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Frank Lichtenheld X-Patchwork-Id: 4850 Return-Path: Delivered-To: patchwork@openvpn.net Received: by 2002:a05:7000:2f09:b0:83c:d90d:321 with SMTP id fv9csp657327mab; Wed, 25 Mar 2026 07:20:00 -0700 (PDT) X-Forwarded-Encrypted: i=2; AJvYcCVAjTfhjzXNqS/YWl2Gj5ZNvIJmeMkLekRS6t0G1udemgVGC5N00E6CaY7n1h3i5zt398bkcx0oM1A=@openvpn.net X-Received: by 2002:a05:6808:2519:b0:467:e7b:6fdb with SMTP id 5614622812f47-46a5c752a9bmr1861621b6e.42.1774448399934; Wed, 25 Mar 2026 07:19:59 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1774448399; cv=none; d=google.com; s=arc-20240605; b=S0hObMtDOPICXdeiQdu0p9MOcixa0bbpCGOFp//olaC0bYD7MVNyERhevb7wOi8Zko QjI488dhUIv8p9kniTLZDr4mqub4bYa9hbOJOYlrjPgNabTg4AEP9mwUAaI8d7ph5i/q 8UC6AFdXM4ZvVDbK8yiyHF/jJ51y2jGeoJ/H29tsgRKOMvwI5LGJN6x5nHkO/O61i0Vn /fMJil/W3eogbCf7/9L/ez+BU3A2gToA7vnwJ2p8j9Jebq1TQfhaO3UWs3bOSSe+Vw1l cP7y1PNJtvg4jJYd/9pnryj3lY/0gbQiX2EAEZKtOOi9CX5O0wJImxy4OOvatOsNcoJm 00zA== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=errors-to:content-transfer-encoding:list-subscribe:list-help :list-post:list-archive:list-unsubscribe:list-id:precedence:subject :mime-version:references:in-reply-to:message-id:date:to:from :dkim-signature:dkim-signature:dkim-signature; bh=73IUbqjPtAGtzjQ/7A91UNhSdPQJuhXo7GFjMOjloFI=; fh=4NbAC/LsuMLI0S0hprUlLSLCiHwg6SCAifhH718Jh0Q=; b=AZLFBiarI4yMJ/6F77D10PeowjQMmctYrNtHn8UP6eoPrFOxlUyODS4lw2xM5+mn60 0ABBgxAufrzLY85zItWI57MrN1H1R2qTwqV7/8C3yCj9GSYPROOFQ/fuX2EL26MFw6SI AshvjTWtpilSs7dzF6RXV5ksrTPpp8Ey+0z1hqwlNmTrvpmr8YG7gond+ivPVKqeHtHD 8weTptuOYWjEad9sbItRGVohqcUmi7xvkF2MikZdhYIhnGEUXPgiH2mxUoUjZzUgfT4T f9rGhWEsDn33NAZEGj7VOPQ6zj9QuvCiCcx4d8D5Rpikm/qH4JhGrGPiGexigEsdwq61 MITg==; dara=google.com ARC-Authentication-Results: i=1; mx.google.com; dkim=pass header.i=@lists.sourceforge.net header.s=beta header.b=SFy82PNq; dkim=neutral (body hash did not verify) header.i=@sourceforge.net header.s=x header.b=gYuwRj9s; dkim=neutral (body hash did not verify) header.i=@sf.net header.s=x header.b=IvlpMaTz; spf=pass (google.com: domain of openvpn-devel-bounces@lists.sourceforge.net designates 216.105.38.7 as permitted sender) smtp.mailfrom=openvpn-devel-bounces@lists.sourceforge.net Received: from lists.sourceforge.net (lists.sourceforge.net. [216.105.38.7]) by mx.google.com with ESMTPS id 5614622812f47-467e7dc3b44si8644690b6e.59.2026.03.25.07.19.59 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Wed, 25 Mar 2026 07:19:59 -0700 (PDT) Received-SPF: pass (google.com: domain of openvpn-devel-bounces@lists.sourceforge.net designates 216.105.38.7 as permitted sender) client-ip=216.105.38.7; Authentication-Results: mx.google.com; dkim=pass header.i=@lists.sourceforge.net header.s=beta header.b=SFy82PNq; dkim=neutral (body hash did not verify) header.i=@sourceforge.net header.s=x header.b=gYuwRj9s; dkim=neutral (body hash did not verify) header.i=@sf.net header.s=x header.b=IvlpMaTz; spf=pass (google.com: domain of openvpn-devel-bounces@lists.sourceforge.net designates 216.105.38.7 as permitted sender) smtp.mailfrom=openvpn-devel-bounces@lists.sourceforge.net DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=lists.sourceforge.net; s=beta; h=Content-Transfer-Encoding:Content-Type: List-Subscribe:List-Help:List-Post:List-Archive:List-Unsubscribe:List-Id: Subject:MIME-Version:References:In-Reply-To:Message-ID:Date:To:From:Sender: Reply-To:Cc:Content-ID:Content-Description:Resent-Date:Resent-From: Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:List-Owner; bh=73IUbqjPtAGtzjQ/7A91UNhSdPQJuhXo7GFjMOjloFI=; b=SFy82PNqIAPP1iykvG1/XTQMok 1EEgEWBWw6afJFCFhkyRVn6uKZ3yLyrt33fVzTCB8m9nzMPdaOcrfCrfzJslrKFOsWQgPAo+knHgZ H950M7CtLTllqHMFtt/1N37jyvlt8oKptME6YA1MqUXGUwNJjUazcH64xjdMcYJ9twlo=; Received: from [127.0.0.1] (helo=sfs-ml-2.v29.lw.sourceforge.com) by sfs-ml-2.v29.lw.sourceforge.com with esmtp (Exim 4.95) (envelope-from ) id 1w5P5L-00038z-M8; Wed, 25 Mar 2026 14:19:52 +0000 Received: from [172.30.29.66] (helo=mx.sourceforge.net) by sfs-ml-2.v29.lw.sourceforge.com with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.95) (envelope-from ) id 1w5P5L-00038q-3R for openvpn-devel@lists.sourceforge.net; Wed, 25 Mar 2026 14:19:51 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sourceforge.net; s=x; h=Content-Transfer-Encoding:MIME-Version:References: In-Reply-To:Message-ID:Date:Subject:Cc:To:From:Sender:Reply-To:Content-Type: Content-ID:Content-Description:Resent-Date:Resent-From:Resent-Sender: Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help:List-Unsubscribe: List-Subscribe:List-Post:List-Owner:List-Archive; bh=tveHPcdUgi3wyoWLSHnrXK4ZT+jqyDw3FVTJNo9IpsQ=; b=gYuwRj9s77k9udsN3sxUHGmGkR 7QW02VagkAkcYhYuPgxwawm6N79fC+tZG85HMbcjAY6vFafw+ZDeevtIfuBHg8RhVj4SSabf+eD45 i+aeTWCMpULNc6G1JiBkt2WI7hIL95J70D/aGuyhDttjjM0IMKGdT+/pUyYVss+8X/Mg=; DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sf.net; s=x ; h=Content-Transfer-Encoding:MIME-Version:References:In-Reply-To:Message-ID: Date:Subject:Cc:To:From:Sender:Reply-To:Content-Type:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=tveHPcdUgi3wyoWLSHnrXK4ZT+jqyDw3FVTJNo9IpsQ=; b=IvlpMaTzmGS3C1//he+VMxr5KS E8T8gK35sfZ8E1hzFk8nssItnp9XC504yZG/MJf8SgFQ8WR/RZGcKLFk53rEA3hmgtvFtn0hAx433 3UXbzfBGNwDcVqsWBMXEMIt7OgozEgf6YxXhW3Rwx6cf6j5Ky0JSzXpWwrFNPrspiqCU=; Received: from mout-p-101.mailbox.org ([80.241.56.151]) by sfi-mx-2.v28.lw.sourceforge.com with esmtps (TLS1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.95) id 1w5P5I-0006nr-PR for openvpn-devel@lists.sourceforge.net; Wed, 25 Mar 2026 14:19:51 +0000 Received: from smtp202.mailbox.org (smtp202.mailbox.org [IPv6:2001:67c:2050:b231:465::202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by mout-p-101.mailbox.org (Postfix) with ESMTPS id 4fgmjB1W1fz9vHX; Wed, 25 Mar 2026 13:40:46 +0100 (CET) From: Frank Lichtenheld To: openvpn-devel@lists.sourceforge.net Date: Wed, 25 Mar 2026 13:40:38 +0100 Message-ID: <20260325124038.122882-1-frank@lichtenheld.com> In-Reply-To: References: MIME-Version: 1.0 X-Spam-Score: 0.0 (/) X-Spam-Report: Spam detection software, running on the system "sfi-spamd-1.hosts.colo.sdot.me", has NOT identified this incoming email as spam. The original message has been attached to this so you can view it or label similar future email. If you have any questions, see the administrator of that system for details. Content preview: From: Gianmarco De Gregori Introduce the --netns option to allow interface creation into a user-defined network namespace. This allows the VPN data plane to be isolated from the main OpenVPN process namespace. Content analysis details: (0.0 points, 5.0 required) pts rule name description ---- ---------------------- -------------------------------------------------- X-Headers-End: 1w5P5I-0006nr-PR Subject: [Openvpn-devel] [PATCH v8] Add support for user defined network namespace X-BeenThere: openvpn-devel@lists.sourceforge.net X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: openvpn-devel-bounces@lists.sourceforge.net X-getmail-retrieved-from-mailbox: Inbox X-GMAIL-THRID: =?utf-8?q?1860644005424169755?= X-GMAIL-MSGID: =?utf-8?q?1860644005424169755?= From: Gianmarco De Gregori Introduce the --netns option to allow interface creation into a user-defined network namespace. This allows the VPN data plane to be isolated from the main OpenVPN process namespace. The current netlink library integration supports interface creation and deletion in a target namespace. However, subsequent configuration operations (e.g. address or mtu set) are executed in the caller's namespace, as they rely on the default netlink socket context. As a result, interface-related configuration performed after creation may be applied in the wrong namespace. Introduce helper functions to temporarily switch the process to the requested network namespace using setns(2), execute the required netlink operations, and then restore the original namespace. The namespace switch is temporary and scoped to each netlink operation. Once the operation completes, the original namespace is restored to preserve the process execution context. Note: This feature is Linux-only and depends on setns(2). It is not compatible (yet) with Data Channel Offload (DCO). Change-Id: I8b0d1cad7062856abcc40c4e16ec93b45295bbd3 Signed-off-by: Gianmarco De Gregori Acked-by: Frank Lichtenheld Gerrit URL: https://gerrit.openvpn.net/c/openvpn/+/1521 --- This change was reviewed on Gerrit and approved by at least one developer. I request to merge it to master. Gerrit URL: https://gerrit.openvpn.net/c/openvpn/+/1521 This mail reflects revision 8 of this Change. Acked-by according to Gerrit (reflected above): Frank Lichtenheld diff --git a/doc/man-sections/vpn-network-options.rst b/doc/man-sections/vpn-network-options.rst index 33ebedb..cd54cd4 100644 --- a/doc/man-sections/vpn-network-options.rst +++ b/doc/man-sections/vpn-network-options.rst @@ -304,6 +304,12 @@ Specify the link layer address, more commonly known as the MAC address. Only applied to TAP devices. +--netns name + Move the created tunnel interface into the specified network + namespace. The namespace must already exist. + + (Supported on Linux only, on other platforms this is a no-op). + --persist-tun Don't close and reopen TUN/TAP device or run up/down scripts across :code:`SIGUSR1` or ``--ping-restart`` restarts. diff --git a/src/openvpn/dco.c b/src/openvpn/dco.c index 26b8645..a61e587 100644 --- a/src/openvpn/dco.c +++ b/src/openvpn/dco.c @@ -388,6 +388,12 @@ ret); } } + + if (o->netns) + { + msg(msglevel, "Note: --netns not supported by DCO, disabling data channel offload."); + return false; + } #endif /* if defined(_WIN32) */ #if defined(HAVE_LIBCAPNG) diff --git a/src/openvpn/networking.h b/src/openvpn/networking.h index bce0c19..fab6bbf 100644 --- a/src/openvpn/networking.h +++ b/src/openvpn/networking.h @@ -39,10 +39,10 @@ typedef void *openvpn_net_iface_t; #endif /* ifdef ENABLE_SITNL */ -/* Only the iproute2 backend implements these functions, +/* Only the iproute2 and sitnl backend implements these functions, * the rest can rely on these stubs */ -#if !defined(ENABLE_IPROUTE) +#if !defined(ENABLE_IPROUTE) && !defined(ENABLE_SITNL) || defined(TARGET_ANDROID) static inline int net_ctx_init(struct context *c, openvpn_net_ctx_t *ctx) { @@ -63,7 +63,7 @@ { (void)ctx; } -#endif /* !defined(ENABLE_IPROUTE) */ +#endif /* !defined(ENABLE_IPROUTE) && !defined(ENABLE_SITNL) || defined(TARGET_ANDROID) */ #if defined(ENABLE_SITNL) || defined(ENABLE_IPROUTE) diff --git a/src/openvpn/networking_sitnl.c b/src/openvpn/networking_sitnl.c index b88f03c..541c855 100644 --- a/src/openvpn/networking_sitnl.c +++ b/src/openvpn/networking_sitnl.c @@ -33,14 +33,17 @@ #include "networking.h" #include "proto.h" #include "route.h" +#include "openvpn.h" #include #include #include +#include #include #include #include #include +#include #define SNDBUF_SIZE (1024 * 2) #define RCVBUF_SIZE (1024 * 4) @@ -65,6 +68,12 @@ _nest->rta_len = (unsigned short)((void *)sitnl_nlmsg_tail(_msg) - (void *)_nest); \ } +#define SITNL_NETNS_RTA(r) \ + ((struct rtattr *)(((char *)(r)) + NLMSG_ALIGN(sizeof(struct rtgenmsg)))) + +#define NETNS_RUN_DIR "/run/netns" +#define NETNS_ORIG_DIR "/proc/self/ns/net" + /* This function was originally implemented as a macro, but compiling with * gcc and -O3 was getting confused about the math and thus raising * security warnings on subsequent memcpy() calls. @@ -121,6 +130,16 @@ char buf[256]; }; +/** + * Network namespace NSID request message + */ +struct sitnl_nsid_req +{ + struct nlmsghdr n; + struct rtgenmsg g; + char buf[256]; +}; + typedef int (*sitnl_parse_reply_cb)(struct nlmsghdr *msg, void *arg); /** @@ -165,6 +184,105 @@ return 0; } +#if defined(TARGET_LINUX) && defined(ENABLE_SITNL) + +static int netns_id_from_name_sitnl(const char *name); +static int set_netns_id_from_name_sitnl(const char *name); + +int +net_ctx_init(struct context *c, openvpn_net_ctx_t *ctx) +{ + struct stat st; + char path[PATH_MAX]; + + ctx->netns = NULL; + + if (c && c->options.netns) + { + snprintf(path, sizeof(path), "%s/%s", NETNS_RUN_DIR, c->options.netns); + + if (stat(path, &st) != 0) + { + msg(M_ERR, "%s: Network namespace %s does not exist", __func__, path); + } + + ctx->netns = c->options.netns; + } + + ctx->gc = gc_new(); + return 0; +} + +void +net_ctx_reset(openvpn_net_ctx_t *ctx) +{ + gc_reset(&ctx->gc); +} + +void +net_ctx_free(openvpn_net_ctx_t *ctx) +{ + gc_free(&ctx->gc); +} + +int +netns_switch(openvpn_net_ctx_t *ctx) +{ + char net_path[PATH_MAX]; + int orig_fd = -1, netns_fd; + const char *netns = ctx ? ctx->netns : NULL; + + if (!netns) + { + return -1; + } + + /* Save current netns descriptor */ + orig_fd = open(NETNS_ORIG_DIR, O_RDONLY | O_CLOEXEC); + if (orig_fd < 0) + { + msg(M_WARN | M_ERRNO, "%s: Cannot open original network namespace", __func__); + return -1; + } + + snprintf(net_path, sizeof(net_path), "%s/%s", NETNS_RUN_DIR, netns); + netns_fd = open(net_path, O_RDONLY | O_CLOEXEC); + if (netns_fd < 0) + { + msg(M_WARN | M_ERRNO, "%s: Cannot open network namespace \"%s\"", __func__, netns); + close(orig_fd); + return -1; + } + + if (setns(netns_fd, CLONE_NEWNET) < 0) + { + msg(M_WARN | M_ERRNO, "%s: setting the network namespace \"%s\" failed", __func__, netns); + close(netns_fd); + close(orig_fd); + return -1; + } + + close(netns_fd); + return orig_fd; +} + +void +netns_restore(int orig_fd) +{ + if (orig_fd < 0) + { + return; + } + + if (setns(orig_fd, CLONE_NEWNET) < 0) + { + msg(M_WARN | M_ERRNO, "%s: Cannot restore original network namespace", __func__); + } + + close(orig_fd); +} +#endif /* defined (TARGET_LINUX) && defined (ENABLE_SITNL) */ + /** * Open RTNL socket */ @@ -454,6 +572,290 @@ return ret; } +#ifdef ENABLE_SITNL +/** + * Netlink callback to extract the interface index from a RTM_NEWLINK message. + * + * This function is used as a Netlink response handler. It inspects incoming + * Netlink messages and, when a RTM_NEWLINK message is received, copies the + * contained `struct ifinfomsg` into the caller-provided result structure. + * + * @param n Netlink message header received from the kernel. + * @param arg Pointer to a `struct sitnl_link_req` used to store the result. + * + * @return 0 to stop Netlink message processing once the interface is found, + * 1 to continue processing other messages. + */ +static int +sitnl_link_get_ifindex(struct nlmsghdr *n, void *arg) +{ + struct sitnl_link_req *res = arg; + struct ifinfomsg *ifi; + + if (n->nlmsg_type != RTM_NEWLINK) + { + return 1; + } + + ifi = NLMSG_DATA(n); + res->i = *ifi; + + return 0; +} + +/** + * Retrieve the interface index of a network device in a specific network namespace. + * + * Sends an RTM_GETLINK Netlink request targeting the given network namespace ID + * and interface name, and extracts the resulting interface index from the kernel + * response. + * + * On failure, this function returns 0, matching the behavior of + * `if_nametoindex()`. + * + * @param ifname Name of the network interface. + * @param netnsid Target network namespace ID. + * + * @return Interface index on success, or 0 on error. + */ +static int +get_ifindex_in_netns(const char *ifname, int netnsid) +{ + struct sitnl_link_req req; + struct sitnl_link_req res; + int ret = -1, ifindex = 0; + + CLEAR(req); + CLEAR(res); + + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(req.i)); + req.n.nlmsg_type = RTM_GETLINK; + req.n.nlmsg_flags = NLM_F_REQUEST; + + SITNL_ADDATTR(&req.n, sizeof(req), IFLA_TARGET_NETNSID, &netnsid, sizeof(int)); + SITNL_ADDATTR(&req.n, sizeof(req), IFLA_IFNAME, ifname, strlen(ifname) + 1); + + ret = sitnl_send(&req.n, 0, 0, sitnl_link_get_ifindex, &res); + + if (ret < 0) + { + return ret; + } + + ifindex = res.i.ifi_index; + +err: + return ifindex; +} + +static int +sitnl_parse_rtattr_flags(struct rtattr *tb[], size_t max, struct rtattr *rta, size_t len, + unsigned short flags) +{ + unsigned short type; + + memset(tb, 0, sizeof(struct rtattr *) * (max + 1)); + + while (RTA_OK(rta, len)) + { + type = rta->rta_type & ~flags; + + if ((type <= max) && (!tb[type])) + { + tb[type] = rta; + } + + rta = RTA_NEXT(rta, len); + } + + if (len) + { + msg(D_ROUTE, "%s: %zu bytes not parsed! (rta_len=%u)", __func__, len, rta->rta_len); + } + + return 0; +} + +static int +sitnl_parse_rtattr(struct rtattr *tb[], size_t max, struct rtattr *rta, size_t len) +{ + return sitnl_parse_rtattr_flags(tb, max, rta, len, 0); +} + +/** + * Netlink callback to extract and store a network namespace ID (NSID). + * + * This function processes Netlink responses to RTM_GETNSID requests. + * If a NETNSA_NSID attribute is present, its value is copied into the + * caller-provided storage. + * + * If a Netlink error message is received, the contained error code is + * returned directly. + * + * @param n Netlink message header received from the kernel. + * @param arg Pointer to an integer where the NSID will be stored. + * + * @return 0 on success, a negative error code on failure. + */ +static int +sitnl_nsid_save(struct nlmsghdr *n, void *arg) +{ + int *nsid = arg; + struct rtgenmsg *g = NLMSG_DATA(n); + struct rtattr *tb[NETNSA_MAX + 1]; + int ret; + + if (n->nlmsg_type == NLMSG_ERROR) + { + struct nlmsgerr *err = NLMSG_DATA(n); + return err->error; + } + + ret = sitnl_parse_rtattr(tb, NETNSA_MAX, SITNL_NETNS_RTA(g), NLMSG_PAYLOAD(n, sizeof(*g))); + + if (ret < 0) + { + return ret; + } + + if (!tb[NETNSA_NSID]) + { + return -ENOENT; + } + + memcpy(nsid, RTA_DATA(tb[NETNSA_NSID]), sizeof(*nsid)); + return 0; +} + +/** + * Retrieve the network namespace ID (NSID) associated with a namespace name. + * + * This function opens the network namespace identified by the given path + * and sends an RTM_GETNSID Netlink request to retrieve its assigned NSID. + * + * @param name Name of the network namespace. + * + * @return The network namespace ID on success, or a negative value on failure. + */ +static int +netns_id_from_name_sitnl(const char *name) +{ + struct sitnl_nsid_req req; + + int netnsid = -1; + CLEAR(req); + + int netns_fd = open(name, O_RDONLY); + if (netns_fd < 0) + { + goto err; + } + + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(req.g)); + req.n.nlmsg_type = RTM_GETNSID; + req.n.nlmsg_flags = NLM_F_REQUEST; + req.g.rtgen_family = AF_UNSPEC; + + SITNL_ADDATTR(&req.n, sizeof(req), NETNSA_FD, &netns_fd, sizeof(int)); + + sitnl_send(&req.n, 0, 0, sitnl_nsid_save, &netnsid); + +err: + if (netns_fd >= 0) + { + close(netns_fd); + } + return netnsid; +} + +/** + * Assign a network namespace ID (NSID) to a namespace. + * + * This function sends an RTM_NEWNSID Netlink request to associate a + * NSID with the network namespace identified by the given path. + * + * @param name Name of the network namespace. + * + * @return 0 on success, or a negative error code on failure. + */ +static int +set_netns_id_from_name_sitnl(const char *name) +{ + struct sitnl_nsid_req req; + + int ret = -1, netnsid = -1; + CLEAR(req); + + int netns_fd = open(name, O_RDONLY); + if (netns_fd < 0) + { + goto err; + } + + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(req.g)); + req.n.nlmsg_type = RTM_NEWNSID; + req.n.nlmsg_flags = NLM_F_REQUEST; + req.g.rtgen_family = AF_UNSPEC; + + SITNL_ADDATTR(&req.n, sizeof(req), NETNSA_FD, &netns_fd, sizeof(int)); + SITNL_ADDATTR(&req.n, sizeof(req), NETNSA_NSID, &netnsid, sizeof(int)); + + ret = sitnl_send(&req.n, 0, 0, NULL, NULL); + +err: + if (netns_fd >= 0) + { + close(netns_fd); + } + return ret; +} + +int +get_or_create_netnsid_sitnl(const char *name) +{ + char net_path[PATH_MAX]; + int netnsid; + snprintf(net_path, sizeof(net_path), "%s/%s", NETNS_RUN_DIR, name); + + /* First get */ + netnsid = netns_id_from_name_sitnl(net_path); + if (netnsid >= 0) + { + return netnsid; + } + + /* No netnsid yet? Try to assign one */ + netnsid = set_netns_id_from_name_sitnl(net_path); + if (netnsid < 0) + { + return -1; + } + + /* Second get */ + netnsid = netns_id_from_name_sitnl(net_path); + if (netnsid >= 0) + { + return netnsid; + } + + return -1; +} + +int +openvpn_if_nametoindex(const char *ifname, openvpn_net_ctx_t *ctx) +{ + const char *netns = ctx ? ctx->netns : NULL; + int netnsid = get_or_create_netnsid_sitnl(netns); + + if (netnsid < 0) + { + return if_nametoindex(ifname); + } + + return get_ifindex_in_netns(ifname, netnsid); +} +#endif /* ifdef ENABLE_SITNL */ + typedef struct { size_t addr_size; @@ -661,7 +1063,7 @@ net_iface_up(openvpn_net_ctx_t *ctx, const char *iface, bool up) { struct sitnl_link_req req; - int ifindex; + int ifindex, orig = -1; CLEAR(req); @@ -671,13 +1073,15 @@ return -EINVAL; } - ifindex = if_nametoindex(iface); + ifindex = openvpn_if_nametoindex(iface, ctx); if (ifindex == 0) { - msg(M_WARN, "%s: rtnl: cannot get ifindex for %s: %s", __func__, iface, strerror(errno)); + msg(M_WARN | M_ERRNO, "%s: rtnl: cannot get ifindex for %s", __func__, iface); return -ENOENT; } + orig = netns_switch(ctx); + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(req.i)); req.n.nlmsg_flags = NLM_F_REQUEST; req.n.nlmsg_type = RTM_NEWLINK; @@ -696,24 +1100,30 @@ msg(M_INFO, "%s: set %s %s", __func__, iface, up ? "up" : "down"); - return sitnl_send(&req.n, 0, 0, NULL, NULL); + int ret = sitnl_send(&req.n, 0, 0, NULL, NULL); + + netns_restore(orig); + + return ret; } int net_iface_mtu_set(openvpn_net_ctx_t *ctx, const char *iface, uint32_t mtu) { struct sitnl_link_req req; - int ifindex, ret = -1; + int ifindex, orig = -1, ret = -1; CLEAR(req); - ifindex = if_nametoindex(iface); + ifindex = openvpn_if_nametoindex(iface, ctx); if (ifindex == 0) { msg(M_WARN | M_ERRNO, "%s: rtnl: cannot get ifindex for %s", __func__, iface); return -1; } + orig = netns_switch(ctx); + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(req.i)); req.n.nlmsg_flags = NLM_F_REQUEST; req.n.nlmsg_type = RTM_NEWLINK; @@ -726,7 +1136,9 @@ msg(M_INFO, "%s: mtu %u for %s", __func__, mtu, iface); ret = sitnl_send(&req.n, 0, 0, NULL, NULL); + err: + netns_restore(orig); return ret; } @@ -734,17 +1146,19 @@ net_addr_ll_set(openvpn_net_ctx_t *ctx, const openvpn_net_iface_t *iface, uint8_t *addr) { struct sitnl_link_req req; - int ifindex, ret = -1; + int ifindex, orig = -1, ret = -1; CLEAR(req); - ifindex = if_nametoindex(iface); + ifindex = openvpn_if_nametoindex(iface, ctx); if (ifindex == 0) { msg(M_WARN | M_ERRNO, "%s: rtnl: cannot get ifindex for %s", __func__, iface); return -1; } + orig = netns_switch(ctx); + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(req.i)); req.n.nlmsg_flags = NLM_F_REQUEST; req.n.nlmsg_type = RTM_NEWLINK; @@ -757,17 +1171,19 @@ msg(M_INFO, "%s: lladdr " MAC_FMT " for %s", __func__, MAC_PRINT_ARG(addr), iface); ret = sitnl_send(&req.n, 0, 0, NULL, NULL); + err: + netns_restore(orig); return ret; } static int -sitnl_addr_set(uint16_t cmd, uint16_t flags, int ifindex, sa_family_t af_family, +sitnl_addr_set(openvpn_net_ctx_t *ctx, uint16_t cmd, uint16_t flags, int ifindex, sa_family_t af_family, const inet_address_t *local, const inet_address_t *remote, int prefixlen) { struct sitnl_addr_req req; uint32_t size; - int ret = -EINVAL; + int ret = -EINVAL, orig = -1; CLEAR(req); @@ -819,17 +1235,22 @@ SITNL_ADDATTR(&req.n, sizeof(req), IFA_BROADCAST, &broadcast, size); } + orig = netns_switch(ctx); + ret = sitnl_send(&req.n, 0, 0, NULL, NULL); + if (ret == -EEXIST) { ret = 0; } + err: + netns_restore(orig); return ret; } static int -sitnl_addr_ptp_add(sa_family_t af_family, const char *iface, const inet_address_t *local, +sitnl_addr_ptp_add(openvpn_net_ctx_t *ctx, sa_family_t af_family, const char *iface, const inet_address_t *local, const inet_address_t *remote) { int ifindex; @@ -850,19 +1271,19 @@ return -EINVAL; } - ifindex = if_nametoindex(iface); + ifindex = openvpn_if_nametoindex(iface, ctx); if (ifindex == 0) { - msg(M_WARN, "%s: cannot get ifindex for %s: %s", __func__, np(iface), strerror(errno)); + msg(M_WARN | M_ERRNO, "%s: cannot get ifindex for %s", __func__, np(iface)); return -ENOENT; } - return sitnl_addr_set(RTM_NEWADDR, NLM_F_CREATE | NLM_F_REPLACE, ifindex, af_family, local, + return sitnl_addr_set(ctx, RTM_NEWADDR, NLM_F_CREATE | NLM_F_REPLACE, ifindex, af_family, local, remote, 0); } static int -sitnl_addr_ptp_del(sa_family_t af_family, const char *iface, const inet_address_t *local) +sitnl_addr_ptp_del(openvpn_net_ctx_t *ctx, sa_family_t af_family, const char *iface, const inet_address_t *local) { int ifindex; @@ -882,23 +1303,23 @@ return -EINVAL; } - ifindex = if_nametoindex(iface); + ifindex = openvpn_if_nametoindex(iface, ctx); if (ifindex == 0) { msg(M_WARN | M_ERRNO, "%s: cannot get ifindex for %s", __func__, iface); return -ENOENT; } - return sitnl_addr_set(RTM_DELADDR, 0, ifindex, af_family, local, NULL, 0); + return sitnl_addr_set(ctx, RTM_DELADDR, 0, ifindex, af_family, local, NULL, 0); } static int -sitnl_route_set(uint16_t cmd, uint16_t flags, int ifindex, sa_family_t af_family, const void *dst, +sitnl_route_set(openvpn_net_ctx_t *ctx, uint16_t cmd, uint16_t flags, int ifindex, sa_family_t af_family, const void *dst, int prefixlen, const void *gw, enum rt_class_t table, int metric, enum rt_scope_t scope, unsigned char protocol, unsigned char type) { struct sitnl_route_req req; - int ret = -1, size; + int ret = -1, size, orig = -1; CLEAR(req); @@ -957,14 +1378,17 @@ { SITNL_ADDATTR(&req.n, sizeof(req), RTA_PRIORITY, &metric, 4); } + orig = netns_switch(ctx); ret = sitnl_send(&req.n, 0, 0, NULL, NULL); + err: + netns_restore(orig); return ret; } static int -sitnl_addr_add(sa_family_t af_family, const char *iface, const inet_address_t *addr, int prefixlen) +sitnl_addr_add(openvpn_net_ctx_t *ctx, sa_family_t af_family, const char *iface, const inet_address_t *addr, int prefixlen) { int ifindex; @@ -984,19 +1408,19 @@ return -EINVAL; } - ifindex = if_nametoindex(iface); + ifindex = openvpn_if_nametoindex(iface, ctx); if (ifindex == 0) { msg(M_WARN | M_ERRNO, "%s: rtnl: cannot get ifindex for %s", __func__, iface); return -ENOENT; } - return sitnl_addr_set(RTM_NEWADDR, NLM_F_CREATE | NLM_F_REPLACE, ifindex, af_family, addr, NULL, + return sitnl_addr_set(ctx, RTM_NEWADDR, NLM_F_CREATE | NLM_F_REPLACE, ifindex, af_family, addr, NULL, prefixlen); } static int -sitnl_addr_del(sa_family_t af_family, const char *iface, inet_address_t *addr, int prefixlen) +sitnl_addr_del(openvpn_net_ctx_t *ctx, sa_family_t af_family, const char *iface, inet_address_t *addr, int prefixlen) { int ifindex; @@ -1016,14 +1440,14 @@ return -EINVAL; } - ifindex = if_nametoindex(iface); + ifindex = openvpn_if_nametoindex(iface, ctx); if (ifindex == 0) { msg(M_WARN | M_ERRNO, "%s: rtnl: cannot get ifindex for %s", __func__, iface); return -ENOENT; } - return sitnl_addr_set(RTM_DELADDR, 0, ifindex, af_family, addr, NULL, prefixlen); + return sitnl_addr_set(ctx, RTM_DELADDR, 0, ifindex, af_family, addr, NULL, prefixlen); } int @@ -1042,7 +1466,7 @@ msg(M_INFO, "%s: %s/%d dev %s", __func__, inet_ntop(AF_INET, &addr_v4.ipv4, buf, sizeof(buf)), prefixlen, iface); - return sitnl_addr_add(AF_INET, iface, &addr_v4, prefixlen); + return sitnl_addr_add(ctx, AF_INET, iface, &addr_v4, prefixlen); } int @@ -1062,7 +1486,7 @@ msg(M_INFO, "%s: %s/%d dev %s", __func__, inet_ntop(AF_INET6, &addr_v6.ipv6, buf, sizeof(buf)), prefixlen, iface); - return sitnl_addr_add(AF_INET6, iface, &addr_v6, prefixlen); + return sitnl_addr_add(ctx, AF_INET6, iface, &addr_v6, prefixlen); } int @@ -1081,7 +1505,7 @@ msg(M_INFO, "%s: %s dev %s", __func__, inet_ntop(AF_INET, &addr_v4.ipv4, buf, sizeof(buf)), iface); - return sitnl_addr_del(AF_INET, iface, &addr_v4, prefixlen); + return sitnl_addr_del(ctx, AF_INET, iface, &addr_v4, prefixlen); } int @@ -1101,7 +1525,7 @@ msg(M_INFO, "%s: %s/%d dev %s", __func__, inet_ntop(AF_INET6, &addr_v6.ipv6, buf, sizeof(buf)), prefixlen, iface); - return sitnl_addr_del(AF_INET6, iface, &addr_v6, prefixlen); + return sitnl_addr_del(ctx, AF_INET6, iface, &addr_v6, prefixlen); } int @@ -1129,7 +1553,7 @@ inet_ntop(AF_INET, &local_v4.ipv4, buf1, sizeof(buf1)), inet_ntop(AF_INET, &remote_v4.ipv4, buf2, sizeof(buf2)), iface); - return sitnl_addr_ptp_add(AF_INET, iface, &local_v4, &remote_v4); + return sitnl_addr_ptp_add(ctx, AF_INET, iface, &local_v4, &remote_v4); } int @@ -1150,11 +1574,11 @@ msg(M_INFO, "%s: %s dev %s", __func__, inet_ntop(AF_INET, &local_v4.ipv4, buf, sizeof(buf)), iface); - return sitnl_addr_ptp_del(AF_INET, iface, &local_v4); + return sitnl_addr_ptp_del(ctx, AF_INET, iface, &local_v4); } static int -sitnl_route_add(const char *iface, sa_family_t af_family, const void *dst, int prefixlen, +sitnl_route_add(openvpn_net_ctx_t *ctx, const char *iface, sa_family_t af_family, const void *dst, int prefixlen, const void *gw, uint32_t table, int metric) { enum rt_scope_t scope = RT_SCOPE_UNIVERSE; @@ -1162,7 +1586,7 @@ if (iface) { - ifindex = if_nametoindex(iface); + ifindex = openvpn_if_nametoindex(iface, ctx); if (ifindex == 0) { msg(M_WARN | M_ERRNO, "%s: rtnl: can't get ifindex for %s", __func__, iface); @@ -1180,7 +1604,7 @@ scope = RT_SCOPE_LINK; } - return sitnl_route_set(RTM_NEWROUTE, NLM_F_CREATE, ifindex, af_family, dst, prefixlen, gw, + return sitnl_route_set(ctx, RTM_NEWROUTE, NLM_F_CREATE, ifindex, af_family, dst, prefixlen, gw, table, metric, scope, RTPROT_BOOT, RTN_UNICAST); } @@ -1209,7 +1633,7 @@ inet_ntop(AF_INET, &dst_be, dst_str, sizeof(dst_str)), prefixlen, inet_ntop(AF_INET, &gw_be, gw_str, sizeof(gw_str)), np(iface), table, metric); - return sitnl_route_add(iface, AF_INET, dst_ptr, prefixlen, gw_ptr, table, metric); + return sitnl_route_add(ctx, iface, AF_INET, dst_ptr, prefixlen, gw_ptr, table, metric); } int @@ -1235,18 +1659,18 @@ inet_ntop(AF_INET6, &dst_v6.ipv6, dst_str, sizeof(dst_str)), prefixlen, inet_ntop(AF_INET6, &gw_v6.ipv6, gw_str, sizeof(gw_str)), np(iface), table, metric); - return sitnl_route_add(iface, AF_INET6, dst, prefixlen, gw, table, metric); + return sitnl_route_add(ctx, iface, AF_INET6, dst, prefixlen, gw, table, metric); } static int -sitnl_route_del(const char *iface, sa_family_t af_family, inet_address_t *dst, int prefixlen, +sitnl_route_del(openvpn_net_ctx_t *ctx, const char *iface, sa_family_t af_family, inet_address_t *dst, int prefixlen, inet_address_t *gw, uint32_t table, int metric) { int ifindex = 0; if (iface) { - ifindex = if_nametoindex(iface); + ifindex = openvpn_if_nametoindex(iface, ctx); if (ifindex == 0) { msg(M_WARN | M_ERRNO, "%s: rtnl: can't get ifindex for %s", __func__, iface); @@ -1259,7 +1683,7 @@ table = RT_TABLE_MAIN; } - return sitnl_route_set(RTM_DELROUTE, 0, ifindex, af_family, dst, prefixlen, gw, table, metric, + return sitnl_route_set(ctx, RTM_DELROUTE, 0, ifindex, af_family, dst, prefixlen, gw, table, metric, RT_SCOPE_NOWHERE, 0, 0); } @@ -1286,7 +1710,7 @@ inet_ntop(AF_INET, &dst_v4.ipv4, dst_str, sizeof(dst_str)), prefixlen, inet_ntop(AF_INET, &gw_v4.ipv4, gw_str, sizeof(gw_str)), np(iface), table, metric); - return sitnl_route_del(iface, AF_INET, &dst_v4, prefixlen, &gw_v4, table, metric); + return sitnl_route_del(ctx, iface, AF_INET, &dst_v4, prefixlen, &gw_v4, table, metric); } int @@ -1312,7 +1736,7 @@ inet_ntop(AF_INET6, &dst_v6.ipv6, dst_str, sizeof(dst_str)), prefixlen, inet_ntop(AF_INET6, &gw_v6.ipv6, gw_str, sizeof(gw_str)), np(iface), table, metric); - return sitnl_route_del(iface, AF_INET6, &dst_v6, prefixlen, &gw_v6, table, metric); + return sitnl_route_del(ctx, iface, AF_INET6, &dst_v6, prefixlen, &gw_v6, table, metric); } @@ -1356,44 +1780,11 @@ msg(D_ROUTE, "%s: add %s type %s", __func__, iface, type); ret = sitnl_send(&req.n, 0, 0, NULL, NULL); + err: return ret; } -static int -sitnl_parse_rtattr_flags(struct rtattr *tb[], size_t max, struct rtattr *rta, size_t len, - unsigned short flags) -{ - unsigned short type; - - memset(tb, 0, sizeof(struct rtattr *) * (max + 1)); - - while (RTA_OK(rta, len)) - { - type = rta->rta_type & ~flags; - - if ((type <= max) && (!tb[type])) - { - tb[type] = rta; - } - - rta = RTA_NEXT(rta, len); - } - - if (len) - { - msg(D_ROUTE, "%s: %zu bytes not parsed! (rta_len=%u)", __func__, len, rta->rta_len); - } - - return 0; -} - -static int -sitnl_parse_rtattr(struct rtattr *tb[], size_t max, struct rtattr *rta, size_t len) -{ - return sitnl_parse_rtattr_flags(tb, max, rta, len, 0); -} - #define sitnl_parse_rtattr_nested(tb, max, rta) \ (sitnl_parse_rtattr_flags(tb, max, RTA_DATA(rta), RTA_PAYLOAD(rta), NLA_F_NESTED)) @@ -1436,13 +1827,16 @@ net_iface_type(openvpn_net_ctx_t *ctx, const char *iface, char type[IFACE_TYPE_LEN_MAX]) { struct sitnl_link_req req = {}; - int ifindex = if_nametoindex(iface); + int ifindex = openvpn_if_nametoindex(iface, ctx); + int orig = -1; if (!ifindex) { return -errno; } + orig = netns_switch(ctx); + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(req.i)); req.n.nlmsg_flags = NLM_F_REQUEST; req.n.nlmsg_type = RTM_GETLINK; @@ -1455,26 +1849,32 @@ int ret = sitnl_send(&req.n, 0, 0, sitnl_type_save, type); if (ret < 0) { + netns_restore(orig); msg(D_ROUTE, "%s: cannot retrieve iface %s: %s (%d)", __func__, iface, strerror(-ret), ret); return ret; } msg(D_ROUTE, "%s: type of %s: %s", __func__, iface, type); + netns_restore(orig); + return 0; } int net_iface_del(openvpn_net_ctx_t *ctx, const char *iface) { + int ret = -1, orig = -1; struct sitnl_link_req req = {}; - int ifindex = if_nametoindex(iface); + int ifindex = openvpn_if_nametoindex(iface, ctx); if (!ifindex) { return -errno; } + orig = netns_switch(ctx); + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(req.i)); req.n.nlmsg_flags = NLM_F_REQUEST; req.n.nlmsg_type = RTM_DELLINK; @@ -1484,7 +1884,11 @@ msg(D_ROUTE, "%s: delete %s", __func__, iface); - return sitnl_send(&req.n, 0, 0, NULL, NULL); + ret = sitnl_send(&req.n, 0, 0, NULL, NULL); + + netns_restore(orig); + + return ret; } #endif /* !ENABLE_SITNL */ diff --git a/src/openvpn/networking_sitnl.h b/src/openvpn/networking_sitnl.h index 481cc36..7e723fe 100644 --- a/src/openvpn/networking_sitnl.h +++ b/src/openvpn/networking_sitnl.h @@ -21,7 +21,97 @@ #ifndef NETWORKING_SITNL_H_ #define NETWORKING_SITNL_H_ +#include "env_set.h" + typedef char openvpn_net_iface_t; -typedef void *openvpn_net_ctx_t; + +struct openvpn_net_ctx +{ + const char *netns; + struct gc_arena gc; +}; + +typedef struct openvpn_net_ctx openvpn_net_ctx_t; + +/** + * @brief Switch the current thread to the network namespace specified + * in the given OpenVPN network context. + * + * This function changes the calling thread's network namespace to the one + * identified by ctx->netns. The current (original) network namespace file + * descriptor is saved and returned, so it can later be restored using + * netns_restore(). + * + * If @p ctx is NULL or ctx->netns is NULL, the function fails and returns -1. + * + * The switch is performed using setns(2). This approach is required because + * the netlink library does not support performing operations in an arbitrary + * target network namespace, except for interface creation and deletion. + * Therefore, in order to execute generic netlink operations inside a + * specific network namespace, the thread must temporarily enter + * that namespace via setns(). + * + * @param ctx Pointer to an OpenVPN network context structure containing + * the target network namespace name (ctx->netns). The namespace + * is expected to exist under NETNS_RUN_DIR (e.g. /run/netns/). + * + * @return On success, returns a file descriptor referring to the original + * network namespace. This descriptor must be passed to + * netns_restore() to switch back. + * @return -1 on failure (an error is logged and no namespace switch is kept). + * + * @note The returned file descriptor must be passed to netns_restore(), + * which will restore the original namespace and close it. + */ +int netns_switch(openvpn_net_ctx_t *ctx); + +/** + * @brief Restore the previously saved network namespace. + * + * This function restores the network namespace saved by netns_switch() + * using the file descriptor returned by that function. + * + * If @p orig_fd is negative, the function does nothing and returns + * immediately. + * + * The restoration is performed using setns(2), switching the calling + * thread back to its original network namespace. + * + * @param orig_fd File descriptor of the original network namespace, + * as returned by netns_switch(). + * + * @note This function always closes @p orig_fd if it is >= 0, + * regardless of whether setns() succeeds or fails. + * After calling this function, @p orig_fd must not be reused. + */ +void netns_restore(int orig_fd); + +/** + * Resolve a network interface name to its interface index. + * + * If a valid network namespace ID is provided, the lookup is performed inside + * that network namespace using Netlink. Otherwise, this function falls back + * to the standard `if_nametoindex()` call in the current namespace. + * + * @param ifname Name of the network interface. + * @param ctx The network context where we retrieve + * the network namespace name. + * + * @return Interface index on success, or 0 on error. + */ +int openvpn_if_nametoindex(const char *ifname, openvpn_net_ctx_t *ctx); + +/** + * Retrieve or create a network namespace ID (NSID) for a given namespace. + * + * This function first attempts to retrieve the NSID associated with the + * specified network namespace. If no NSID is currently assigned, it + * requests the kernel to create one and then retries the lookup. + * + * @param name Name of the network namespace. + * + * @return The network namespace ID on success, or -1 on failure. + */ +int get_or_create_netnsid_sitnl(const char *name); #endif /* NETWORKING_SITNL_H_ */ diff --git a/src/openvpn/options.c b/src/openvpn/options.c index 1db781d..d29f835 100644 --- a/src/openvpn/options.c +++ b/src/openvpn/options.c @@ -6462,6 +6462,14 @@ VERIFY_PERMISSION(OPT_P_GENERAL); options->mtu_test = true; } + else if (streq(p[0], "netns") && p[1] && !p[2]) + { +#ifndef ENABLE_SITNL + msg(M_WARN, "NOTE: --netns is supported only on Linux when compiled with SITNL"); +#endif + VERIFY_PERMISSION(OPT_P_GENERAL); + options->netns = p[1]; + } else if (streq(p[0], "nice") && p[1] && !p[2]) { VERIFY_PERMISSION(OPT_P_NICE); diff --git a/src/openvpn/options.h b/src/openvpn/options.h index 3d8b505..6e54d8f 100644 --- a/src/openvpn/options.h +++ b/src/openvpn/options.h @@ -315,6 +315,7 @@ struct dns_options dns_options; + const char *netns; bool remote_random; const char *ipchange; const char *dev; diff --git a/src/openvpn/tun.c b/src/openvpn/tun.c index f46802f..68378d8 100644 --- a/src/openvpn/tun.c +++ b/src/openvpn/tun.c @@ -2082,6 +2082,13 @@ } else { +#if defined(TARGET_LINUX) && defined(ENABLE_SITNL) + int orig_fd = -1; + if (ctx->netns) + { + orig_fd = netns_switch(ctx); + } +#endif /* if defined(TARGET_LINUX) && defined(ENABLE_SITNL) */ /* * Process --dev-node */ @@ -2098,6 +2105,12 @@ { msg(M_ERR, "ERROR: Cannot open TUN/TAP dev %s", node); } +#if defined(TARGET_LINUX) && defined(ENABLE_SITNL) + if (orig_fd != -1) + { + netns_restore(orig_fd); + } +#endif /* * Process --tun-ipv6