一个network namespace是网络栈的一个逻辑拷贝,它包含了自有的路由表、防火墙规则和网络设备。

首先假定已经基本了解network namespace(网络命名空间)并熟悉使用iproute2工具包的ip命令操作netns。本文主要记录相关操作命令的实现逻辑,比如netns的名字是如何设定的、netns在内核中是如何存在的、用户态进程是如何与netns关联的。

本文运行环境:

  • 操作系统 CentOS Linux release 7.5.1804
  • 内核版本 3.10.0-862.el7.x86_64

netns 相关内核结构

netns相关结构体这里介绍三个,分别为

  • struct net
    netns网络命名空间自身。
  • struct nsproxy
    每个进程所属的所有命名空间整合在该结构体中,体现了namespace与进程的关系。
  • struct net_device
    网络接口设备结构体,体现了netns与网络设备的关系。

net 结构

struct net结构是网络命名空间自身的结构体。这里简单说明一下几个比较重要的成员

  • list
    所有netns通过其成员list串联成一个链表,链表头是全局变量net_namespace_list
  • user_ns
    为了管理netns的权限,有一个struct user_namespace指针成员user_ns负责管控权限。
  • loopback_dev
    为每个netns中的回环网络设备。
  • rtnl/genl_sock
    之前关于netlinkgenetlink的介绍中有提到过netlink socket是与netns关联的,这里的成员rtnlgenl_sock就分别是该netns中rtnetlink和genetlink的指针,其他类型的netlink也有对应成员。
  • proc_inum
    该netns在proc文件系统中对应的inode号,可以考虑在内核模块输出调试信息时用于唯一标记该netns,相比输出net结构体地址更友好一些。

这里说一下netns本身是没有名字的,使用ip命令时赋予和操作的名字仅仅是ip命令内部为了便于区分netns在文件系统中创建的与netns对应的文件的文件名,文件所在目录为/var/run/netns/,这样可以使用户态程序更简单的区分和操作netns,netns 操作部分可以看到ip命令对该名字的实现机制。

初始netns有一个全局变量init_net,全局变量定义于文件net/core/net_namespace.c。

struct net 结构部分成员。定义于include/net/net_namespace.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct net {
/* ... */

struct list_head list;

/* ... */

struct user_namespace *user_ns;

/* ... */

unsigned int proc_inum;

/* ... */

struct sock *rtnl;
struct sock *genl_sock;

/* ... */

struct net_device *lookback_dev

/* ... */
}

与进程关系

Linux中namespace机制的目的是资源隔离,资源的生效体现在进程中,netns是namespace的一种,因此一定会与代表进程的结构体task_struct有关联。实际上task_struct中有一个struct nsproxy指针类型的成员nsproxy保存了进程所关联的所有namespace,也包括netns。

nsproxy与netns类似也有一个全局变量init_nsproxy作为初始的nsproxy。

struct task_struct结构,定义位于include/linux/sched.h
1
2
3
4
5
6
7
8
struct task_struct {
/* ...... */

/* namespaces */
struct nsproxy *nsproxy;

/* ...... */
}
struct nsproxy结构。定义位于include/linux/nsproxy.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* A structure to contain pointers to all per-process
* namespaces - fs (mount), uts, network, sysvipc, etc.
*
* 'count' is the number of tasks holding a reference.
* The count for each namespace, then, will be the number
* of nsproxies pointing to it, not the number of tasks.
*
* The nsproxy is shared by tasks which share all namespaces.
* As soon as a single namespace is cloned or unshared, the
* nsproxy is copied.
*/
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns;
struct net *net_ns;
};
extern struct nsproxy init_nsproxy;
init_nsproxy,定义位于kernel/nsproxy.c
1
2
3
4
5
6
7
8
9
10
11
12
struct nsproxy init_nsproxy = {
.count = ATOMIC_INIT(1),
.uts_ns = &init_uts_ns,
#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)
.ipc_ns = &init_ipc_ns,
#endif
.mnt_ns = NULL,
.pid_ns = &init_pid_ns,
#ifdef CONFIG_NET
.net_ns = &init_net,
#endif
};

