使用虚拟机作为路由器

​ 目前,OpenWrt等智能路由器固件已经越来越流行,但大多数路由器性能有限,不能支持大型应用。此外,一般的路由器固件对Linux进行了精简,有时我们希望使用完整的Linux系统作为路由器。很多用户往往不想专门购买一台设备安装Linux,因为大多数厂商的虚拟机提供了桥接模式的网卡,理论上可以作路由器使用。但实际上,虚拟机的网卡并不是物理网卡,因此在某些环境可能会出现问题。

虚拟机不能桥接无线网卡做路由器

​ 一般认为,网桥工作在链路层,不识别IP地址,但无线网桥通常不是这样。我在VMware虚拟机安装了Linux,配置了桥接网卡,桥接到无线网卡。平时,虚拟机连接到互联网或者主机都没有问题,但如果把主机的网关设置为虚拟机IP,主机不能连接到互联网。在主机抓包发现,有一个目标IP不为虚拟机IP的包,其目标MAC地址为虚拟机。但在虚拟机使用tcpdump却抓不到这个包。而一旦把虚拟机的网卡桥接到有线网络,则一切正常。

​ 查阅资料后发现,无线网卡和有线网卡确实存在重要区别,其关键在于,无线链路设备必须有MAC地址,而有线链路设备不必有MAC地址。有线链路,如网线、交换机,他们是不需要MAC地址的。当网卡连接到交换机的某个端口或者某条网线时,该有线网卡的物理层信号只会在这条链路传播。但无线网卡的物理层信号的传播范围是不确定的。比如,有2个无线接入点(AP),分别是AP1、AP2,他们都在对方的信号范围内。有一台手机要连接到AP1,但AP2也会收到发送给AP1的无线信号,因此无线信号帧必须包含AP1的MAC地址,这样AP1才知道这个无线信号是在自己的链路上的。但问题在于,AP是链路层设备,没有IP。假设手机向路由器的IP发送了数据包,那么该数据包的以太网帧的目标地址为路由器的MAC地址。但当数据包到达无线网卡时,无线网卡必须将该包发往AP1,并要求AP1将该帧转发到路由器。由此可见,这时从无线网卡发出的帧包含了路由器、AP、手机的MAC地址,并不是向802.3那样只有2个地址。现在假设路由器向手机发送了回复。路由器发出了目标MAC地址为手机的帧,内部的交换机经过学习后直接将该帧转发到了AP1。一般来说,连接到AP的设备只有1个MAC地址,因此当AP收到该帧后,就知道要转发给手机。而虚拟机的无线桥接网卡为了满足这种一般情况,把虚拟机发出的帧的源地址改为了主机的地址。这样做在发出数据包时不会有问题,因为一个MAC地址完全可以有多个IP。但当接收数据包的时候就会有问题,虚拟网桥怎么知道一个数据包是发给主机还是虚拟机呢?经过实验,可以推断无线网桥查看了数据包的IP,如果是主机的IP就转发到主机,如果是虚拟机的IP就转发给虚拟机。如果都不是的话,不会转发给虚拟机,貌似转发给主机。因此,AP中的其他设备无法使用主机里面的虚拟机的IP作为网关。

​ 进一步的实验发现,VMware的无线桥接原理与Hyper-V不同。主机可以使用Hyper-V虚拟机的IP作为网关(AP内的其他设备依然不行),但VMware不可以。通过Wireshark抓包发现,VMware虚拟机的联网流量可以被抓到。但Hyper-V的外部模式虚拟交换机屏蔽了我的无线网卡,给我新建了一个vEthernet网卡,在所有vEthernet网卡都抓不到虚拟机的流量。当主机和虚拟机通信时,在主机抓包得到的数据帧的MAC地址情况如下

虚拟机类型 传入虚拟机 传出虚拟机
VMware src=host, dst=vmware src=dst=host
Hyper-V src=host_vethernet, dst=hyperv src=hyperv, dst=host_vethernet

​ 可以想象,Hyper-V在无线网卡上层插入了一个交换机,该交换机连接了主机vEthernet网卡和虚拟机网卡,因此Hyper-V不需要在该交换机的层次进行snat --to-address $host --snat-arp(以下简称伪装),所以主机可以使用虚拟机做网关。这也就是为什么Hyper-V新建外部虚拟交换机要断网。而VMware的无线桥接没有新建虚拟网卡,我推测它应该是在网卡驱动加入了hook,对虚拟机发出的所有流量都伪装了源MAC地址。

​ 综合实验现象,我推测VMware虚拟机无线网卡桥接的协议栈如下所示

   +---------+       +---------+     +---------+
   |  Host   |       |   VM1   |     |   VM2   |
   | Network |       | Network |     | Network |
   | Protocol|       | Protocol|     | Protocol|
   |  Stack  |       |  Stack  |     |  Stack  |
   +---------+       +---------+     +---------+
        |                 |               |
        |            +---------+     +---------+
        |            |   VM1   |     |   VM2   |
        |            |vEthernet|     |vEthernet|
        |            +---------+     +---------+
        |                 |                |
        |          +-------------------------------+
        |          |           l2 switch           |
        |          +-------------------------------+
        |                 |
    +-------------------------------+
    |           l3 switch           |
    +-------------------------------+
                   |
           +--------------+
           | wireless nic |
           +--------------+

