从源码上分析网络命名空间如何建立

network-namespace

环境

linux kernel-4.9.37

介绍

Network namespaces
网络命名空间可以理解为是完全独立丝毫没有相互影响的另一套协议栈。

疑问

Q1:网络命名空间之间为什么不能通讯?
A1:因为没有网卡设备。真实的网卡设备一般在 根命名空间使用。需要是由 veth(virtual Ethernet) 充当网络命名空间的网卡设备。

Q2:网络命名空间之间的网络通讯流程是什么样的?
A1:具体就流程就是 veth 的工作流程,会将数据包直接塞到接收侧的 rx 队列中。
参考1: veth跨命名空间通信

源码分析

环境

iprout2-5.9.0

命令分析

先从 networking namspace 的命令进行分析,进而了解整个 networking namespace 的构成。
ip netns list #列出网络命名空间
ip netns add NAME #添加网络命名空间

下面就是 netns 参数可以使用的全部操作,暂时重点只关注 list 和 add

int do_netns(int argc, char **argv)
{
	netns_nsid_socket_init();

	if (argc < 1) {
		netns_map_init();
		return netns_list(0, NULL);
	}

	if (!do_all && argc > 1 && invalid_name(argv[1])) {
		fprintf(stderr, "Invalid netns name \"%s\"\n", argv[1]);
		exit(-1);
	}

	if ((matches(*argv, "list") == 0) || (matches(*argv, "show") == 0) ||
	    (matches(*argv, "lst") == 0)) {
		netns_map_init();
		return netns_list(argc-1, argv+1);
	}

	if ((matches(*argv, "list-id") == 0)) {
		netns_map_init();
		return netns_list_id(argc-1, argv+1);
	}

	if (matches(*argv, "help") == 0)
		return usage();

	if (matches(*argv, "add") == 0)
		return netns_add(argc-1, argv+1, true);

	if (matches(*argv, "set") == 0)
		return netns_set(argc-1, argv+1);

	if (matches(*argv, "delete") == 0)
		return netns_delete(argc-1, argv+1);

	if (matches(*argv, "identify") == 0)
		return netns_identify(argc-1, argv+1);

	if (matches(*argv, "pids") == 0)
		return netns_pids(argc-1, argv+1);

	if (matches(*argv, "exec") == 0)
		return netns_exec(argc-1, argv+1);

	if (matches(*argv, "monitor") == 0)
		return netns_monitor(argc-1, argv+1);

	if (matches(*argv, "attach") == 0)
		return netns_add(argc-1, argv+1, false);

	fprintf(stderr, "Command \"%s\" is unknown, try \"ip netns help\".\n", *argv);
	exit(-1);
}

netns list 命令会在 NETNS_RUN_DIR(/var/run/netns) 目录中找到信息,并将这些信息通过 netns_map_add 函数组成 hash 结构,便于以后使用。
再之后会通过 netns_list 函数将信息打印。

void netns_map_init(void)
{
	static int initialized;
	struct dirent *entry;
	DIR *dir;
	int nsid;

	if (initialized || !ipnetns_have_nsid())
		return;

	dir = opendir(NETNS_RUN_DIR);
	if (!dir)
		return;

	while ((entry = readdir(dir)) != NULL) {
		if (strcmp(entry->d_name, ".") == 0)
			continue;
		if (strcmp(entry->d_name, "..") == 0)
			continue;
		nsid = get_netnsid_from_name(entry->d_name);

		if (nsid >= 0)
			netns_map_add(nsid, entry->d_name);
	}
	closedir(dir);
	initialized = 1;
}

netns add NAME 命令
这个函数创建了一个新的网络命名空间,然后挂载该命名空间到一个文件系统上众所周知的位置上。挂载名称使用该命名空间的名称。
下面函数的主要逻辑:

  1. 创建 /var/run/netns 文件夹
  2. 创建 /var/run/netns/name-space 文件
  3. 使用 unshare 将当前进程移至新的网络命名空房间,并且不共享信息。
  4. 打开 /proc/self/ns/net 文件,得到全局描述符 saved_netns
  5. 挂载 /proc/self/ns/net 到 /var/run/netns/name-space
  6. 使用 setns 函数移动当前进程到指定网络命名空间
