如何从单个进程实例创建多个网络命名空间

20

我正在使用下面的C函数,从一个单一的进程实例中创建多个网络命名空间

void create_namespace(const char *ns_name)
{
    char ns_path[100];

    snprintf(ns_path, 100, "%s/%s", "/var/run/netns", ns_name);
    close(open(ns_path, O_RDONLY|O_CREAT|O_EXCL, 0));
    unshare(CLONE_NEWNET);
    mount("/proc/self/ns/net", ns_path, "none", MS_BIND , NULL);
}

在我的进程创建了所有的命名空间并将一个tap接口添加到任何一个网络命名空间(使用ip link set tap1 netns ns1命令)之后,我实际上会在所有的命名空间中看到这个接口(这可能实际上是一个单一的命名空间,在不同的名称下运行)。

但是,如果我使用多个进程创建多个命名空间,那么一切都正常工作。

这里可能出了什么问题?我是否需要在单个进程实例中传递任何其他标志以使其工作?单个进程实例无法创建多个网络命名空间吗?还是mount()调用存在问题,因为/proc/self/ns/net实际上被多次挂载?

更新: 似乎unshare()函数正确地创建了多个网络命名空间,但在/var/run/netns/中的所有挂载点实际上都引用了在该目录中挂载的第一个网络命名空间。

更新2: 似乎最好的方法是fork()另一个进程并从那里执行create_namespace()函数。无论如何,我很乐意听到一个更好的解决方案,不涉及fork()调用,或者至少得到一个证实,证明无法从单个进程创建和管理多个网络命名空间。

更新3: 我能够使用以下代码使用unshare()创建多个命名空间:

int  main() {
    create_namespace("a");
    system("ip tuntap add mode tap tapa");
    system("ifconfig -a");//shows lo and tapA interface
    create_namespace("b");
    system("ip tuntap add mode tap tapb");
    system("ifconfig -a");//show lo and tapB interface, but does not show tapA. So this is second namespace created.
}

但是在进程终止后,当我执行ip netns exec a ifconfig -aip netns exec b ifconfig -a时,似乎两个命令突然都在a命名空间中执行。因此,实际的问题是存储对命名空间的引用(或以正确的方式调用mount()函数)。但我不确定这是否可行。

2个回答

20

网络命名空间是通过调用clone创建的,按设计要求,并且可以在之后通过unshare进行修改。请注意,即使您使用unshare创建了一个新的网络命名空间,实际上您只是修改了正在运行的进程的网络堆栈。 unshare无法修改其他进程的网络堆栈,因此您将无法仅使用unshare创建另一个网络命名空间。

为了正常工作,新的网络命名空间需要一个新的网络堆栈,因此它需要一个新的进程。就是这样。

好消息是,使用clone可以使其非常轻量级,请参见

Clone()与UNIX中传统的fork()系统调用不同,它允许父进程和子进程选择性地共享或复制资源。

你只能在这个网络堆栈上进行转移(避免内存空间,文件描述符表和信号处理程序表)。你的新网络进程可以更像是一个线程而不是一个真正的fork。
你可以使用C代码或Linux内核和/或LXC工具来操作它们。
例如,要将设备添加到新的网络命名空间中,只需执行以下简单操作:
echo $PID > /sys/class/net/ethX/new_ns_pid

请查看此页面以获取更多关于可用CLI的信息。

在C端,您可以查看lxc-unshare的实现。尽管它的名称是这样的,但它使用clone,正如您可以看到(lxc_clone就在这里)。您还可以查看LTP实现,其中作者选择直接使用fork。

编辑:有一个小技巧可以让它们保持持久化,但是你仍然需要分叉,即使是暂时的。

看一下这段 ipsource2 的代码(为了清晰起见,我已经删除了错误检查):

snprintf(netns_path, sizeof(netns_path), "%s/%s", NETNS_RUN_DIR, name);

/* Create the base netns directory if it doesn't exist */
mkdir(NETNS_RUN_DIR, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH);

/* Create the filesystem state */
fd = open(netns_path, O_RDONLY|O_CREAT|O_EXCL, 0);
[...]
close(fd);
unshare(CLONE_NEWNET);
/* Bind the netns last so I can watch for it */
mount("/proc/self/ns/net", netns_path, "none", MS_BIND, NULL)