其中l2 switch是链路层交换机,根据链路层地址转发数据帧。l3 switch是网络层交换机,根据网络层的目标地址转发数据包。对于从l2 switch进入l3 switch的数据帧,伪装源MAC地址。对于从wireless nic进入l3 switch,且目标地址为虚拟机IP的数据帧,将其目标MAC地址改为虚拟机地址。如果桥接的是有线网络,则l3 switch的功能与l2 switch相同。由此可见,虚拟机向主机发送的数据帧的源MAC地址被改为主机地址,主机向虚拟机发送的转发包不会到达虚拟机。

​ 有人可能会混淆虚拟机有线桥接的l3 switch和Linux bridge。实际上这两者存在微妙区别,虚拟机的虚拟网卡只能桥接一个物理网卡,而Linux bridge可以桥接若干网卡。当虚拟机l3 switch收到的帧的目标MAC地址为物理网卡时,会把数据包交给主机网络协议栈。但Linux bridge收到的帧的目标MAC地址为物理网卡时,会把该帧转发到对应的物理网卡,而不是对应的物理网卡的协议栈。也就是说,与Linux bridge关联的物理网卡相当于l2 switch的一个端口,或者说相当于一条网线。假设l2 switch有2个端口A、B,端口A收到目标地址为B的帧,那么该帧会转发到端口B。如果端口A收到目标地址为A的帧,那么交换机会丢弃该帧,不会把该帧重新转发到端口A。因此,如果我们希望物理网卡收到的数据帧被主机处理,那么该帧的目标MAC地址只能是bridge,而不能是任何物理网卡。所以,bridge可以设置IP,而物理网卡不可以设置IP。此时,物理网卡的MAC地址的实际意义也不大,如果bridge没有开启生成树协议,则不可以用网线连接两个在同一个bridge上的网口。

​ Hyper-V的协议栈有所不同,如下图。

   +---------+       +---------+     +---------+
   |  Host   |       |   VM1   |     |   VM2   |
   | Network |       | Network |     | Network |
   | Protocol|       | Protocol|     | Protocol|
   |  Stack  |       |  Stack  |     |  Stack  |
   +---------+       +---------+     +---------+
        |                 |               |
   +---------+       +---------+     +---------+
   |  Host   |       |   VM1   |     |   VM2   |
   |vEthernet|       |vEthernet|     |vEthernet|
   +---------+       +---------+     +---------+
        |                 |                |
      +--------------------------------------+
      |              l2 switch               |
      +--------------------------------------+
                          |
                    +-----------+
                    | l3 switch |
                    +-----------+
                          |
                   +--------------+
                   | wireless nic |
                   +--------------+

图中l2 switch与l3 switch的含义与上述相同。

​ 那么,难道无线网络真的不能进行链路层桥接了吗?理论上可以,但实际上没发现可用案例。实际上,802.11帧可以写入4个MAC地址,前面3个MAC地址的作用上面已经论述,而第4个地址就是用来解决桥接的问题的。如果无线网卡在第4个地址处填入虚拟机的地址,那么虚拟机桥接无线网卡不能做路由的问题就能解决。但遗憾的是,国内常见的计算机网络教科书,如谢希仁《计算机网络(第7版)》、《计算机网络:自顶向下方法(第7版)》均没有讨论这个问题,因此市面上的AP是否具备这个功能,恐怕要打个问号。我用一台TP-LINK路由器桥接了OpenWrt路由器,并将手机连接到TP-LINK的桥接网络,在OpenWrt查阅arp表发现手机的MAC地址为TP-LINK,也就是说这并不是链路层的桥接。虚拟机厂商很可能也意识到了这个问题,因此他们选择从网络层处理这个难题,毕竟对于虚拟机网卡的驱动来说,网络层还是比较容易控制。

不能将一个物理网卡同时设置为WAN和LAN

​ 有人可能想用树莓派做路由器。由于树莓派一般只有一个有线网口,所以用来做旁路由肯定是没有问题的。但能不能作为主路由呢?有人已经在OpenWrt上使用虚拟网卡实现了多播,那我们能不能在树莓派上使用虚拟网卡,物理网口为LAN,虚拟网口为WAN?由于我手上还没有树莓派,因此这个实验在VMware虚拟机上进行,网卡类型为仅主机。

​ Linux有多种虚拟网卡:

  • ifconfig eth0:0 192.168.10.1/24 up 没有独立MAC地址
  • tun/tap 有独立MAC地址,网卡两端分别为用户程序和系统网络协议栈,本身无法连接外网
  • macvlan 有独立MAC地址,网络访问权限与物理网卡相同

