网络命名空间将内核网络协议栈(路由、流控、Netfilter、网桥等系统)虚拟成多个。内核默认创建的网络命名空间init_net。
struct net init_net = {
.count = ATOMIC_INIT(1),
.dev_base_head = LIST_HEAD_INIT(init_net.dev_base_head),
};
网络命名空间创建
iproute2工具集通过unshare(CLONE_NEWNET)系统调用创建新的网络命名空间。到Linux内核中,由函数copy_net_ns处理。其一分配网络命名空间内存;其二调用setup_net初始化命名空间中注册的所有协议栈模块。
struct net *copy_net_ns(unsigned long flags, struct user_namespace *user_ns, struct net *old_net)
{
struct net *net;
if (!(flags & CLONE_NEWNET))
return get_net(old_net);
net = net_alloc();
rv = setup_net(net, user_ns);
}
内核初始化时,各个协议栈模块通过register_pernet_subsys或者register_pernet_device注册其初始化函数到网络命名空间系统中。在新的命名空间创建时,遍历全局链表pernet_list,执行每个子模块注册的初始化函数。
static __net_init int setup_net(struct net *net, struct user_namespace *user_ns)
{
idr_init(&net->netns_ids);
list_for_each_entry(ops, &pernet_list, list)
error = ops_init(ops, net);
}
所有命名空间中注册的子模块(struct pernet_operations)都链接在全局链表pernet_list上。两个命名空间子模块注册函数register_pernet_device和register_pernet_subsys用于注册子模块,功能基本相同,差别在于子模块插入的位置。register_pernet_device将子模块添加在pernet_list的尾部,而register_pernet_subsys将子模块添加在第一个使用register_pernet_device注册的子模块在链表中的尾部。
|--<-----| prev
| |
head-->submod0-->submod1-->subdev0
| |
|---<----------------------| next
prev
|--<------|
| |--<-----| prev
| | |
head-->submod0-->submod1-->submod2-->subdev0
| |
|---<--------------------------------| next
如上图所示,如果链表中已经插入了一个device子模块(subdev0),之后插入的subsys子模块(submod2)将插入在subdev0之前。最终pernet_list链表中的元素排列为,所有使用register_pernet_subsys注册的子模块按照注册顺序位于链表头,而使用register_pernet_device注册的子模块按顺序位于链表尾部。目前内核中注册的device子模块为gre隧道处理、fou、vti、ipip等隧道设备模块,由于位于链表尾部,这些设备模块在创建命名空间时排在最后初始化。
对于采用模块形式动态加载的网络子模块,其在注册命名空间操作时,内核将遍历所有已经创建的网络命名空间,针对每一个命名空间执行其初始化函数。
static int __register_pernet_operations(struct list_head *list, struct pernet_operations *ops)
{
list_add_tail(&ops->list, list);
if (ops->init || (ops->id && ops->size)) {
for_each_net(net)
error = ops_init(ops, net);
}
}
网络命名空间通用数据
网络协议栈的各个模块都会有私有的数据需要保存,网络命名空间提供了net_generic结构可用来保存模块私有数据的地址(指针)。这些私有数据基于命名空间,不同的命名空间保存有不同的模块私有数据,每个模块的私有数据指针按照id为索引保存在net_generic的指针数组成员ptr中。模块的id值在函数register_pernet_device或者register_pernet_subsys的调用中生成,之后返回给调用模块,并且根据参数中提供的私有数据大小,在返回前分配好模块所需的私有数据空间。
static int register_pernet_operations(struct list_head *list, struct pernet_operations *ops)
{
if (ops->id) {
again:
error = ida_get_new_above(&net_generic_ids, MIN_PERNET_OPS_ID, ops->id);
max_gen_ptrs = max(max_gen_ptrs, *ops->id + 1);
}
}
函数ida_get_new_above获取id值,max_gen_ptrs记录系统中最大的id值,以便在分配net_generic成员ptr空间时,分配合适的大小。max_gen_ptrs初始值为INITIAL_NET_GEN_PTRS(13),随着各个网络命名空间中的子模块注册而递增。
网络各个子模块可使用net_generic函数,指定参数net(网络命名空间)和id值,即可取到私有数据空间的首指针,进行必要的初始化工作已经进行特定命名空间的操作。
struct net_generic {
union {
void *ptr[0];
};
};
static inline void *net_generic(const struct net *net, unsigned int id)
{
struct net_generic *ng;
ng = rcu_dereference(net->gen);
ptr = ng->ptr[id];
}
例如对于ipgre_net_ops子模块,在调用register_pernet_device之后,网络命名空间系统将分配的id值保存到变量ipgre_net_id中,并且已分配好大小为sizeof(struct ip_tunnel_net)的内存空间,之后像ipgre_rcv等函数,可使用net_generic获取此空间。
static struct pernet_operations ipgre_net_ops = {
.init = ipgre_init_net,
.id = &ipgre_net_id,
.size = sizeof(struct ip_tunnel_net),
};
static int __init ipgre_init(void)
{
err = register_pernet_device(&ipgre_net_ops);
}
static int ipgre_rcv(struct sk_buff *skb, const struct tnl_ptk_info *tpi, int hdr_len)
{
struct net *net = dev_net(skb->dev);
struct ip_tunnel_net *itn = net_generic(net, ipgre_net_id);
}
命名空间添加设备
内核为每个新建的网络命名空间创建一个回环接口lo。使用ip命令添加设备到命名空间中,如将网络设备eth1添加到网络命名空间netns1中。对于回环接口、网桥设备、聚合设备bond和一些隧道设备,不能使用IP命名将其在命名空间之间移动,内核在设备的features中设置了NETIF_F_NETNS_LOCAL标志来标识这类设备,其仅属于创建时的命名空间。
ip link set eth1 netns netns1
内核中dev_change_net_namespace函数处理设备在命名空间中的移动。目的是更改网络设备net_device结构体中的成员nd_net指向新的网络命名空间,但是在更改前后需要做一些清理和初始化工作。 首先在更改之前关闭设备,清理kobject、流控队列,清空地址等,发送NETDEV_DOWN、NETDEV_GOING_DOWN、NETDEV_UNREGISTER等消息通知旧的命名空间的协议栈,如路由系统接收到NETDEV_DOWN消息将会清除此接口上的IP地址对应的路由信息。其次更改设备到新的命名空间,修改设备id,在新命名空间发送通知消息NETDEV_REGISTER等通知新命名空间的其它模块。
鉴于以上操作,移动设备之后,之前配置的IP地址等信息将会被清除。
内核版本
Linux-4.15