构建你自己的Zerotier

实现类似于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表
      MacVPort
      11:11:11:11:11:11VPort-1
      aa:aa:aa:aa:aa:aaVPort-a
    • 基于MAC表实现以太网帧交换服务
  • 客户端(VPort)模拟物理交换机端口的行为,负责在交换机和计算机之间中继以太网帧。

    • VPort有两端。
      • 一端通过TAP设备连接到计算机(linux内核)。
      • 另一端通过UDP插座连接到VSwitch。
    • 负责在计算机(linux内核)和VSwitch之间中继数据包。
      Computer's Linux Kernel <==[TAP]==> (Ethernet Frame) <==[UDP]==> VServer
      
  • 建筑图

        +----------------------------------------------+
        |                   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
    
    

源代码

  1. vswitch.pyVSwitch的代码

实现了一个简易的虚拟交换机(vSwitch),它使用UDP套接字来接收和转发以太网帧。

参数解析

首先,代码通过命令行参数获取虚拟交换机的服务端口。如果用户没有按照要求输入正确数量的参数(即虚拟交换机的端口),程序会提示正确的用法并退出。

创建UDP套接字

程序创建了一个UDP套接字并绑定到之前解析的服务端口上。这个套接字用于监听来自任何IP地址("0.0.0.0")的UDP数据包。

MAC 地址表

mac_table是一个字典,用于存储MAC地址到虚拟端口地址(vport_addr)的映射。这个表类似于现实世界交换机中的MAC地址表,用于记录哪个MAC地址最近从哪个端口看到,以便于后续正确转发帧。

主循环

程序进入一个无限循环,不断地从套接字中读取以太网帧,并进行处理:

  1. 解析以太网帧 - 程序提取以太网帧的目的地址和源地址。以太网帧的前14个字节包含了目标和源MAC地址(各6字节)以及帧类型/长度字段(2字节)。

  2. 更新MAC地址表 - 如果源MAC地址尚未出现在MAC地址表中,或者来源虚拟端口(vport_addr)发生变化,程序会更新该表。

  3. 转发以太网帧 - 如果目的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")
  1. vport.cVPort的代码

这段代码实现了一个虚拟端口(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),这允许并行处理上行和下行数据流。
  • 使用了assertfprintf进行错误检查和调试信息的打印。
  • 在实际部署和使用这段代码之前,需要确保相关的网络配置和权限设置已经完成,比如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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值