static int netns_add(int argc, char **argv, bool create)
{
	/* This function creates a new network namespace and
	 * a new mount namespace and bind them into a well known
	 * location in the filesystem based on the name provided.
	 *
	 * If create is true, a new namespace will be created,
	 * otherwise an existing one will be attached to the file.
	 *
	 * The mount namespace is created so that any necessary
	 * userspace tweaks like remounting /sys, or bind mounting
	 * a new /etc/resolv.conf can be shared between users.
	 */
	char netns_path[PATH_MAX], proc_path[PATH_MAX];
	const char *name;
	pid_t pid;
	int fd;
	int made_netns_run_dir_mount = 0;

	if (create) {
		if (argc < 1) {
			fprintf(stderr, "No netns name specified\n");
			return -1;
		}
	} else {
		if (argc < 2) {
			fprintf(stderr, "No netns name and PID specified\n");
			return -1;
		}

		if (get_s32(&pid, argv[1], 0) || !pid) {
			fprintf(stderr, "Invalid PID: %s\n", argv[1]);
			return -1;
		}
	}
	name = argv[0];

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

	if (create_netns_dir())
		return -1;

	/* Make it possible for network namespace mounts to propagate between
	 * mount namespaces.  This makes it likely that a unmounting a network
	 * namespace file in one namespace will unmount the network namespace
	 * file in all namespaces allowing the network namespace to be freed
	 * sooner.
	 */
	while (mount("", NETNS_RUN_DIR, "none", MS_SHARED | MS_REC, NULL)) {
		/* Fail unless we need to make the mount point */
		if (errno != EINVAL || made_netns_run_dir_mount) {
			fprintf(stderr, "mount --make-shared %s failed: %s\n",
				NETNS_RUN_DIR, strerror(errno));
			return -1;
		}

		/* Upgrade NETNS_RUN_DIR to a mount point */
		if (mount(NETNS_RUN_DIR, NETNS_RUN_DIR, "none", MS_BIND | MS_REC, NULL)) {
			fprintf(stderr, "mount --bind %s %s failed: %s\n",
				NETNS_RUN_DIR, NETNS_RUN_DIR, strerror(errno));
			return -1;
		}
		made_netns_run_dir_mount = 1;
	}

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

	if (create) {
		netns_save();
		if (unshare(CLONE_NEWNET) < 0) {
			fprintf(stderr, "Failed to create a new network namespace \"%s\": %s\n",
				name, strerror(errno));
			goto out_delete;
		}

		strcpy(proc_path, "/proc/self/ns/net");
	} else {
		snprintf(proc_path, sizeof(proc_path), "/proc/%d/ns/net", pid);
	}

	/* Bind the netns last so I can watch for it */
	if (mount(proc_path, netns_path, "none", MS_BIND, NULL) < 0) {
		fprintf(stderr, "Bind %s -> %s failed: %s\n",
			proc_path, netns_path, strerror(errno));
		goto out_delete;
	}
	netns_restore();

	return 0;
out_delete:
	if (create) {
		netns_restore();
		netns_delete(argc, argv);
	} else if (unlink(netns_path) < 0) {
		fprintf(stderr, "Cannot remove namespace file \"%s\": %s\n",
			netns_path, strerror(errno));
	}
	return -1;
}

ip netns exec namespace ip a s 通过这个命令可以看到 namespace 命名空间中的网络地址情况。
-cmd_exec 函数会通过执行 do_switch 函数切换到命名空间,之后使用 execvp 命令。
在 do_switch 中几乎和添加命名空间类似,也是使用 unshare 函数和 setns 函数进行切换。