与网络设备关系

Linux中每个网络设备都归属一个netns,直接体现在网络设备对应的结构体中。

struct net_device,定义位于include/linux/netdevice.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct net_device {

/*
* This is the first field of the "visible" part of this structure
* (i.e. as seen by users in the "Space.c" file). It is the name
* of the interface.
*/
char name[IFNAMSIZ];

/* device name hash chain, please keep it close to name[] */
struct hlist_node name_hlist;

/* snmp alias */
char *ifalias;

/* ...... */

/* Network namespace this network device is inside */
possible_net_t nd_net;

/* ...... */

}

其中netns为possible_net_t类型成员nd_net,在本文参考内核源码中定义为:

#define possible_net_t struct net *

好奇翻了一下内核提交记录,possible_net_t类型在原始Linux内核中定义于rhel内核并不相同。

在最初,并不存在possible_net_t类型,在netns引入之初,所有需要struct net指针成员的结构体都需要用如下方式引入该成员:

1
2
3
#ifdef CONFIG_NET_NS
struct net *nd_net;
#endif

同时用配置CONFIG_NET_NS编译时选择不同的方式实现read_pnetwrite_pnet

2015年一位同学不爽这种冗余且容易出错的方式,因此引入了possible_net_t,具体参考提交记录,这时内核版本已经是4了,而且定义与本文参考的内核源码并不相同,如下:

1
2
3
4
5
typedef struct {
#ifdef CONFIG_NET_NS
struct net *net;
#endif
} possible_net_t;

同时配合修改了read_pnetwrite_pnet

这样需要netns的结构体中直接加入possible_net_t类型成员就可以了,不需要在结构体定义中考虑编译配置CONFIG_NET_NS。直到当前最新(2019年3月)的内核源码中,该定义仍没有变化。

由于我没有找到可以简单查看rhel内核源码修改记录的方式,因此没有查到具体rhel引入该类型的历史如何,但可以想见,rhel内核将Linux内核版本4的该修改引入到更早的版本3中,同时引入该修改的工程师更喜欢另一种实现方式,也就是在这里看到的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define possible_net_t  struct net *

static inline void write_pnet(possible_net_t *pnet, struct net *net)
{
#ifdef CONFIG_NET_NS
*pnet = net;
#endif
}

static inline struct net *read_pnet(possible_net_t const *pnet)
{
#ifdef CONFIG_NET_NS
return *pnet;
#else
return &init_net;
#endif
}

netns 模块初始化

netns模块由pure_initcall(net_ns_init);注册了系统启动时运行函数net_ns_initinit_net就是在这里初始化的。这里还通过register_pernet_subsys函数注册了net_ns_ops,也就是每个netns在创建时将会分配proc文件系统的inode号赋予给成员proc_inum,在netns销毁时释放。

net_ns_ops
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static __net_init int net_ns_net_init(struct net *net)
{
return proc_alloc_inum(&net->proc_inum);
}

static __net_exit void net_ns_net_exit(struct net *net)
{
proc_free_inum(net->proc_inum);
}

static struct pernet_operations __net_initdata net_ns_ops = {
.init = net_ns_net_init,
.exit = net_ns_net_exit,
};
struct pernet_operations
1
2
3
4
5
6
7
8
struct pernet_operations {
struct list_head list;
int (*init)(struct net *net);
void (*exit)(struct net *net);
void (*exit_batch)(struct list_head *net_exit_list);
int *id;
size_t size;
};

函数register_pernet_subsys的作用就是将struct pernet_operations类型变量加入到一个链表中,而且对已经存在的netns运行init函数指针,每个新的netns创建后也会遍历执行该链表中的函数指针。netns销毁或struct pernet_operations类型变量调用unregister_pernet_subsys时同理执行exit函数指针。

netns 相关系统调用

命名空间是对进程访问资源的隔离,对进程命名空间的修改也就是操作task_struct成员nsproxy。相关系统调用如下。

clone

clone系统调用用于创建一个新的process(可以是进程,也可以是线程)。glibs对该系统调用的包装函数为:

long clone(unsigned long flags, void *child_stack, void *ptid, void *ctid, struct pt_regs *regs);

