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


Reply via email to