static int netns_exec(int argc, char **argv)
{
	/* Setup the proper environment for apps that are not netns
	 * aware, and execute a program in that environment.
	 */
	if (argc < 1 && !do_all) {
		fprintf(stderr, "No netns name specified\n");
		return -1;
	}
	if ((argc < 2 && !do_all) || (argc < 1 && do_all)) {
		fprintf(stderr, "No command specified\n");
		return -1;
	}

	if (do_all)
		return netns_foreach(on_netns_exec, argv);

	/* ip must return the status of the child,
	 * but do_cmd() will add a minus to this,
	 * so let's add another one here to cancel it.
	 */
	return -cmd_exec(argv[1], argv + 1, !!batch_mode, do_switch, argv[0]);    // 可以看出最关键的就是这个函数
}

int cmd_exec(const char *cmd, char **argv, bool do_fork,
	     int (*setup)(void *), void *arg)
{
	fflush(stdout);
	if (do_fork) {
		int status;
		pid_t pid;

		pid = fork();
		if (pid < 0) {
			perror("fork");
			exit(1);
		}

		if (pid != 0) {
			/* Parent  */
			if (waitpid(pid, &status, 0) < 0) {
				perror("waitpid");
				exit(1);
			}

			if (WIFEXITED(status)) {
				return WEXITSTATUS(status);
			}

			exit(1);
		}
	}

	if (setup && setup(arg))
		return -1;

	if (execvp(cmd, argv)  < 0)
		fprintf(stderr, "exec of \"%s\" failed: %s\n",
				cmd, strerror(errno));
	_exit(1);
}

static int do_switch(void *arg)
{
	char *netns = arg;

	/* we just changed namespaces. clear any vrf association
	 * with prior namespace before exec'ing command
	 */
	vrf_reset();

	return netns_switch(netns);
}

int netns_switch(char *name)
{
	char net_path[PATH_MAX];
	int netns;
	unsigned long mountflags = 0;
	struct statvfs fsstat;

	snprintf(net_path, sizeof(net_path), "%s/%s", NETNS_RUN_DIR, name);
	netns = open(net_path, 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);

	if (unshare(CLONE_NEWNS) < 0) {
		fprintf(stderr, "unshare failed: %s\n", strerror(errno));
		return -1;
	}
	/* Don't let any mounts propagate back to the parent */
	if (mount("", "/", "none", MS_SLAVE | MS_REC, NULL)) {
		fprintf(stderr, "\"mount --make-rslave /\" failed: %s\n",
			strerror(errno));
		return -1;
	}

	/* Mount a version of /sys that describes the network namespace */

	if (umount2("/sys", MNT_DETACH) < 0) {
		/* If this fails, perhaps there wasn't a sysfs instance mounted. Good. */
		if (statvfs("/sys", &fsstat) == 0) {
			/* We couldn't umount the sysfs, we'll attempt to overlay it.
			 * A read-only instance can't be shadowed with a read-write one. */
			if (fsstat.f_flag & ST_RDONLY)
				mountflags = MS_RDONLY;
		}
	}
	if (mount(name, "/sys", "sysfs", mountflags, NULL) < 0) {
		fprintf(stderr, "mount of /sys failed: %s\n",strerror(errno));
		return -1;
	}

	/* Setup bind mounts for config files in /etc */
	bind_etc(name);
	return 0;
}

setns 系统调用

内核中 setns 函数的具体作用。

SYSCALL_DEFINE2(setns, int, fd, int, nstype)
{
	struct task_struct *tsk = current;
	struct nsproxy *new_nsproxy;
	struct file *file;
	struct ns_common *ns;
	int err;

	file = proc_ns_fget(fd);        // 获得文件结构
	if (IS_ERR(file))
		return PTR_ERR(file);

	err = -EINVAL;
	ns = get_proc_ns(file_inode(file));    // 获得文件中的 ns_common 结构
	if (nstype && (ns->ops->type != nstype))
		goto out;

	new_nsproxy = create_new_namespaces(0, tsk, current_user_ns(), tsk->fs);    // 创建新的命名空间
	if (IS_ERR(new_nsproxy)) {
		err = PTR_ERR(new_nsproxy);
		goto out;
	}

	err = ns->ops->install(new_nsproxy, ns);    // 将新的命名空间与文件进行关联
	if (err) {
		free_nsproxy(new_nsproxy);
		goto out;
	}
	switch_task_namespaces(tsk, new_nsproxy);    // 将当前任务移动至新的命名空间
out:
	fput(file);
	return err;
}