其中flags用于控制新process各项属性,如果包含CLONE_NEWNET将会为新process创建一个新的netns。

函数细节参考man 2 clone

unshare

unshare系统调用用于将当前进程的一部分执行上下文(与其他进程共享的上下文)解除关联。glibc对该系统调用的包装函数为:

int unshare(int flags);

其中flags用于设置需要解除关联的上下文类型,如果包含CLONE_NEWNET将会为当前进程创建一个新的netns。

unshare与clone在对netns操作上的区别在于,unshare操作的是当前进程,而且不会有新的process被创建。

函数细节参考man 2 unshare

setns

setns系统调用用于将当前线程与一个指定的命名空间关联。glibc对该系统调用的包装函数为:

int setns(int fd, int nstype);

其中nstype用于设置需要关联的命名空间类型,比如CLONE_NEWNET标识设置的是网络命名空间,该参数需要与fd对应的命名空间文件匹配。。fd是与命名空间关联的文件的文件描述符,与命名空间关联的文件的典型位置为/proc/[pid]/ns/目录,proc文件系统中对应命名空间的文件可以通过绑定挂载(bind mounting)的方式挂载到文件系统的其他位置以达到该命名空间内所有进程都关闭后仍然可以保持命名空间存在的目的(ip netns使用该特性保持其创建的netns存在)。如果没有绑定挂载和保持打开的文件描述符,那么命名空间内所有进程退出后该命名空间将被释放。

proc文件系统部分参考man 5 proc

setns函数细节参考man 2 setns

netns 操作

这里参考iproute2工具包源码,用简化的实例代码的形式,演示network namespace是如何操作的。

netns 创建

在当前目录创建与新netns关联的文件,文件名作为标识。

netns_add.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <errno.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char **argv) {
char *name = argv[1];
int fd;

if (argc < 2) {
printf("Usage: %s netns_name\n", argv[0]);
return -1;
}

// 使用unshare将进程的network namespace剥离,创建一个新的network namespace
if (unshare(CLONE_NEWNET) < 0) {
fprintf(stderr, "Failed to create a new network namespace \"%s\": %s\n",
name, strerror(errno));
return -1;
}

fd = open(name, O_RDONLY|O_CREAT|O_EXCL, 0);
if (fd < 0) {
fprintf(stderr, "Cannot create namespace file \"%s\": %s\n",
name, strerror(errno));
return -1;
}
close(fd);

// 使用绑定挂载保持新network namespace在进程退出后依然存在,
// 并使用文件名作为该network namespace的标识。
// 为什么绑定挂载可以达到这个目的,可能与proc文件系统中ns实现有关,这里不做关注
if (mount("/proc/self/ns/net", name, "none", MS_BIND, NULL) < 0) {
fprintf(stderr, "Bind /proc/self/ns/net -> %s failed: %s\n",
name, strerror(errno));
return -1;
}

return 0;
}

netns 中执行进程

以当前目录与netns关联的文件为目标,在其netns中执行参数指定的命令。

netns_exec.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>

int netns_switch(char *name)
{
int netns;

netns = open(name, O_RDONLY | O_CLOEXEC);
if (netns < 0) {
fprintf(stderr, "Cannot open network namespace \"%s\": %s\n",
name, strerror(errno));
return -1;
}

// 使用setns切换到指定的network namespace
if (setns(netns, CLONE_NEWNET) < 0) {
fprintf(stderr, "setting the network namespace \"%s\" failed: %s\n",
name, strerror(errno));
close(netns);
return -1;
}
close(netns);

// 处理mount

// 处理/etc下配置文件

return 0;
}

int main(int argc, char **argv) {
if (argc < 3) {
printf("Usage: %s netns_name cmd\n", argv[0]);
return -1;
}

// 使用setns切换到指定的network namespace
if (netns_switch(argv[1])) {
return -1;
}

// 进程已经切换到指定network namespace,执行参数指定的可执行程序
if (execvp(argv[2], &argv[2]) < 0) {
fprintf(stderr, "exec of \"%s\" failed: %s\n",
argv[2], strerror(errno));
return -1;
}

return 0;
}

