一个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
之前关于netlink 和genetlink 的介绍中有提到过netlink socket是与netns关联的,这里的成员rtnl
和genl_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 { 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 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 { char name[IFNAMSIZ]; struct hlist_node name_hlist ; char *ifalias; 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_pnet
和write_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_pnet
和write_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_init
。init_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 ; } 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); 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 ; } 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); return 0 ; } int main (int argc, char **argv) { if (argc < 3 ) { printf ("Usage: %s netns_name cmd\n" , argv[0 ]); return -1 ; } if (netns_switch(argv[1 ])) { return -1 ; } 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, }; int netns; if (argc < 3 ) { printf ("Usage: %s dev_name netns_name\n" , argv[0 ]); return -1 ; } devname = argv[1 ]; nsname = argv[2 ]; 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' ) { 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 ; } if (umount2(name, MNT_DETACH)) { } if (unlink(name) < 0 ) { fprintf (stderr , "Cannot remove namespace file \"%s\": %s\n" , name, strerror(errno)); return -1 ; } return 0 ; }