涉及的函数

mount

#include <sys/mount.h>

int mount(const char *source, const char *target,
            const char *filesystemtype, unsigned long mountflags,
            const void *data);

unshare

#include <sched.h>

int unshare(int flags);

unshare() 允许进程(或线程)解除当前与其他进程(或线程)共享的执行上下文部分的关联。
其阐述基本和 setns 函数的第二个参数相同。重点看一下上面代码中使用的 CLONE_NEWNET 参数。
该参数会使得将当前进程移动到一个新的与之前进程不共享的命名空间中。

setns

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <sched.h>

int setns(int fd, int nstype);

给定一个引用命名空间的文件描述符,将其与调用进程重新关联。将当前进程加入一个已存在的网络命名空间。
参数 fd 是一个 /proc/[pid]/ns 的一个文件描述符。将根据 nstype 的不同,调用线程将重新关联响应的命名空间。

nstype可以是一下几种选项:

  1. 0 允许任何类型的命名空间加入
  2. CLONE_NEWIPC (since Linux 3.0) fd必须是 IPC 命名空间
  3. CLONE_NEWNS (since Linux 3.0) fd必须是网络命名空间

    更多查看 man netns 或者 Linux系统调用-- mount/umount函数详解

抓包分析

wireshark 过滤规则
netlink-route.ifla_ifname == "veth0"

参考

https://www.cnblogs.com/linhaifeng/p/6657119.html
https://blog.csdn.net/zhanglidn013/article/details/70241732
https://www.cnblogs.com/-xuan/p/10838052.html
https://blog.csdn.net/supahero/article/details/100606953
https://blog.csdn.net/guotianqing/article/details/82356096

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于内核源码分析,建议您先了解一些基础知识,例如操作系统的原理、进程管理、内存管理等。同时,了解 C 语言和汇编语言也是必要的。 对于 mount 源码分析,可以参考以下步骤: 1. 阅读 mount 的 man 手册,了解 mount 命令的作用以及参数。 2. 在内核源码中搜索 mount 相关的代码。可以使用 grep 命令,例如: ``` grep -r "do_mount" /path/to/kernel/source ``` 这里以 do_mount 函数为例,它是 mount 命令的核心函数。 3. 了解 do_mount 函数的实现原理。do_mount 函数主要是通过调用 vfs_mount 函数实现挂载的,而 vfs_mount 函数则调用了各个文件系统的 mount 函数来完成实际挂载工作。 4. 深入挖掘各个文件系统的 mount 函数实现,了解它们是如何完成挂载的。这里以 ext4 文件系统为例,可以在 ext4 文件系统源码中找到 mount 函数的实现。 5. 对于一些高级特性,例如 mount namespace,可以在相关的命名空间源码中查找实现。 对于 runc 源码分析,可以参考以下步骤: 1. 了解容器的基本概念和实现原理。例如容器的隔离、命名空间、控制组等。 2. 了解 runc 的作用和实现原理。runc 是一个轻量级容器运行时,它可以创建和管理容器,同时也支持容器的隔离和配置等功能。 3. 在 runc 源码中找到主要的函数和数据结构。例如,可以查找 runc 的 main 函数,了解它是如何解析命令行参数、创建容器和启动容器的。 4. 深入了解 runc 的关键实现。例如,了解 runc 是如何配合 Linux 内核的命名空间和控制组来实现容器的隔离和限制的。 5. 对于一些高级特性,例如容器的网络和存储,可以在相关的代码中查找实现。同时,也可以了解一些容器编排工具,例如 Docker 和 Kubernetes,它们是如何使用 runc 实现容器的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值