如果您在分叉进程中执行此代码,就可以随意创建新的网络命名空间。为了删除它们,您只需卸载并删除此绑定即可:
umount2(netns_path, MNT_DETACH);
if (unlink(netns_path) < 0) [...]

编辑2: 另一个(不太正规的)技巧是使用system执行“ip netns add ..”命令行。


1
+1,但您能解释一下“请注意,您不要使用unshare创建新的网络命名空间”是什么意思吗?请参见更新#3,因为我的理解是unshare()仍然可以创建网络命名空间。clone(CLONE_NEWNET)类似于“我将创建一个具有新网络命名空间的新子进程”,而unshare(CLONE_NEWNET)则类似于“我不想再与父进程共享网络命名空间。所以创建一个新的。”lxc使用clone(),而iproute2使用unshare()。 - user389238
1
我来尝试解释一下。您可以使用unshare为当前进程创建一个网络命名空间,但由于网络命名空间需要一个PID才能正常运行,因此您无法仅使用unshare为同一进程创建一个新的PID。 - Coren
我理解你的观点,但是unshare()仍然可以创建一个新的网络命名空间(这需要更新你的答案)。此外,我猜想,命名空间不一定需要一个实际的PID来存活(例如,在执行“ip netns add nsX”命令后,ip进程终止,但命名空间nsX仍然存在)。我猜测这个限制“为什么无法从单个进程创建多个网络命名空间”与mount()的工作方式有关。 - user389238
1
如果您查看iproute2源代码,您会发现它们通过mount技巧在进程终止后仍保留当前网络堆栈: /* Bind the netns last so I can watch for it */ if (mount("/proc/self/ns/net", netns_path, "none", MS_BIND, NULL) < 0) - Coren
他们使用Linux内核的这个神奇功能,即已删除但仍可用的文件,因为它们仍然被正在运行的进程打开,使他们的网络命名空间持久化。 - Coren

13
如果您需要从另一个进程访问这些名称空间,或者需要获取句柄以在两个名称空间之间切换,则只需绑定挂载/proc/*/ns/*。不需要从单个进程使用多个名称空间。
  • unshare会创建新的名称空间。
  • clone和fork默认情况下不创建任何新的名称空间。
  • 每种类型的“当前”名称空间都分配给进程。可以通过unshare或setns更改它。名称空间集(默认情况下)由子进程继承。

每当您打开(/proc/N/ns/net)时,它将为此文件创建inode,并且所有后续的open()将返回绑定到相同名称空间的文件。详细信息丢失在内核dentry高速缓存中。

此外,每个进程仅有一个/proc/self/ns/net文件条目,并且绑定挂载不会创建此proc文件的新实例。打开这些挂载的文件与直接打开/proc/self/ns/net文件相同(在第一次打开时仍将指向它所指的名称空间)。

看起来“/proc/*/ns”是半成品。

因此,如果您只需要2个名称空间,则可以:

  • 打开/proc/1/ns/net
  • unshare
  • 打开/proc/self/ns/net

并在两者之间切换。

如果您需要超过2个名称空间,则可能需要使用clone()。似乎没有办法为每个进程创建多个/proc/N/ns/net文件。

但是,如果您不需要在运行时切换名称空间或与其他进程共享它们,则可以像这样使用多个名称空间:

  • 打开套接字并在主名称空间中运行进程。
  • unshare
  • 打开套接字并在第二个名称空间(netlink、tcp等)中运行进程
  • unshare
  • ...
  • unshare
  • 开启套接字并在第N个命名空间中运行进程(如netlink、tcp等)
  • 开启的套接字会保留对其网络命名空间的引用,因此它们只有在关闭套接字后才会被回收。

    您还可以使用netlink将接口在命名空间之间移动,方法是在源命名空间上发送netlink命令,并指定目标命名空间,可以通过PID或命名空间文件描述符来指定。

    在访问依赖于特定命名空间的/proc条目之前,需要切换进程的命名空间。一旦“proc”文件打开,它就会保持对该命名空间的引用。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接