实现类似于Zerotier或虚拟交换机的L2 VPN。
它模拟物理交换机的行为,为连接到交换机端口的设备提供以太网帧交换服务。
区别在于,这个虚拟交换机的端口可以通过互联网连接到世界各地的设备,使它们看起来与操作系统位于同一局域网中。
原项目地址:
https://github.com/peiyuanix/build-your-own-zerotier?tab=readme-ov-file
什么是网络交换机?
网络交换机是网络硬件,通过使用分组交换来接收数据并将其转发到目标设备,从而连接计算机网络上的设备。
网络交换机是一个多端口网络桥接器,它使用MAC地址在OSI模型的数据链路层(第2层)转发数据。一些交换机还可以通过额外合并路由功能在网络层(第3层)转发数据。此类开关通常被称为第三层交换机或多层交换机。
在这个项目中,我们重点放在第2层交换机上,这些交换机可以识别和转发以太网帧。转发数据包时,交换机使用转发表来查找与目标地址对应的端口。
在交换机中,转发表通常称为MAC地址表。此表维护本地网络中已知的MAC地址及其相应的端口。当交换机收到以太网帧时,它会在转发表中查找与目标MAC地址对应的端口,并仅将数据帧转发到该端口,从而在局域网内实现数据转发。如果目标MAC地址不在表中,交换机会将以太网帧转发到除源端口以外的所有端口,以便目标设备可以响应并更新转发表。
在这个项目中,我们将编写一个名为VSwitch的程序作为虚拟交换机,以实现以太网帧交换功能。
虚拟网络设备:TAP
TAP是一种虚拟网络设备,可以模拟物理网络接口,允许操作系统和应用程序将其作为物理接口使用。TAP设备通常用于在不同计算机之间创建虚拟专用网络(VPN)连接,以便通过公共网络进行安全的数据传输。
TAP设备在操作系统内核中实现。它看起来像一个常规的网络接口,可用于常规物理网卡等应用程序。当数据包通过TAP设备发送时,它们被传递给内核中的TUN/TAP驱动程序,该驱动程序将数据包传递给应用程序。该应用程序可以处理数据包并将其传递给其他设备或网络。同样,当应用程序发送数据包时,它们被传递给TUN/TAP驱动程序,该驱动程序将它们转发到指定的目标设备或网络。
在这个项目中,TAP设备用于连接客户端计算机和虚拟交换机,在客户端计算机和虚拟交换机之间实现数据包转发。
在这个项目中,TAP设备用于连接客户端计算机和虚拟交换机,在客户端计算机和虚拟交换机之间实现数据包转发。
系统架构
-
由一个服务器(VSwitch)和几个客户端(VPorts)组成
-
服务器(VSwitch)模拟物理网络交换机的行为,为通过VPorts连接到VSwitch的每台计算机提供以太网帧交换服务。
- 维护一个MAC表
Mac VPort 11:11:11:11:11:11 VPort-1 aa:aa:aa:aa:aa:aa VPort-a - 基于MAC表实现以太网帧交换服务
- 维护一个MAC表
-
客户端(VPort)模拟物理交换机端口的行为,负责在交换机和计算机之间中继以太网帧。
- VPort有两端。
- 一端通过TAP设备连接到计算机(linux内核)。
- 另一端通过UDP插座连接到VSwitch。
- 负责在计算机(linux内核)和VSwitch之间中继数据包。
Computer's Linux Kernel <==[TAP]==> (Ethernet Frame) <==[UDP]==> VServer
- VPort有两端。
-
建筑图
+----------------------------------------------+ | VSwitch | | | | +----------------+---------------+ | | | MAC Table | | | |--------------------------------+ | | | MAC | VPort | | | |--------------------------------+ | | | 11:11:11:11:11 | VClient-1 | | | |--------------------------------+ | | | aa:aa:aa:aa:aa | VClient-a | | | +----------------+---------------+ | | | | ^ ^ | +-----------|-----------------------|----------+ +-------|--------+ +--------|-------+ | v | | v | | +------------+ | | +------------+ | | | UDP Socket | | | | UDP Socket | | | +------------+ | | +------------+ | | ^ | | ^ | | | | | | | |(Ethernet Frame)| |(Ethernet Frame)| | | | | | | VPort | v | | v | VPort | +------------+ | | +------------+ | | | TAP Device | | | | TAP Device | | | +------------+ | | +------------+ | | ^ | | ^ | +-------|--------+ +--------|-------+ v v ------------------------- ------------------------- Computer A's Linux Kernel Computer B's Linux Kernel
源代码
- vswitch.py:VSwitch的代码
实现了一个简易的虚拟交换机(vSwitch),它使用UDP套接字来接收和转发以太网帧。
参数解析
首先,代码通过命令行参数获取虚拟交换机的服务端口。如果用户没有按照要求输入正确数量的参数(即虚拟交换机的端口),程序会提示正确的用法并退出。
创建UDP套接字
程序创建了一个UDP套接字并绑定到之前解析的服务端口上。这个套接字用于监听来自任何IP地址("0.0.0.0")的UDP数据包。
MAC 地址表
mac_table
是一个字典,用于存储MAC地址到虚拟端口地址(vport_addr)的映射。这个表类似于现实世界交换机中的MAC地址表,用于记录哪个MAC地址最近从哪个端口看到,以便于后续正确转发帧。
主循环
程序进入一个无限循环,不断地从套接字中读取以太网帧,并进行处理:
-
解析以太网帧 - 程序提取以太网帧的目的地址和源地址。以太网帧的前14个字节包含了目标和源MAC地址(各6字节)以及帧类型/长度字段(2字节)。
-
更新MAC地址表 - 如果源MAC地址尚未出现在MAC地址表中,或者来源虚拟端口(vport_addr)发生变化,程序会更新该表。
-
转发以太网帧 - 如果目的MAC地址在MAC地址表中找到,帧将被转发到对应的虚拟端口。如果目的MAC地址是广播地址("ff:ff:ff:ff:ff:ff"),帧将被广播到除源虚拟端口之外的所有已知虚拟端口。否则,对于那些在MAC地址表中找不到的目的地址,以太网帧将被丢弃,以简化处理流程。
import socket
import sys
# parse parameters
server_port = None
if len(sys.argv) != 2:
print("Usage: python3 vswitch.py {VSWITCH_PORT}")
sys.exit(1)
else:
server_port = int(sys.argv[1])
server_addr = ("0.0.0.0", server_port)
# 0. create UDP socket, bind to service port
vserver_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
vserver_sock.bind(server_addr)
print(f"[VSwitch] Started at {server_addr[0]}:{server_addr[1]}")
mac_table = {}
while True:
# 1. read ethernet frame from VPort
data, vport_addr = vserver_sock.recvfrom(1518)
# 2. parse ethernet frame
eth_header = data[:14]
# ethernet destination hardware address (MAC)
eth_dst = ":".join("{:02x}".format(x) for x in eth_header[0:6])
# ethernet source hardware address (MAC)
eth_src = ":".join("{:02x}".format(x) for x in eth_header[6:12])
print(f"[VSwitch] vport_addr<{vport_addr}> "
f"src<{eth_src}> dst<{eth_dst}> datasz<{len(data)}>")
# 3. insert/update mac table
if (eth_src not in mac_table or mac_table[eth_src] != vport_addr):
mac_table[eth_src] = vport_addr
print(f" ARP Cache: {mac_table}")
# 4. forward ethernet frame
# if dest in mac table, forward ethernet frame to it
if eth_dst in mac_table:
vserver_sock.sendto(data, mac_table[eth_dst])
print(f" Forwarded to: {eth_dst}")
# if dest is broadcast address,
# broadcast ethernet frame to every known VPort except source VPort
elif eth_dst == "ff:ff:ff:ff:ff:ff":
brd_dst_macs = list(mac_table.keys())
brd_dst_macs.remove(eth_src)
brd_dst_vports = {mac_table[mac] for mac in brd_dst_macs}
print(f" Broadcasted to: {brd_dst_vports}")
for brd_dst in brd_dst_vports:
vserver_sock.sendto(data, brd_dst)
# otherwise, for simplicity, discard the ethernet frame
else:
print(f" Discarded")
- vport.c:VPort的代码
这段代码实现了一个虚拟端口(VPort)功能,它充当虚拟交换机(VSwitch)和TAP设备之间的桥梁,以转发以太网帧。这种机制典型地应用于虚拟化环境,使得虚拟机(VM)能够与物理网络进行互动。下面是对代码主要组成部分的详细解释:
结构体 struct vport_t
tapfd
: 与TAP设备关联的文件描述符。TAP设备是一种虚拟网络接口,模拟为一个网络设备,工作在数据链路层,可以接收和发送以太网帧。vport_sockfd
: 客户端套接字文件描述符,用于与VSwitch通信。vswitch_addr
: VSwitch的地址结构体,包含了VSwitch的IP地址和端口号。
函数 vport_init
这个函数初始化一个vport_t
实例。它创建了一个TAP设备,并为与VSwitch的通信打开一个客户端套接字。然后,它将VSwitch的地址信息填充到结构体中,为后续的通信做准备。
函数 forward_ether_data_to_vswitch
这个函数用于将数据从TAP设备转发到VSwitch。它不断从TAP设备读取以太网帧,然后通过之前创建的套接字将这些帧发送到VSwitch。每次转发时,都会打印出所转发帧的相关信息,包括目的地址、源地址和帧类型。
函数 forward_ether_data_to_tap
这个函数与forward_ether_data_to_vswitch
相反,它负责将数据从VSwitch转发到TAP设备。通过VSwitch的套接字接收以太网帧,然后将这些帧写入TAP设备。同样,在转发时会打印出帧的详细信息。
主函数 main
main
函数首先解析命令行参数,获得VSwitch的IP地址和端口号,然后调用vport_init
初始化一个虚拟端口。接下来,创建两个线程:一个用于处理从TAP到VSwitch的数据转发(上行),另一个用于处理从VSwitch到TAP的数据转发(下行)。最后,它等待这两个线程完成工作。
其他注意点
- 代码使用了POSIX线程库(pthread),这允许并行处理上行和下行数据流。
- 使用了
assert
和fprintf
进行错误检查和调试信息的打印。 - 在实际部署和使用这段代码之前,需要确保相关的网络配置和权限设置已经完成,比如TAP设备的创建和配置、网络地址的分配等。
struct vport_t
{
int tapfd; // TAP device file descriptor, connected to linux kernel network stack
int vport_sockfd; // client socket, for communicating with VSwitch
struct sockaddr_in vswitch_addr; // VSwitch address
};
void vport_init(struct vport_t *vport, const char *server_ip_str, int server_port);
void *forward_ether_data_to_vswitch(void *raw_vport);
void *forward_ether_data_to_tap(void *raw_vport);
int main(int argc, char const *argv[])
{
// parse arguments
if (argc != 3)
{
ERROR_PRINT_THEN_EXIT("Usage: vport {server_ip} {server_port}\n");
}
const char *server_ip_str = argv[1];
int server_port = atoi(argv[2]);
// vport init
struct vport_t vport;
vport_init(&vport, server_ip_str, server_port);
// up forwarder
pthread_t up_forwarder;
if (pthread_create(&up_forwarder, NULL, forward_ether_data_to_vswitch, &vport) != 0)
{
ERROR_PRINT_THEN_EXIT("fail to pthread_create: %s\n", strerror(errno));
}
// down forwarder
pthread_t down_forwarder;
if (pthread_create(&down_forwarder, NULL, forward_ether_data_to_tap, &vport) != 0)
{
ERROR_PRINT_THEN_EXIT("fail to pthread_create: %s\n", strerror(errno));
}
// wait for up forwarder & down forwarder
if (pthread_join(up_forwarder, NULL) != 0 || pthread_join(down_forwarder, NULL) != 0)
{
ERROR_PRINT_THEN_EXIT("fail to pthread_join: %s\n", strerror(errno));
}
return 0;
}
/**
* init VPort instance: create TAP device and client socket
*/
void vport_init(struct vport_t *vport, const char *server_ip_str, int server_port)
{
// alloc tap device
char ifname[IFNAMSIZ] = "tapyuan";
int tapfd = tap_alloc(ifname);
if (tapfd < 0)
{
ERROR_PRINT_THEN_EXIT("fail to tap_alloc: %s\n", strerror(errno));
}
// create socket
int vport_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (vport_sockfd < 0)
{
ERROR_PRINT_THEN_EXIT("fail to socket: %s\n", strerror(errno));
}
// setup vswitch info
struct sockaddr_in vswitch_addr;
memset(&vswitch_addr, 0, sizeof(vswitch_addr));
vswitch_addr.sin_family = AF_INET;
vswitch_addr.sin_port = htons(server_port);
if (inet_pton(AF_INET, server_ip_str, &vswitch_addr.sin_addr) != 1)
{
ERROR_PRINT_THEN_EXIT("fail to inet_pton: %s\n", strerror(errno));
}
vport->tapfd = tapfd;
vport->vport_sockfd = vport_sockfd;
vport->vswitch_addr = vswitch_addr;
printf("[VPort] TAP device name: %s, VSwitch: %s:%d\n", ifname, server_ip_str, server_port);
}
/**
* Forward ethernet frame from TAP device to VSwitch
*/
void *forward_ether_data_to_vswitch(void *raw_vport)
{
struct vport_t *vport = (struct vport_t *)raw_vport;
char ether_data[ETHER_MAX_LEN];
while (true)
{
// read ethernet from tap device
int ether_datasz = read(vport->tapfd, ether_data, sizeof(ether_data));
if (ether_datasz > 0)
{
assert(ether_datasz >= 14);
const struct ether_header *hdr = (const struct ether_header *)ether_data;
// forward ethernet frame to VSwitch
ssize_t sendsz = sendto(vport->vport_sockfd, ether_data, ether_datasz, 0, (struct sockaddr *)&vport->vswitch_addr, sizeof(vport->vswitch_addr));
if (sendsz != ether_datasz)
{
fprintf(stderr, "sendto size mismatch: ether_datasz=%d, sendsz=%d\n", ether_datasz, sendsz);
}
printf("[VPort] Sent to VSwitch:"
" dhost<%02x:%02x:%02x:%02x:%02x:%02x>"
" shost<%02x:%02x:%02x:%02x:%02x:%02x>"
" type<%04x>"
" datasz=<%d>\n",
hdr->ether_dhost[0], hdr->ether_dhost[1], hdr->ether_dhost[2], hdr->ether_dhost[3], hdr->ether_dhost[4], hdr->ether_dhost[5],
hdr->ether_shost[0], hdr->ether_shost[1], hdr->ether_shost[2], hdr->ether_shost[3], hdr->ether_shost[4], hdr->ether_shost[5],
ntohs(hdr->ether_type),
ether_datasz);
}
}
}
/**
* Forward ethernet frame from VSwitch to TAP device
*/
void *forward_ether_data_to_tap(void *raw_vport)
{
struct vport_t *vport = (struct vport_t *)raw_vport;
char ether_data[ETHER_MAX_LEN];
while (true)
{
// read ethernet frame from VSwitch
socklen_t vswitch_addr = sizeof(vport->vswitch_addr);
int ether_datasz = recvfrom(vport->vport_sockfd, ether_data, sizeof(ether_data), 0,
(struct sockaddr *)&vport->vswitch_addr, &vswitch_addr);
if (ether_datasz > 0)
{
assert(ether_datasz >= 14);
const struct ether_header *hdr = (const struct ether_header *)ether_data;
// forward ethernet frame to TAP device (Linux network stack)
ssize_t sendsz = write(vport->tapfd, ether_data, ether_datasz);
if (sendsz != ether_datasz)
{
fprintf(stderr, "sendto size mismatch: ether_datasz=%d, sendsz=%d\n", ether_datasz, sendsz);
}
printf("[VPort] Forward to TAP device:"
" dhost<%02x:%02x:%02x:%02x:%02x:%02x>"
" shost<%02x:%02x:%02x:%02x:%02x:%02x>"
" type<%04x>"
" datasz=<%d>\n",
hdr->ether_dhost[0], hdr->ether_dhost[1], hdr->ether_dhost[2], hdr->ether_dhost[3], hdr->ether_dhost[4], hdr->ether_dhost[5],
hdr->ether_shost[0], hdr->ether_shost[1], hdr->ether_shost[2], hdr->ether_shost[3], hdr->ether_shost[4], hdr->ether_shost[5],
ntohs(hdr->ether_type),
ether_datasz);
}
}
}
如何构建
跑吧
make
如何部署
环境准备
- 具有公共IP的服务器,用于运行VSwitch
- 至少两个客户端,运行VPort连接到VSwitch,构建虚拟专用网络
- 假设公共IP是
SERVER_IP
,服务器端口是SERVER_PORT
第1步。运行VSwitch
在具有公共IP的服务器上:
python3 vswitch.py ${SERVER_PORT}
第2步。在客户端-1上运行和配置VPort
- 运行VPort
sudo ./vport ${SERVER_IP} ${SERVER_PORT}
- 配置TAP设备
sudo ip addr add 10.1.1.101/24 dev tapyuan sudo ip link set tapyuan up
第3步。在客户端-2上运行和配置VPort
- 运行VPort
sudo ./vport ${SERVER_IP} ${SERVER_PORT}
- 配置TAP设备
sudo ip addr add 10.1.1.102/24 dev tapyuan sudo ip link set tapyuan up
第4步。Ping连接测试
- 来自客户端-1的Ping客户端-2
ping 10.1.1.102
- 来自客户端-2的Ping客户端-1
ping 10.1.1.101