看起来macvlan是个不错的选择,于是想当然地设计了这样的网络结构

 +-------------------------------------------------+
 |                   l2 switch                     |
 +-------------------------------------------------+
    |             |             |             |
 +-----+       +-----+      +------+      +------+
 | wan |       |route|      | lan1 |      | lan2 |
 +-----+       +-----+      +------+      +------+
10.3.0.1/24  10.3.0.10/24  192.168.1.101/24 192.168.1.102/24
             192.168.1.1/24

这样也许可以使用,但实际上不建议这样做。首先是安全问题,wan口连接互联网,随时可能有黑客攻击。假如黑客控制了wan,偷偷把IP改成192.168.1.x/24,就能访问内网资源。另外,由于Linux下arp的特性,向route发送的数据包即使MAC与IP不对应也会被接受,而且route发出的数据包的IP和MAC地址也可能不对应,除非专门指定了网卡,这又增加了安全隐患。此外,内网的DHCP广播会被转发到wan,这也是不合适的。因此,wan和lan应该都是独立的网口。

TPROXY

​ 与REDIRECT相比,TPROXY不修改网络层的目标地址,更适合用作透明代理。进行网络配置时需要注意在IPv6中,路由器不会转发源地址为FE00::/10的本地链路地址,因此需要给联网设备分配全局单播地址或唯一本地地址。

​ 部分关键配置如下

ip rule add fwmark 1 table 100
ip route add local 0.0.0.0/0 dev lo table 100
ip -6 rule add fwmark 1 table 100
ip -6 route add local ::/0 dev lo table 100

sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1
flush ruleset
table inet tproxy_chains {
        set tproxy_proto {
                type inet_proto
                flags constant
                elements = { tcp, udp }
        }

        set private_ipv4 {
                type ipv4_addr
                flags constant,interval
                elements = { 10.0.0.0/8, 127.0.0.0/8,
                             169.254.0.0/16, 172.16.0.0/12,
                             192.168.0.0/16, 224.0.0.0/4,
                             255.255.255.255 }
        }

        set private_ipv6 {
                type ipv6_addr
                flags constant,interval
                elements = { ::/127,
                             fc00::/7,
                             fe80::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff }
        }

        chain prerouting {
                type nat hook prerouting priority mangle; policy accept;
                meta l4proto @tproxy_proto socket transparent 1 meta mark set 0x00000001 counter packets 0 bytes 0 accept
                meta mark 0x000000ff counter packets 0 bytes 0 return
                meta mark 0x00000001 counter packets 0 bytes 0 goto to_tproxy
                ip daddr @private_ipv4 counter packets 0 bytes 0 return
                ip6 daddr @private_ipv6 counter packets 0 bytes 0 return
                meta l4proto @tproxy_proto meta mark set 0x00000001 counter packets 0 bytes 0 jump to_tproxy
        }

        chain output {
                type route hook output priority mangle; policy accept;
                meta mark 0x000000ff counter packets 0 bytes 0 return
                ip daddr @private_ipv4 counter packets 0 bytes 0 return
                ip6 daddr @private_ipv6 counter packets 0 bytes 0 return
                meta l4proto @tproxy_proto meta mark set 0x00000001 counter packets 0 bytes 0 accept
        }

        chain to_tproxy {
                meta l4proto { tcp, udp } tproxy ip to 127.0.0.1:12345 counter packets 0 bytes 0 accept
                meta l4proto { tcp, udp } tproxy ip6 to [::1]:12345 counter packets 0 bytes 0 accept
        }
}

​ 对于其他机器eth1发给虚拟机的转发包,首先会进入prerouting链。在tproxy前需要排除不需要tproxy的地址。对于已有连接的转发包,使用socket transparent检测出来后直接标记,避免了重复tproxy和目标地址判断。

+----------+  +------+
|web client|  |tproxy|
+----------+  +------+
     |         ↑    |
   1 |       2 |  3 |
     ↓         |    ↓
   +------------+  +----+
   |     lo     |  |eth0|
   +------------+  +----+

​ 如果希望把虚拟机自身的流量转发到tproxy,则情况相对复杂,如上图所示,需要对tproxy输出的流量进行另外的标记,这里标记为0xff。虚拟机web client输出的流量经过output链,排除了不tproxy的地址后,路由到lo,然后经过prerouting链,进入tproxy。tproxy输出的流量,直接发到eth0。那么,为什么在prerouting链要检测0xff标记呢?因为当tproxy回复web client时,目标IP为本机,不需要设置IP_TRANSPARENT,在图中反方向路径2时,会通过output链到达lo,然后经反方向路径1通过prerouting链进入web client。因此,prerouting链对于被标记为0xff的包应当直接return。

​ 为了进一步优化效率,在prerouting链对于标记为0x1的包直接tproxy,不再匹配目标地址。这些包在output链已经完成了地址匹配。

​ 实验中还发现一个奇怪的现象,socket transparent不能在output链进行匹配,否则一旦socket transparent为1,即使不执行任何实际操作,Linux也会死机。

results matching ""

    No results matching ""