netns 中添加网络设备

这里代码的功能参考ip li set DEV netns NETNS命令。

与前面单纯操作netns不同,这里操作的对象是网络设备,不再使用前面介绍的3个系统调用,而是使用rtnetlink的方式设定网络设备的netns。可以参考前面写过的获取网卡列表的几种方式Linux netlink socket 内核通信。使用的属性IFLA_NET_NS_FD在man文档有没有看到,查看iproute2具体源码得到该使用方式。

netns_set.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/rtnetlink.h>
#include <net/if.h>

#define NLMSG_TAIL(nmsg) \
((struct rtattr *) (((void *) (nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len)))

struct iplink_req {
struct nlmsghdr n;
struct ifinfomsg i;
char buf[1024];
};

int addattr_l(struct nlmsghdr *n, int maxlen, int type, const void *data,
int alen)
{
int len = RTA_LENGTH(alen);
struct rtattr *rta;

if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len) > maxlen) {
fprintf(stderr,
"addattr_l ERROR: message exceeded bound of %d\n",
maxlen);
return -1;
}
rta = NLMSG_TAIL(n);
rta->rta_type = type;
rta->rta_len = len;
if (alen)
memcpy(RTA_DATA(rta), data, alen);
n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len);
return 0;
}

int main(int argc, char **argv) {
char *nsname;
char *devname;
int fd;
int ret;

struct iplink_req req = {
.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
.n.nlmsg_type = RTM_NEWLINK,
.i.ifi_family = AF_UNSPEC,
};

// 模仿ip命令,可以是文件fd,也可以是进程pid
int netns;

if (argc < 3) {
printf("Usage: %s dev_name netns_name\n", argv[0]);
return -1;
}

devname = argv[1];
nsname = argv[2];

// 填充netns,可以是文件fd,也可以是进程pid
netns = open(nsname, O_RDONLY);
if (netns >= 0) {
addattr_l(&req.n, sizeof(req), IFLA_NET_NS_FD, &netns, 4);
} else if (nsname[0] >= '0' && nsname[0] <= '1') {
// 当做进程pid处理,实例代码不做完善校验
netns = atoi(nsname);
addattr_l(&req.n, sizeof(req), IFLA_NET_NS_PID, &netns, 4);
} else {
fprintf(stderr, "Invalid netns value \"%s\"\n", nsname);
return -1;
}

// 填充网络设备索引号
req.i.ifi_index = if_nametoindex(devname);
if (req.i.ifi_index == 0) {
fprintf(stderr, "Cannot find device \"%s\", %s\n", devname, strerror(errno));
return -1;
}

// 发送请求
fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd == -1) {
fprintf(stderr, "socket failed, %s\n", strerror(errno));
return -1;
}

ret = send(fd, &req, req.n.nlmsg_len, 0);
if (ret == -1) {
fprintf(stderr, "send failed, %s\n", strerror(errno));
return -1;
}

// 接收请求结果
ret = recv(fd, &req, sizeof(req), 0);
if (ret == -1) {
fprintf(stderr, "recv failed, %s\n", strerror(errno));
return -1;
}
if (req.n.nlmsg_type == NLMSG_ERROR) {
struct nlmsgerr *err;
err = NLMSG_DATA(&req.n);
if (err->error != 0) {
fprintf(stderr, "%s\n", strerror(-err->error));
return -1;
}
} else {
fprintf(stderr, "unknown nlmsg_type %hu\n", req.n.nlmsg_type);
return -1;
}

return 0;
}

netns 删除

netns_del.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mount.h>
#include <errno.h>
#include <string.h>


int main(int argc, char **argv) {
char *name = argv[1];

if (argc < 2) {
printf("Usage: %s netns_name\n", argv[0]);
return -1;
}

// 对于绑定挂载的文件,需要先umount
if (umount2(name, MNT_DETACH)) {
// 出错不影响继续
}

// umount该文件与network namespace不再有关系,删除
if (unlink(name) < 0) {
fprintf(stderr, "Cannot remove namespace file \"%s\": %s\n",
name, strerror(errno));
return -1;
}

return 0;
}