The guest's vsock device is not a netdev, so rtnetlink cannot move it
between network namespaces. Add an "ip vsock" object that goes through
the "vsock" generic netlink family instead:
ip vsock set netns { PID | NETNSNAME | NETNSFILE }
ip vsock show
"set" issues VSOCK_CMD_DEV_NETNS_SET from inside the target namespace,
since the kernel moves the device to the namespace the request was sent
from. "show" issues VSOCK_CMD_DEV_NETNS_GET and prints where the device
is, relative to the current namespace.
Link:
https://lore.kernel.org/all/[email protected]/
Signed-off-by: Bobby Eshleman <[email protected]>
---
ip/Makefile | 2 +-
ip/ip.c | 3 +-
ip/ip_common.h | 1 +
ip/ipvsock.c | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++++
man/man8/ip-vsock.8 | 121 +++++++++++++++++++++++++++++++
man/man8/ip.8 | 7 +-
6 files changed, 336 insertions(+), 3 deletions(-)
diff --git a/ip/Makefile b/ip/Makefile
index 978f68413025..681140ef8ec4 100644
--- a/ip/Makefile
+++ b/ip/Makefile
@@ -13,7 +13,7 @@ IPOBJ=ip.o ipaddress.o ipaddrlabel.o iproute.o iprule.o
ipnetns.o \
ipvrf.o iplink_xstats.o ipseg6.o iplink_netdevsim.o iplink_rmnet.o \
ipnexthop.o ipmptcp.o iplink_bareudp.o iplink_wwan.o ipioam6.o \
iplink_amt.o iplink_batadv.o iplink_gtp.o iplink_virt_wifi.o \
- iplink_netkit.o ipstats.o iplink_ovpn.o
+ iplink_netkit.o ipstats.o iplink_ovpn.o ipvsock.o
RTMONOBJ=rtmon.o
diff --git a/ip/ip.c b/ip/ip.c
index e4b71bde94b4..68deac93a538 100644
--- a/ip/ip.c
+++ b/ip/ip.c
@@ -63,7 +63,7 @@ static void usage(void)
" macsec | maddress | monitor | mptcp |
mroute | mrule |\n"
" neighbor | neighbour | netconf | netns |
nexthop | ntable |\n"
" ntbl | route | rule | sr | stats | tap |
tcpmetrics |\n"
- " token | tunnel | tuntap | vrf | xfrm }\n"
+ " token | tunnel | tuntap | vrf | vsock |
xfrm }\n"
" OPTIONS := { -V[ersion] | -s[tatistics] | -d[etails] |
-r[esolve] |\n"
" -h[uman-readable] | -iec | -j[son] |
-p[retty] |\n"
" -f[amily] { inet | inet6 | mpls | bridge |
link } |\n"
@@ -113,6 +113,7 @@ static const struct cmd {
{ "netns", do_netns },
{ "netconf", do_ipnetconf },
{ "vrf", do_ipvrf},
+ { "vsock", do_ipvsock },
{ "sr", do_seg6 },
{ "nexthop", do_ipnh },
{ "mptcp", do_mptcp },
diff --git a/ip/ip_common.h b/ip/ip_common.h
index 3f55ea336b37..c82b1132e43c 100644
--- a/ip/ip_common.h
+++ b/ip/ip_common.h
@@ -93,6 +93,7 @@ int do_ipnh(int argc, char **argv);
int do_mptcp(int argc, char **argv);
int do_ioam6(int argc, char **argv);
int do_ipstats(int argc, char **argv);
+int do_ipvsock(int argc, char **argv);
int iplink_get(char *name, __u32 filt_mask);
int iplink_ifla_xstats(int argc, char **argv);
diff --git a/ip/ipvsock.c b/ip/ipvsock.c
new file mode 100644
index 000000000000..ed4347dd0084
--- /dev/null
+++ b/ip/ipvsock.c
@@ -0,0 +1,205 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * ipvsock.c "ip vsock"
+ *
+ * The guest's vsock device is not a netdev, so rtnetlink cannot move it.
+ * The "vsock" generic netlink family moves it with VSOCK_CMD_DEV_NETNS_SET
+ * and reports where it is with VSOCK_CMD_DEV_NETNS_GET. There is nothing to
+ * name in a request: the kernel serves it in the namespace of the socket it
+ * arrived on, so the destination is chosen by sending it from there, and the
+ * reported namespace is an nsid relative to the sender.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <linux/genetlink.h>
+#include <linux/net_namespace.h>
+#include <linux/vsock_netlink.h>
+
+#include "libgenl.h"
+#include "utils.h"
+#include "ip_common.h"
+#include "json_print.h"
+#include "namespace.h"
+
+static void usage(void) __attribute__((noreturn));
+
+static void usage(void)
+{
+ fprintf(stderr,
+ "Usage: ip vsock set netns { PID | NETNSNAME | NETNSFILE }\n"
+ " ip vsock show\n");
+ exit(-1);
+}
+
+static int vsock_talk(__u8 cmd, struct nlmsghdr **answer)
+{
+ GENL_REQUEST(req, 128, 0, 0, VSOCK_FAMILY_VERSION, cmd, NLM_F_REQUEST);
+ struct rtnl_handle grth = { .fd = -1 };
+ int family = -1;
+ int ret;
+
+ if (genl_init_handle(&grth, VSOCK_FAMILY_NAME, &family)) {
+ fprintf(stderr,
+ "Cannot resolve the \"%s\" netlink family, the kernel
may not support moving the vsock device.\n",
+ VSOCK_FAMILY_NAME);
+ return -1;
+ }
+
+ req.n.nlmsg_type = family;
+
+ ret = rtnl_talk(&grth, &req.n, answer);
+ rtnl_close(&grth);
+
+ return ret < 0 ? -2 : 0;
+}
+
+static int vsock_dev_netns_set(const char *netns)
+{
+ int netns_fd, orig_fd, ret;
+
+ netns_fd = netns_get_fd(netns);
+ if (netns_fd < 0) {
+ fprintf(stderr, "Invalid \"netns\" value \"%s\"\n", netns);
+ return -1;
+ }
+
+ orig_fd = open("/proc/self/ns/net", O_RDONLY);
+ if (orig_fd < 0) {
+ fprintf(stderr, "Cannot open namespace: %s\n", strerror(errno));
+ close(netns_fd);
+ return -1;
+ }
+
+ if (setns(netns_fd, CLONE_NEWNET) < 0) {
+ fprintf(stderr, "Cannot set namespace: %s\n", strerror(errno));
+ ret = -1;
+ goto out;
+ }
+
+ /* VSOCK_CMD_DEV_NETNS_SET has no reply, so wait for the ACK. */
+ ret = vsock_talk(VSOCK_CMD_DEV_NETNS_SET, NULL);
+
+ if (setns(orig_fd, CLONE_NEWNET) < 0) {
+ fprintf(stderr, "Cannot restore namespace: %s\n",
+ strerror(errno));
+ ret = -1;
+ }
+
+out:
+ close(orig_fd);
+ close(netns_fd);
+
+ return ret;
+}
+
+static void vsock_print_netnsid(int id)
+{
+ char *name;
+
+ if (is_json_context()) {
+ print_int(PRINT_JSON, "netnsid", NULL, id);
+ return;
+ }
+
+ if (id == NETNSA_NSID_NOT_ASSIGNED) {
+ print_string(PRINT_FP, NULL, " netnsid %s", "unknown");
+ return;
+ }
+
+ name = get_name_from_nsid(id);
+ if (name)
+ print_string(PRINT_FP, NULL, " netns %s", name);
+ else
+ print_int(PRINT_FP, NULL, " netnsid %d", id);
+}
+
+static int vsock_dev_netns_show(void)
+{
+ struct rtattr *tb[VSOCK_A_MAX + 1];
+ struct nlmsghdr *answer;
+ struct genlmsghdr *ghdr;
+ int len, ret;
+
+ ret = vsock_talk(VSOCK_CMD_DEV_NETNS_GET, &answer);
+ if (ret)
+ return ret;
+
+ ghdr = NLMSG_DATA(answer);
+ len = answer->nlmsg_len - NLMSG_LENGTH(GENL_HDRLEN);
+ if (len < 0) {
+ fprintf(stderr, "Wrong vsock reply length %d\n", len);
+ free(answer);
+ return -1;
+ }
+
+ parse_rtattr(tb, VSOCK_A_MAX, (void *)ghdr + GENL_HDRLEN, len);
+
+ new_json_obj(json);
+ open_json_object(NULL);
+ print_string(PRINT_ANY, "dev", "%s", "vsock");
+ if (tb[VSOCK_A_NETNS_ID])
+ vsock_print_netnsid(rta_getattr_s32(tb[VSOCK_A_NETNS_ID]));
+ print_nl();
+ close_json_object();
+ delete_json_obj();
+
+ free(answer);
+
+ return 0;
+}
+
+static int vsock_set(int argc, char **argv)
+{
+ const char *netns = NULL;
+
+ while (argc > 0) {
+ if (strcmp(*argv, "netns") == 0) {
+ NEXT_ARG();
+ if (netns)
+ duparg("netns", *argv);
+ netns = *argv;
+ } else if (matches(*argv, "help") == 0) {
+ usage();
+ } else {
+ invarg("unknown argument", *argv);
+ }
+ argc--;
+ argv++;
+ }
+
+ if (!netns) {
+ fprintf(stderr, "Not enough information: \"netns\" argument is
required.\n");
+ exit(-1);
+ }
+
+ return vsock_dev_netns_set(netns);
+}
+
+int do_ipvsock(int argc, char **argv)
+{
+ if (argc < 1)
+ return vsock_dev_netns_show();
+
+ if (matches(*argv, "show") == 0 ||
+ matches(*argv, "list") == 0) {
+ if (argc > 1)
+ invarg("unknown argument", argv[1]);
+ return vsock_dev_netns_show();
+ }
+
+ if (matches(*argv, "set") == 0)
+ return vsock_set(argc - 1, argv + 1);
+
+ if (matches(*argv, "help") == 0)
+ usage();
+
+ fprintf(stderr, "Command \"%s\" is unknown, try \"ip vsock help\".\n",
+ *argv);
+ exit(-1);
+}
diff --git a/man/man8/ip-vsock.8 b/man/man8/ip-vsock.8
new file mode 100644
index 000000000000..18e618241523
--- /dev/null
+++ b/man/man8/ip-vsock.8
@@ -0,0 +1,121 @@
+.TH IP\-VSOCK 8 "5 Nov 2026" "iproute2" "Linux"
+.SH "NAME"
+ip-vsock \- guest vsock device network namespace management
+.SH "SYNOPSIS"
+.sp
+.ad l
+.in +8
+.ti -8
+.B ip
+.RI "[ " OPTIONS " ]"
+.B vsock
+.RI "{ " COMMAND " | "
+.BR help " }"
+.sp
+
+.ti -8
+.B ip vsock set netns
+.RI "{ " PID " | " NETNSNAME " | " NETNSFILE " }"
+
+.ti -8
+.B ip vsock show
+
+.SH DESCRIPTION
+The
+.B ip vsock
+command manages the network namespace of the guest's vsock device, the one
+owned by the guest-to-host transport. That device is not a network device, so
+it cannot be moved with
+.BR "ip link set" " ... " netns .
+Instead,
+.B ip vsock
+uses the
+.B vsock
+generic netlink family.
+
+.SS ip vsock set netns - move the guest vsock device to a network namespace
+
+Move the device to the network namespace associated with process
+.IR "PID " or
+the name
+.IR "NETNSNAME " or
+the file
+.IR "NETNSFILE" .
+This is carried out with the
+.B VSOCK_CMD_DEV_NETNS_SET
+command, which the kernel serves in the namespace the request is sent from, so
+.B ip vsock
+sends it from inside the destination namespace.
+
+The vsock mode of the destination namespace, reported by
+.IR /proc/sys/net/vsock/ns_mode " and inherited from the creating namespace's"
+.IR /proc/sys/net/vsock/child_ns_mode ,
+then decides who may use the device. A
+.B global
+mode namespace shares it with every other global mode namespace, while a
+.B local
+mode namespace reserves the connection to the host for itself.
+
+The device starts out in the initial namespace, so moving it back there is how
+an assignment is undone. An assignment keeps the namespace alive, so deleting
+the namespace does not move the device. Connections made before the move, from
+a namespace that can no longer reach the device, are reset.
+
+Support is transport dependent: a guest-to-host transport that does not
+implement the move refuses the request with
+.BR EOPNOTSUPP ,
+as does a loaded host-to-guest transport that cannot check the device's CID.
+The move is refused with
+.B EADDRINUSE
+if a guest in the destination namespace already uses the device's CID. The
+command requires
+.B CAP_NET_ADMIN
+in the initial user namespace.
+
+.SS ip vsock show - show the network namespace of the guest vsock device
+
+Show the network namespace the device is in, relative to the current one,
+using the
+.B VSOCK_CMD_DEV_NETNS_GET
+command. This is also what
+.B ip vsock
+does when given no command. Nothing follows
+.B vsock
+when the device is in the current namespace. Otherwise the namespace is printed
+as
+.BI "netns " NETNSNAME
+if it has a name, or as
+.BI "netnsid " ID
+if it does not. When the current namespace cannot reach the device, or no nsid
+can be allocated for the device's namespace, the kernel does not say where the
+device is, and
+.B netnsid unknown
+is printed instead.
+
+.SH EXAMPLES
+.PP
+ip netns add guest
+.br
+ip vsock set netns guest
+.RS 4
+Move the guest vsock device to the network namespace "guest".
+.RE
+.PP
+ip vsock show
+.RS 4
+Show which network namespace the guest vsock device is in.
+.RE
+.PP
+ip vsock set netns 1
+.RS 4
+Move the device back to the initial network namespace, undoing the assignment.
+.RE
+
+.SH SEE ALSO
+.br
+.BR ip (8),
+.BR ip-netns (8),
+.BR vsock (7)
+
+.SH AUTHOR
+Bobby Eshleman <[email protected]>
diff --git a/man/man8/ip.8 b/man/man8/ip.8
index fdae57c5f651..0f0a1560e2c3 100644
--- a/man/man8/ip.8
+++ b/man/man8/ip.8
@@ -24,7 +24,7 @@ ip \- show / manipulate routing, network devices, interfaces
and tunnels
mroute " | " mrule " | " neighbor " | " neighbour " | " netconf " | "\
netns " | " nexthop " | " ntable " | " ntbl " | " route " | " rule " | "\
sr " | " tap " | " tcpmetrics " | " token " | " tunnel " | " tuntap " | "\
- vrf " | " xfrm " }"
+ vrf " | " vsock " | " xfrm " }"
.sp
.ti -8
@@ -356,6 +356,10 @@ Request the kernel to send the applied configuration back.
.B vrf
- manage virtual routing and forwarding devices.
+.TP
+.B vsock
+- manage the network namespace of the guest vsock device.
+
.TP
.B xfrm
- manage IPSec policies.
@@ -463,6 +467,7 @@ was written by Alexey N. Kuznetsov and added in Linux 2.2.
.BR ip-token (8),
.BR ip-tunnel (8),
.BR ip-vrf (8),
+.BR ip-vsock (8),
.BR ip-xfrm (8)
.br
.RB "IP Command reference " ip-cref.ps
--
2.53.0-Meta