Linux namespace 简介 part 5 - NET

本文为原文翻译,特此声明。
原文链接:https://blog.jtlebi.fr/2014/01/19/introduction-to-linux-namespaces-part-5-net/
原文中有部分内容为便于理解,并非直译,可能存在一定的疏漏,因此请参考原文理解。

继上一篇 关于NS namespace的文章(挂载点表的隔离),你想更深入更接近一个全功能的VM吗?好!这个系列的上两篇post很明确地专注于此。而使用“NET” namespace隔离网络接口(这是真的,这不是梦)和user/group描述符让它(VM)更加透明。如果你尚未阅读过之前的post,我强烈建议你先阅读一遍这个系列的第一篇post,了解下linux namespace隔离机制。

我们第一次不先讲添加额外的“CLONE_NEWNET”标志到“clone”系统调用。这个留着之后说明。现在,恕我直言,这个namespace最好的开始方式是异常强大的“iproute2”——网管的瑞士军刀。如果你到现在都还没有这个工具,我强烈建议你安装一个。如果你不想这么做,你可以直接跳过解释部分,直达完整的代码示例。

首先,让我们看看现在有哪些网络接口。

ip link list

它会像这样输出:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT qlen 1000
    link/ether **:**:**:**:**:** brd ff:ff:ff:ff:ff:ff
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DORMANT qlen 1000
    link/ether **:**:**:**:**:** brd ff:ff:ff:ff:ff:ff
# ...

这里没有什么意料之外的事情。我有一个工作着的loopback,UP(是的,‘UNKNOWN’表示‘UP’),我也连接着我的无线网络,当然这里还有其他的连接,只不过为了这篇文章被隐藏了。

现在,我们创建一个网络namespace,并从内部运行同样的命令:

# create a network namespace called "demo"
ip netns add demo
# exec "ip link list" inside the namespace
ip netns exec demo ip link list

输出如下:

1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

嗯,这里有一个loopback,它的状态为“DOWN”。更有趣的是,它是完全和主loopback隔离的。也就是说,任何在namespace内和这个loopback绑定的应用,只能够和同样在这个namespace内的软件通信。准确的说,和IPC namespce同样的隔离级别。是不是很high?

好,但是我该如何与interwebz通讯呢?

这里有几种解决方案。最简单最普通的一种是,在“Host”与“Guest”之间,创建一个点对点(Point-to-Point)管道。Linux Kernel再一次提供了多种替代品。我建议使用“veth”接口,因为它和生态系统整合得最好,尤其是iproute2。这也是经过很严苛测试的代码,因为它在LXC中被使用,而事实上它来自OpenVZ项目。另一个可选方案是“etun”驱动。它在概念上和另外一个名字相同,但是不知道有任何项目使用了它。

“veth”和“etun”都创建一对虚拟的接口和当前namespace中的另一个连接。你可以选择一个,并在目标namespace中移动它,以此得到一个通讯频道(channel)。为了便于理解,你可以将其想象成复杂的粒子运动。

下一步,给它们一个IP,设置为up,然后ping!如下是一个bash会话(session),能够完成这件事:

# Create a "demo" namespace
ip netns add demo
# create a "veth" pair
ip link add veth0 type veth peer name veth1
# and move one to the namespace
ip link set veth1 netns demo
# configure the interfaces (up + IP)
ip netns exec demo ip link set lo up
ip netns exec demo ip link set veth1 up
ip netns exec demo ip addr add 169.254.1.2/30 dev veth1
ip link set veth0 up
ip addr add 169.254.1.1/30 dev veth0

测试一下!没什么吓人的。

如果你需要使用“veth”技术从“guest”系统联网,你可以设置地址伪装——以“NAT”为人们所知。同样的,为了让一个在namespace监听80端口的webserver,直接在主接口上监听,可以使用“DNAT”——以端口转发为人们所知。我将这个留给读者。

这里是一个基本的例子,让我们快速进入:

# make sure ip forwarding is enabled
echo 1 > /proc/sys/net/ipv4/ip_forward
# enable Internet access for the namespace, assuming you ran the previous example
iptables -t nat -A POSTROUTING -o veth0 -j  MASQUERADE
# Forward main ":80" to guest ":80"
iptables -t nat -A PREROUTING -d <your main ip>/32 -p tcp --dport 80 -j  DNAT --to-destination  169.254.1.2:80

现在,我们将它们放置在一起,并且将CLONE_NEWNET标记添加到clone系统调用。为了简化,我们通过system()直接调用“ip”。

main-5-net.c

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#define STACK_SIZE (1024 * 1024)
// sync primitive
int checkpoint[2];
static char child_stack[STACK_SIZE];
char* const child_args[] = {
    "/bin/bash",
    NULL
};
int child_main(void* arg) {
    char c;
    // init sync primitive
    close(checkpoint[1]);
    // setup hostname
    printf(" - [%5d] World !\n", getpid());
    sethostname("In Namespace", 12);
    // remount "/proc" to get accurate "top" && "ps" output
    mount("proc", "/proc", "proc", 0, NULL);
    // wait for network setup in parent
    read(checkpoint[0], &c, 1);
    // setup network
    system("ip link set lo up");
    system("ip link set veth1 up");
    system("ip addr add 169.254.1.2/30 dev veth1");
    execv(child_args[0], child_args);
    printf("Ooops\n");
    return 1;
}
int main() {
    // init sync primitive
    pipe(checkpoint);
    printf(" - [%5d] Hello ?\n", getpid());
    int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | SIGCHLD, NULL);
    // further init: create a veth pair
    char* cmd;
    asprintf(&cmd, "ip link set veth1 netns %d", child_pid);
    system("ip link add veth0 type veth peer name veth1");
    system(cmd);
    system("ip link set veth0 up");
    system("ip addr add 169.254.1.1/30 dev veth0");
    free(cmd);
    // signal "done"
    close(checkpoint[1]);
    waitpid(child_pid, NULL, 0);
    return 0;
}

试着跑一下!

jean-tiare@jeantiare-Ubuntu:~/blog$ gcc -Wall main.c -o ns && sudo ./ns
 - [22094] Hello ?
 - [    1] World !
root@In Namespace:~/blog$ # run a super-powerful server, fully isolated
root@In Namespace:~/blog$ nc -l 4242
Hi !
Bye...
root@In Namespace:~/blog$ exit
jean-tiare@jeantiare-Ubuntu:~/blog$ # done !

如果在另外一个终端按照如下执行,我们就会看到上述结果。

jean-tiare@jeantiare-Ubuntu:~$ nc 169.254.1.2 4242
Hi !    
Bye...
jean-tiare@jeantiare-Ubuntu:~$ 

为了能够在网络虚拟化上走得更远,你可以了解下Linux内核最近引入的接口类型:macvlan,vlan,vxlans,...

这就是“NET” namespace的全部。它是如此强大,以至于作为“CORE”轻量级网络模拟器的基础。下一篇文章,我们将探索最后一个并且是最复杂的namespace “USER”。谢谢阅读!

已有 3 条评论
  1. justin justin

    确实非常不错,很有收获!

  2. 雾霾 雾霾

    补充下关于DNA那个命令有误
    iptables -t nat -A POSTROUTING -o veth0 -j MASQUERADE &&
    iptables -t nat -A PREROUTING -d yourip/24 -p tcp --dport 80 -j DNAT --to-destination 169.254.1.2:80

    1. 确实有问题,谢谢指正。

说两句: