写点什么

Linux 虚拟网络(一)

用户头像
KernelNewbies
关注
发布于: 2021 年 04 月 10 日
Linux 虚拟网络(一)

1.Network Namespace

Network Namespace 提供了对网络资源的隔离,每一个 Network Namespace 都拥有独立的网络栈、网络设备、IP 地址和端口号、IP 路由表、防火墙规则、/proc/net 目录。


创建 Network Namespace 需要用到 ip 命令,创建命令如下:


phl@kernelnewbies:~$ sudo ip netns add ns1
复制代码


ns1 为新的 Network Namespace 的名字。通过 ip 命令也可以查看新建的 Network Namespace,命令及结果如下:


phl@kernelnewbies:~$ ip netns showns1
复制代码


Network Namespace 已经创建了,现在应该看看其中的网络设备了,在某个 Network Namespace 里面执行命令的格式如下:


ip netns exec netns-name command
复制代码


查看一下新 Network Namespace 里面的网络设备,命令及结果如下:


phl@kernelnewbies:~$ sudo ip netns exec ns1 ip link show1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
复制代码


从结果可以看出,每个新的 Network Namespace 里面有一个本地回环设备,该设备默认是关闭的。


如果我们将 command 指定为 Shell,那么我么将得到一个默认使用该 Network Namespace 的 Shell,在该 Shell 里面执行的网络命令只作用于所属的 Network Namespace。命令及结果如下:


phl@kernelnewbies:~$ sudo ip netns exec ns1 /bin/bashroot@kernelnewbies:~# ip link show1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
复制代码

2.VETH

VETH 是一对网络设备,一个设备收到的数据会发送到另一个设备上去,VETH 设备通常用于容器之间的通信。


下面创建两个 Network Namespace,然后创建一对 VETH 设备,然后将两个 VETH 设备分别加入 ns1 和 ns2,通过 VETH 实现两个 Network Namespace 之间的通信,示意图如下:


+---------+      +---------+|   ns1   |      |   ns2   ||         |      |         || +-----+ |      | +-----+ || |veth1| |      | |veth2| || +-----+ |      | +-----+ ||    ^    |      |    ^    |+----|----+      +----|----+     |                |     +----------------+
复制代码


首先,创建两个 Network Namespace,命令如下:


phl@kernelnewbies:~$ sudo ip netns add ns1phl@kernelnewbies:~$ sudo ip netns add ns2
复制代码


然后,创建一对 VETH 设备,命令如下:


phl@kernelnewbies:~$ sudo ip link add veth1 type veth peer name veth2
复制代码


该命令创建了一对 VETH 设备,一个名为 veth1,另一个名为 veth2。查看这两个设备,命令及结果如下:


phl@kernelnewbies:~$ ip link show...4: veth2@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether c6:88:07:eb:05:1e brd ff:ff:ff:ff:ff:ff5: veth1@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 3a:80:2d:c9:96:c9 brd ff:ff:ff:ff:ff:ff
复制代码


然后,将 veth1 加入 ns1,veth2 加入 ns2,命令如下:


phl@kernelnewbies:~$ sudo ip link set veth1 netns ns1phl@kernelnewbies:~$ sudo ip link set veth2 netns ns2
复制代码


该命令用于将设备加入某个 Network Namespace。查看一下全局 Network Namespace、ns1、ns2 中的网络设备,命令及结果如下:


phl@kernelnewbies:~$ ip link show...
phl@kernelnewbies:~$ sudo ip netns exec ns1 ip link show1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:005: veth1@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000link/ether 3a:80:2d:c9:96:c9 brd ff:ff:ff:ff:ff:ff link-netnsid 1
phl@kernelnewbies:~$ sudo ip netns exec ns2 ip link show1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:004: veth2@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether c6:88:07:eb:05:1e brd ff:ff:ff:ff:ff:ff link-netnsid 0
复制代码


现在,启动两个终端,分别运行以下命令以在 ns1 和 ns2 下运行 Shell,命令及结果如下:


phl@kernelnewbies:~$ sudo ip netns exec ns1 /bin/bashroot@kernelnewbies:~# 
phl@kernelnewbies:~$ sudo ip netns exec ns2 /bin/bashroot@kernelnewbies:~#
复制代码


在第一个终端中设置 veth1 的地址,并启动该设备,然后启动一个 TCP 服务器,命令如下:


root@kernelnewbies:~# ip address add 192.168.5.101/24 dev veth1root@kernelnewbies:~# ip link set dev veth1 uproot@kernelnewbies:~# netcat -l 192.168.5.101 10000
复制代码


在第二个终端中设置 veth2 的地址,并启动该设备,然后启动一个 TCP 客户端,并连接到刚启动的 TCP 服务器,命令及结果如下:


root@kernelnewbies:~# ip address add 192.168.5.102/24 dev veth2root@kernelnewbies:~# ip link set dev veth2 uproot@kernelnewbies:~# netcat 192.168.5.101 10000
复制代码


在一个终端输入内容,另一个终端皆可收到。这样就利用 VETH 设备实现了两个容器之间的通信。

3.Bridge

Bridge 是工作在二层的虚拟网络设备,功能类似于物理交换机。


下面创建三个 Network Namespace,然后创建三对 VETH 设备,VETH 设备的一端在 Network Namespace 中,另一端连接到 Bridge 上。通过 Bridge 实现任意两个容器之间的通信,示意图如下:


+-----------+      +-----------+      +-----------+|   ns1     |      |   ns2     |      |   ns3     ||           |      |           |      |           || +-------+ |      | +-------+ |      | +-------+ || |veth1-1| |      | |veth2-1| |      | |veth3-1| || +-------+ |      | +-------+ |      | +-------+ ||     ^     |      |     ^     |      |     ^     |+-----|-----+      +-----|-----+      +-----|-----+      |                  |                  |+-----|------------------|------------------|-----+|     v                  v                  v     || +-------+          +-------+          +-------+ || |veth1-2|          |veth2-2|          |veth3-2| || +-------+          +-------+          +-------+ ||     ^                  ^                  ^     ||     |                  |                  |     ||     +------------------+------------------+     |+-------------------------------------------------+
复制代码


首先,创建并启动 brctl 设备,命令如下:


phl@kernelnewbies$ sudo ip link add name br1 type bridgephl@kernelnewbies$ sudo ip link set br1 up
复制代码


然后,创建 3 个 Network Namespace,命令如下:


phl@kernelnewbies$ sudo ip netns add ns1phl@kernelnewbies$ sudo ip netns add ns2phl@kernelnewbies$ sudo ip netns add ns3
复制代码


然后,创建 3 对 VETH 设备,命令如下:


phl@kernelnewbies$ sudo ip link add veth1-1 type veth peer name veth1-2phl@kernelnewbies$ sudo ip link add veth2-1 type veth peer name veth2-2phl@kernelnewbies$ sudo ip link add veth3-1 type veth peer name veth3-2
复制代码


然后,将名为 vethN-1 的设备分别放入对应的 Network Namespace nsN 中,命令如下:


phl@kernelnewbies$ sudo ip link set veth1-1 netns ns1phl@kernelnewbies$ sudo ip link set veth2-1 netns ns2phl@kernelnewbies$ sudo ip link set veth3-1 netns ns3
复制代码


然后,启动名为 vethN-2 的设备,并将其连接到 br1 上,命令如下:


phl@kernelnewbies$ sudo ip link set dev veth1-2 master br1phl@kernelnewbies$ sudo ip link set dev veth2-2 master br1phl@kernelnewbies$ sudo ip link set dev veth3-2 master br1ph@kernelnewbies$ sudo ip link set dev veth1-2 upphl@kernelnewbies$ sudo ip link set dev veth2-2 upphl@kernelnewbies$ sudo ip link set dev veth3-2 up
复制代码


现在,启动三个终端,分别运行以下命令以在 ns1、ns2、ns3 下运行 Shell,命令及结果如下:


phl@kernelnewbies:~$ sudo ip netns exec ns1 /bin/bashroot@kernelnewbies:~# 
phl@kernelnewbies:~$ sudo ip netns exec ns2 /bin/bashroot@kernelnewbies:~#
phl@kernelnewbies:~$ sudo ip netns exec ns3 /bin/bashroot@kernelnewbies:~#
复制代码


在第一个终端中设置 veth1 的地址,并启动该设备,命令如下:


root@kernelnewbies:~# ip address add 192.168.5.101/24 dev veth1-1root@kernelnewbies:~# ip link set dev veth1-1 up
复制代码


在第二个终端中设置 veth2 的地址,并启动该设备,命令如下:


root@kernelnewbies:~# ip address add 192.168.5.102/24 dev veth2-1root@kernelnewbies:~# ip link set dev veth2-1 up
复制代码


在第三个终端中设置 veth3 的地址,并启动该设备,命令如下:


root@kernelnewbies:~# ip address add 192.168.5.103/24 dev veth3-1root@kernelnewbies:~# ip link set dev veth3-1 up
复制代码


现在,三个容器就可以互相 ping 通了。如果无法 ping 通,请在 Host 上执行以下指令,然后再试。


phl@kernelnewbies$ sudo sh -c "echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptables"
复制代码

4.TUN/TAP 设备

TUN/TAP 是 Linux 内核实现的一对虚拟网络设备,TAP 工作在二层,TUN 工作在三层,Linux 内核通过 TAP/TUN 设备向绑定该设备的用户空间应用发送数据;反之,用户空间应用也可以像操作硬件网络设备那样,通过 TUN/TAP 设备发送数据。


现在以 TUN 设备为例进行说明,一个 TUN 设备会暴露两个接口:一个网络设备和一个文件。下图中的 TUN 设备向用户暴露了 tun0 设备和一个文件 tun0_file,网络应用通过网络栈向 tun0 发送的数据会写入 tun0_file;应用程序写入 tun0_file 的数据会被发送给 tun0 设备。


+---------+        +---------+| web app |        |   app   |+---------+        +---------+     ^                  ^     |                  |     v                  v  +------+        +-----------+  | tun0 |        | tun0_file |  +------+        +-----------+     ^                  ^     |                  |     +------------------+
复制代码


通常,打开一个文件需要类似 open(“sample.txt”)的调用,调用者需为 open 函数提供一个文件路径。TUNTAP 设备有些不同,Linux 内核提供了/dev/net/tun 设备用于对 TUNTAP 设备进行操作。


通过打开/dev/net/tun 设备,并对其调用 ioctl,可以创建 TUNTAP 设备:包括一个网络设备和一个文件。通过 ip link 命令可看到网络设备,但文件以文件描述符的形式返回,没有出现在文件系统中,当然也就没有文件名,也就是说只有创建 TUNTAP 设备的程序才可以使用这个文件。


下面的代码演示了如何创建一个 TUNTAP 设备,并返回相应的文件描述符。代码如下:


static int tun_open_common(char *dev, int istun){        struct ifreq ifr;        int fd;
if ((fd = open("/dev/net/tun", O_RDWR)) < 0) { perror("open /dev/net/tun error"); return fd; }
memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = (istun ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
if (*dev) { strncpy(ifr.ifr_name, dev, IFNAMSIZ); }
/* 创建 TUNTAP 设备 */ if (ioctl(fd, TUNSETIFF, (void *) &ifr) < 0) { perror("ioctl TUNSETIFF error"); goto failed; }
strcpy(dev, ifr.ifr_name); return fd;
failed: close(fd); return -1;}
复制代码


通过 ioctl 调用还可以调整 TUNTAP 设备的一些属性,例如:持久化 TUNTAP 设备(默认情况下,程序退出后 TUNTAP 设备也就消失了)、修改 TUNTAP 设备权限、删除 TUNTAP 设备等。


下面的示例将创建一个 TUN 设备,然后利用 netcat 向该设备发送并接收返回的 UDP 数据包。


TUN 工作在三层,因此,示例程序读文件收到的是一个 IP 包,包的开头是 20 字节的 IP 头,紧接着是 8 字节的 UDP 头,然后才是用户发送的数据。整个包中 12~15 字节是发送方 IP 地址,16~19 字节是接收方 IP 地址,20~21 字节是发送方端口号(大端),22~23 字节是接收方端口号(大端),28 字节~结尾是用户数据。示意图如下:


0    12      16         20        22           28|    |       |          |         |            |v    v       v          v         v            v+----+-------+-----+----+---------+-------+----+----+|****|ip_from|ip_to|****|port_from|port_to|****|data|+----+-------+-----+----+---------+-------+----+----+^                       ^                      ^    ^|                       |                      |    |+--20 bytes ip header---+--8 bytes udp header--+data+
复制代码


示例程序创建了一个名为 tun0 的 TUN 设备,并读取相应文件。然后从报文中解析出发送方与接受方的 IP 地址和端口号、用户数据。然后对调发送方和接收方的 IP 地址与端口号,并写回该文件。示例程序相当于一个 UDP 版本的 Echo 服务器。


int main(int argc, char **argv){        int fd_tun;        char name_dev[16] = "tun0";
int count_read; char buf[2048]; char tmp[4];
unsigned short port; unsigned char *pport;
pport = (unsigned char *) &port; if ((fd_tun = tun_open_common(name_dev, 1)) < 0) { return fd_tun; } printf("%s\n\n", name_dev);
while (1) { if ((count_read = read(fd_tun, buf, sizeof buf)) < 0) { printf("count_read < 0"); break; }
printf("READ: %d bytes\n", count_read);
/* 发送方 IP */ for (int i = 12; i < 16; i++) { if (15 == i) { printf("%d:", (unsigned char) buf[i]); } else { printf("%d.", (unsigned char) buf[i]); } }
/* 发送方端口号,注意大小端 */ pport[0] = buf[21]; pport[1] = buf[20]; printf("%d", port);
printf(" -> ");
/* 接收方 IP */ for (int i = 16; i < 20; i++) { if (19 == i) { printf("%d:", (unsigned char) buf[i]); } else { printf("%d.", (unsigned char) buf[i]); } }
/* 接收方端口号,注意大小端 */ pport[0] = buf[23]; pport[1] = buf[22]; printf("%d", port);
/* 用户数据 */ buf[count_read] = '\x00'; printf(" %s\n", buf + 28);
/* 对调 IP */ memcpy(tmp, buf + 12, 4); memcpy(buf + 12, buf + 16, 4); memcpy(buf + 16, tmp, 4);
/* 对调端口号 */ memcpy(tmp, buf + 20, 2); memcpy(buf + 20, buf + 22, 2); memcpy(buf + 22, tmp, 2);
write(fd_tun, buf, count_read); }}
复制代码


完整程序仓库地址如下:


https://github.com/pandengyang/linux_network.git
复制代码


编译并运行该程序,命令及结果如下:


phl@kernelnewbies:~$ sudo ./tundemotun0
复制代码


tun0 是网络设备的名字,查看这个设备,命令及结果如下:


phl@kernelnewbies:~$ ip link show4: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 500    link/none
复制代码


将 tun0 设备加入到一个新的 Network Namespace 中,并设置其地址,然后启动该设备。命令及结果如下:


phl@kernelnewbies:~$ sudo ip netns add ns1phl@kernelnewbies:~$ sudo ip link set dev tun0 netns ns1phl@kernelnewbies:~$ sudo ip netns exec ns1 ip address add 192.168.5.101/24 dev tun0phl@kernelnewbies:~$ sudo ip netns exec ns1 ip link set dev tun0 up
复制代码


在新的 Network Namespace 中添加一条默认路由,这样发往非 192.168.5.0/24 网段的数据也会经 tun0 发出。命令如下:


phl@kernelnewbies:~$ sudo ip netns exec ns1 ip route add default via 192.168.5.101
复制代码


现在,启动一个新终端,运行以下命令以在 ns1 下运行 Shell,命令及结果如下:


phl@kernelnewbies:~$ sudo ip netns exec ns1 /bin/bashroot@kernelnewbies:~# 
复制代码


在新终端中启动一个 UDP 客户端,并向一个假装存在的 UDP 服务器发送字符串。命令及结果如下:


root@kernelnewbies:~# netcat -u 192.168.8.123 10000hellohello
复制代码


第一个 hello 为用户输入,第二个 hello 为收到的响应。运行 tuntdemo 的终端输出如下:


READ: 34 bytes192.168.5.101:49666 -> 192.168.8.123:10000 hello
复制代码


发布于: 2021 年 04 月 10 日阅读数: 41
用户头像

KernelNewbies

关注

苔花如米小,也学牡丹开。 2019.03.04 加入

专注创作有助于理解CPU体系结构、虚拟化的文章。

评论

发布
暂无评论
Linux 虚拟网络(一)