零依赖的软路由——使用OpenVSwitch改造家庭网络

十月 08, 2025 [网络与安全] #obsidian #嵌入式 #网络

零依赖的软路由——使用OpenVSwitch改造家庭网络

原文链接:https://zhuanlan.zhihu.com/p/372635925

众所周知,家庭网络少不了路由器。路由器是家庭网络的入口,承载了IP获取、DHCP、DNS、NAT等一系列基础功能。

常见的家庭网络拓扑中,路由器接在光猫后面,设备通过有线或无线的方式接在路由器后面,如下图所示:

                                            +-------+
                                    +-------| Phone |
                                    |       +-------+
                                    |
    +-------+       +--------+      |       +----+
<---| Modem |-------| Router |------+-------| PC |
    +-------+       +--------+      |       +----+
                                    |
                                    |       +------+
                                    +-------| XBox |
                                            +------+

家用路由器功能比较简单,说白了,家用路由器的核心功能不过就是广播域+DHCP Server+简单的SNAT网关+AP热点,这些功能跟网络上所说的“路由”其实关系不大。 由于家庭网络环境简单,设备少,也没有什么复杂的网络功能可以往里加。比较实用的附加功能其实也就是“一键组网”和“无线Mesh”,但是跟“路由”本身也没有什么太大关系。

而软路由则功能比较多,可定制性强。支持例如多拨号,限流,流量处理(例如去广告)等功能,性能富余的软路由还可以跑容器、虚拟机,或者当作NAS使用。此外软路由搭建简单方便,但凡是能装linux的设备,有两个网口(单网口稍复杂一些,后文会讲),就能做软路由,比如树莓派插usb网卡、笔记本电脑、HTPC等,也有一些硬路由可以刷OpenWrt等固件变成软路由。

使用软路由时,有这几种组网方案:

软路由拨号

软路由拨号。需要找电信把光猫配成桥接模式。这样拓扑简单,流量自然而然的都从路由走。

    +--------+              +---------+
<---| Router |--------------| Devices |
    +--------+              +---------+

这里还分两种情况。 一种情况是路由器有至少两个网口,这种情况接线方便,一个口上联接光猫,一个口下联接家庭设备即可。 另一种情况是路由器只有一个网口,这种情况只能把路由器、光猫、以及家中的其他设备接在同一个交换机上(一般家用交换机不支持vlan,如果支持的话接交换机的口开trunk,划分vlan后一个网口可以当作多个网口用,就和前一种情况一样了)。由于上联光猫(或者运营商)对报文有处理,家里出去的广播包不会影响它的正常工作,所以这样接线流量也可以正常跑通。

光猫拨号

软路由作为光猫的“用户”,光猫使用路由模式。其实光猫本身就是一个路由器,电信工作人员也更倾向于将光猫配置成路由模式,这也是现在大部分家庭路由器一接上光猫不用拨号就能直接使用的原因。

这种情况虽然对于普通用户来说简单方便,但如果想尽量减少NAT次数的话,组网反而会稍微复杂一些。

如果路由器只有一个网口,那么你需要关闭光猫的dhcp功能,让路由器做dhcp,或者关闭路由器的DHCP功能,将光猫的DHCP信息中的网关地址配置为路由器的地址(当然,路由器上联需要手动配置网关ip为光猫,而不使用DHCP分配到的,否则会出现网关地址是自己的情况)。这样才能让家庭设备自动将你的路由器作为默认网关。网络包会从路由器上绕一圈,路由器能够有机会处理这些包。 如果光猫没法关dhcp,那么可以找一个支持VLAN的交换机,通过VLAN+chunk口实现多网口路由器的效果。家用的一般叫做“网管交换机”,小几百块一个,也是能负担得起的。

    +-------+.        +--------+
<---| Modem |----+----| Router |
    +-------+    |    +--------+
                 |
            +---------+
            | Devices |
            +---------+

很多人认为,光猫拨号的情况,软路由只能作为所谓的“旁路由”,即上面单口的这种情况。而实际上,如果路由器有多个网口,是可以玩出花来的。这也是本文实操的场景。

基本概念

在继续之前,我们先看一些待会会用到的东西。

SDN

SDN即“软件定义网络”。

传统的网络设备严格按照2、3、4层来处理网络报文:二层根据目的mac选 出端口、三层根据目的ip选下一跳、NAT操作按照五元组做session跟踪。

而SDN则没有任何限定规则。只要你愿意写代码,什么转发能力都可以实现。例如你可以写“如果tcp的目的端口是80,则将目的mac地址设置为00:11:22:33:44:55并将包从eth0发出”。 即使不写代码,绝大部分操作也可以完成。前人已经规范了一组标准的“匹配和转发规则”,这套规则我们一般称作“流”(flow),“流”由优先级组合在一起,形成“流表”。社区常用的配置流表的协议,叫做“OpenFlow”。而OpenVSwitch(简称ovs)则是使用最广泛的应用了OpenFlow的虚拟交换机。

DHCP

DHCP(动态主机配置协议)用于设备的自动网络配置。设备插上网线之后,就会发起DHCP请求,这是一个广播的UDP包,同一个二层的所有机器都会收到DHCP请求。在设计网络时,一个广播域内应当只有一个DHCP服务器。 DHCP服务器收到请求后则会给客户端传回响应,告诉客户端它可以使用的ip、网段、默认网关、dns服务器,以及例如打印机地址之类的各种网络信息。 (注:实际上DHCP有两次请求响应,具体可以看rfc2131)

DNS

DNS用来做域名解析,主要功能(家庭一般能用到的功能)是,把肉眼可读的字符串(例如zhihu.com)转换成ip地址(例如103.41.167.234)。 客户端发送一个基于UDP的DNS包给服务器,DNS服务器响应DNS查询的结果。

网桥(br)

linux和ovs都提供一种叫做网桥(bridge,简称br)的虚拟网络设备。网桥可以类比为交换机。

网桥可以包含其他网络设备,即你可以把网络设备插到网桥上。每个加入网桥的设备,都相当于连了一根网线到网桥这个交换机上。 此外,在创建网桥时,会按你输入的名字建立一个网络设备,用于管理这个网桥,这个网络设备也是可以跑流量的,它默认就连接到了这个网桥里面。不过我们这里并不使用这个设备。

veth

linux提供一种虚拟网络设备,叫做veth,更具体的说,叫做veth pair。veth是一对连接在一起的虚拟网卡。

+-------+                +------------+
| veth0 |----------------| veth0-pair |
+-------+                +------------+

从一端进去的包,会从另一端出来。所以我倒是觉得veth更像是一根网线。举个例子,比如有两个交换机,你可以用一根网线把它们连起来;相对应的,你也可以用一对veth把两个网桥连起来。当然,veth是linux的虚拟设备,所以你可以一端插在网桥里,一端接在linux协议栈上,这样就可以让协议栈与网桥互动。 一般会把veth的一端放在网桥里,另一端放在某个netns里,比如docker默认的bridge网络就是这么干的。

Namespace(名字空间,ns,netns)

linux提供一个网络虚拟化的机制,叫做Namespace,一般简写ns,或者netns。每个ns有自己的一整套网络协议栈,包括网络设备、路由、netfilter等。使用ns可以方便地分割网络配置。

改造之前的网络方案

这里说一下我之前用的比较挫的网络方案:

  1. 光猫使用路由模式
  2. 光猫后面接一台华为WS5200增强版家用路由器,称作R1
  3. 通过HiLink(一键组网功能),无线转接到另一台WS5200增强版家用路由器,称作R2
  4. R1底下没有有线设备
  5. R2底下有线接了台式、群晖、macmini
  6. 无线设备就不多说了,R1R2哪个近接哪个
  7. 群晖上跑一个桥接的虚拟机,上面跑了一个所谓的“旁路由”,但是这个“旁路由”比较特殊,下面具体说一下

这个虚拟机里跑了一个我自己写的软件,叫vproxy。vproxy不是“路由”,而是“DNSServer”和“ProxyServer”。当收到客户端的DNS请求时,vproxy检查请求的域名是否在配置的规则内;如果在规则内,则会在配置的网段内分配一个空闲的ip,将这个ip返回给客户端。在此之后,客户端会以这个返回的ip作为目的地址开TCP连接。这时,请求就会发到vproxy上,vproxy根据分配的这个ip和域名的对应关系,进行实际的流量处理。

拿我的环境举个例子:

  1. 网段192.168.3.0/24
  2. 路由器网关ip 192.168.3.1
  3. vproxy服务器192.168.3.21
  4. 在群晖上配置DHCP服务器(群晖自带的功能)
  5. DHCP配置默认网关192.168.3.1
  6. DHCP配置掩码/24
  7. DHCP配置DNS服务器192.168.3.21(即vproxy服务器)
  8. DHCP配置可分配网段从192.168.3.65到127
  9. vproxy配置ip分配的网段192.168.3.128/25(这是192.168.3.0/24的子段,也就是说所有的IP都是同一个网段下的)

拓扑如下:

                                                         +----------+
                                                   +-----| Synology |(vproxy)
                                                   |     +----------+
   192.168.1.0/24                                  |
     +-------+     +----+               +----+     |     +----+
<----| Modem |-----| R1 | ...))) (((... | R2 |-----+-----| PC |
     +-------+     +----+               +----+     |     +----+
               192.168.3.0/24                      |
                                                   |     +---------+
                                                   +-----| MacMini |
                                                         +---------+

拿http://zhihu.com举例,假设http://zhihu.com在配置的域名规则内,那么客户端访问http://zhihu.com时的顺序为:

  1. 客户端请求vproxy dns:查询zhihu.com对应的A记录
  2. vproxy检查配置,发现zhihu.com需要处理,则在配置的网段内分配一个ip:192.168.3.135,并记录ip和域名zhihu.com的对应关系
  3. vproxy将192.168.3.135响应给客户端
  4. 客户端发起https请求:192.168.3.135:443 ,这是一条TCP连接
  5. vproxy接受连接,从映射关系中,找到192.168.3.135对应的域名zhihu.com
  6. vproxy执行针对zhihu.com的流量处理

这里有一个关键点:vproxy要同时监听整个/25网段的所有ip的所有端口。这自然不是傻乎乎的监听那么多地址(2^(32-25) * 65535)。要监听整个网段的所有端口,可以通过local路由和transparent proxy来实现,只开一个监听socket即可完成。

这里提一下,Envoy作为Istio sidecar时,也可以使用这种监听方式。

原理具体可见Cloudflare的一篇博客:

https://link.zhihu.com/?target=https%3A//blog.cloudflare.com/how-we-built-spectrum/Abusing Linux's firewall: the hack that allowed us to build Spectrumblog.cloudflare.com/how-we-built-spectrum/

上述这种流程(通过域名做负载处理)的好处是,无需处理的域名的流量不会经过vproxy,没有性能损耗;并且只要是用域名访问的服务,就都能享受到统一的负载处理。而现在的互联网环境,绝大多数站点都是通过域名提供服务的,也就是说绝大多数请求都可以享受负载处理。

但是这个网络有不少问题:

  1. R1不支持配置在DHCP中配置DNS服务器,也不支持配置默认网关
  2. 有一部分设备无法访问群晖上的DHCP服务(抓包看discovery广播包都没收到,猜测是有安全限制)
  3. R1和群晖都开DHCP,在R1附近结果都是R1的,在R2附近DHCP结果是随机的。这就是同一个广播域中多个DHCP服务器的冲突问题
  4. 虚拟机占用群晖资源,一些管控操作非常卡。关闭虚拟机后,进入群晖管理页面只要几秒钟,开启虚拟机后需要半分钟甚至一分钟
  5. 虚拟机没有硬盘相关的优化,群晖的硬盘无法正常休眠
  6. 在R1上有一次NAT,在光猫上还有一次NAT,性能略微有些不必要的损失

基于OVS的全新网络方案

其实我想要重新组网是因为看到了一款国产兆芯x86CPU的小机器。这款电脑使用UOS(Debian),外形也比较小巧。 最关键的是,它有两个网口。

x86机器、双网口,可以玩起来了!

组网目标

  1. 网络中只增加这一台新机器(后续简称R0)
  2. 使用机器自带的UOS,不重装操作系统
  3. 使用光猫做拨号和NAT,即光猫的路由模式
  4. 整个网络不感知R0,即机器看到的网关地址、DHCP地址、DNS地址,均为光猫的地址
  5. R0处理DHCP、DNS,以及在R0上使用vproxy进行流量处理
  6. 光猫不做任何修改,即R0不但要对下联透明,还要对上联的光猫透明,就好像网络中根本不存在这个设备一样

实现方案

该网络中需要定制的内容非常多,理论上用iptables配合策略路由也是可以处理的,但是性能会比较差。这里先放一放,后面附录中我会把纯iptables的思路写一下。

而OVS可以操作2-4层所有报文,可以向任意物理网卡和虚拟网卡转发网络包,流表配置也简单方便。若使用OVS,不管是从性能考虑,还是从维护性和扩展性考虑,都会更有优势。

网络拓扑

 +----------------------------------------------------------+
 | +-----------+ +-------------------+ +------------------+ |
 | | netns: gw | |   netns: vproxy   | |                  | |
 | |           | |                   | |                  | |
 | |   dhcp    | |    dns,traffic    | |  ssh,vnc,other   | |
 | |           | |                   | |                  | |
 | |  +-----+  | |    +---------+    | |    +-------+     | |
 | |  | gw0 |  | |    | vproxy0 |    | |    | veth0 |     | |
 | |  +--+--+  | |    +----+----+    | |    +---+---+     | |
 | +-----|-----+ +---------|---------+ +--------|---------+ |
 |       |                 |                    |           |
 |       +-------+         |         +----------+           |
 |               |         |         |                      |
 |          +----|---------|---------|-----+                |
 |          | +--+--+ +----+---+ +---+---+ |                |
 |          | | vgw | | vproxy | | admin | |                |
 |          | +-----+ +--------+ +-------+ |                |
 |          |           OVS-BR             |                |
 |          | +--------+      +----------+ |                |
 |          | | uplink |      | downlink | |                |
 |          | +---+----+      +----+-----+ |                |
 |          +-----|----------------|-------+                |
 |                |       R0       |                        |
 +----------------|----------------|------------------------+
                  |                |
   192.168.1.1/24 |                |
     +-------+    |                |     +----+           +----+
<----| Modem |----+                +-----| R1 | .))) (((. | R2 |--+
     +-------+                           +----+           +----+  |
                                        ap bridge                 |
                                                                  |
                                                                  |
                                            Devices... -----------+
  1. 光猫使用路由模式
  2. 光猫下面接R0
  3. R0内部一个网口作上联(uplink),一个网口作下联(downlink)。并开两个ns,一个把自己虚拟成光猫(netns: gw),并且跑dhcp;另一个跑DNS和流量处理(netns: vproxy)
  4. 使用veth将两个ns连接到br上,将上联、下联物理网口也加入br中,并开一个veth0连接到协议栈上,以便登录机器
  5. R0下联接R1,R1跑ap bridge,R2中继,并通过网线连接PC等设备

网络规则

这里涉及的内容比较多,我们从“让一台机器正常上网”的报文顺序一条一条分析。

当一台设备加入网络时,该设备会发出DHCP discovery广播,尝试获取网络配置信息(ip、网段、网关、dns服务器等)。这时,我们要把DHCP请求(包括广播和单播)发送给图中的netns:gw。 后续的DHCP操作,可能均为广播,也可能会出现单播;不过无论是哪种报文,凡是客户端发送给服务端的DHCP包,都必须发送给netns:gw,而不能发送给光猫。 服务端发送给客户的的DHCP包,理论上无需特殊处理,但是为了避免实现细节问题,可以直接配一条规则:从DHCP服务器出来的的DHCP包一律向下联发送;由于R0机器上的所有ip都会配置为静态ip,所以R0自身无需DHCP,一股脑往下联发就足够了。此外,还需屏蔽从上联过来的任何DHCP包,防止光猫扰乱DHCP。

这些规则配上后,客户端即可正常通过DHCP获取ip了。

接下来,我们还是拿知乎为例,并且假定我们需要处理客户端访问知乎的流量。 假如设备想要访问zhihu.com ,首先设备想要知道zhihu.com对应的ip地址。设备想要发起DNS请求,但此时设备只知道DNS服务器的ip而不知道mac,于是它需要发送ARP来获取DNS服务器的mac地址。 由于DNS ip就是光猫ip,所以这一步直接让光猫响应即可,无需特殊配置。当然,这里做一层arp代答也可以,只不过没什么必要。

后续设备会发起DNS请求,我们需要配置流表,将DNS请求导入netns:vproxy。

这里注意,由于客户端想访问光猫,所以包的目的mac是光猫的mac,为了让linux能够处理报文,我们还需要将这个包的目的mac改为netns:vproxy中虚拟网口的mac。后文会出现多次将包导入其他网口的描述,只要这个包不是广播包,那么就需要修改mac。

收到DNS请求后,vproxy会传回DNS响应。这是一个单播UDP包。在发出前也会通过arp拿到请求者的mac。这一步也无需特殊流表。

这里稍微留意一下,假如vproxy收到的DNS请求是无需处理的域名,或者请求类型不是A或者AAAA记录,那么它需要向光猫代理该DNS请求,这是需要在流表中体现的,后文会说到。

现在设备已经拿到了zhihu.com对应的ip地址。由于zhihu.com是需要处理的域名,所以这个ip地址是由vproxy分配的一个假地址(我经常用100.64.96.0/11这个段),由于这个假地址是LAN192.168.1.0/24以外的,所以设备会将包发给默认网关,也就是192.168.1.1。这里我们要加这样的流表:把目的地址为“假地址网段”的包导入netns:vproxy。

在netns: vproxy中,通过transparent proxy,vproxy可以accept该连接,并可以通过连接的local ip在内存里找到其对应的域名。此时vproxy想知道该域名对应的真实的ip地址,所以它会向光猫192.168.1.1发起DNS请求。 当然,为了发出DNS包,首先还是会发送arp查询192.168.1.1的mac,之后才是发送单播UDP。

在ARP这一步并不需要什么特殊处理。 在DNS这一步,由于上面一个步骤将DNS包一股脑的全导入了netns: vproxy,所以在这里我们需要通过流表来保证,从vproxy0出来的DNS请求包,必须能够正常地流出,不能再导回到netns:vproxy中,或者,也可以明确指定:从vproxy0出来的DNS请求包,都要送到上联(光猫) 去。

接下来vproxy会做实际的TCP转发,这一步无需任何特殊配置。

上述流程走下来,设备看到的网关、DHCP地址、DNS地址均为192.168.1.1,但是实际上流量已经被悄悄地处理过了。

设备看到的都是192.168.1.1

先看看设备

UOS

这台电脑的CPU是兆芯,操作系统是UOS。国产CPU配国产操作系统,没毛病。UOS是deepin的商业版,而deepin是基于debian的。所以Debian能用的,UOS理论上都能用,Debian的使用方式大部分也能搬过来。

neofetch截图

网络

从网络管理界面可以看到,有两个有线网络和一个无线网络。实测下来发现开启有线网络的情况下,即使删除了默认的网络配置,每次重启后还是会自动生成网络配置。我们的网络方案中需要将两块有线网卡全部纳入OVS管理,这里我们直接禁用这两块网卡。 比较“幸运”的是,在这个界面里禁用网卡、再去终端里用ip link set xx up拉起网卡后,这个控制界面里的网卡依旧是禁用的。看来上层UI和底层是分离的。

在界面上禁用所有网卡

这里的无线网卡可以用hostapd做无线网络,不过该技术不在本文介绍的范围内,暂且略过吧。

电源管理

刚开机的时候风扇声音呼呼得,吓了我一跳。如果按这个分贝就别当软路由了,吵都能吵死。于是我想看看有没有省电模式或者低性能模式。进入电源管理,果然有个节能模式。 开启节能模式后风扇声音马上变小了,只有靠近才能听到。

开启节能模式

顺便在电源管理里把“关闭显示器”改成了1分钟,“自动锁屏”改成了1分钟,“进入待机”改成了“从不”。还好是进来看了一眼,不然过一会就给我自动待机了。这里按电源按钮时,本来是“无任何操作”,我改成了“关机”。这样不用时直接按一下电源就能关掉,不需要SSH或者VNC连上去操作了。

禁止自动待机

为了进一步节约能源(虽然可能没啥用),可以进入蓝牙管理,把蓝牙关掉。目前暂时没在用任何蓝牙相关的功能,以后看看能不能发掘一些特殊的玩法把。

更新

当时刚打开机器时还有几个更新项,我直接全给他更了。这里“更新”里面的项目可以全部禁用。不过其实它并不会自动安装。我干脆就一直开启了,在空闲时再VNC进去手动进行安装和重启。

更新设置可以全部关掉

更新其实用的也是apt。

更新中

更新后可能需要重启

正写这篇文章时候刚好检测到新补丁,更新了一把~

进程管理

UOS自带一个“系统监视器”。VNC进去当监控图表看也不是不行。

Root

这里是重中之重。只有在root下才能装ovs,执行我们的SDN软路由配置。UOS允许开启root,但是注意!注意!注意!一旦开启root,设备将失去保修!

这其实也算是商家的一种自保手段。因为root后,debian能干的事UOS都能干。就不说rm -rf /了,刷个固件啥的玩坏也都是有可能的。对这种约束表示理解。

在“控制中心->通用->开发者模式”里,可以“进入开发者模式”。点击后会弹出确认框。接下来需要注册一个UOS账号,登录账号后才能进入开发者模式。好在注册流程比较简单,也没啥花里胡哨的限制。

开启root

准备就绪!

接下来就可以打开终端,开始第一步配置了!

实操环节

1. 让我能ssh进去

老是抱着个小屏幕加临时的键鼠做配置显然不太方便。 我们第一步就是要让网络能跑通,并且能够从其他设备上ssh进去。后续其他配置做起来就会比较舒服了。

打开终端,sudo /bin/bash输入密码进入root。

然后把ovs装上:

apt-get update
apt-get install openvswitch-switch

接着建网桥,然后把两块网卡加进去。

ovs-vsctl add-br ovsbr0 # 网桥的名字是ovsbr0
ovs-vsctl add-port ovsbr0 enp5s0
ovs-vsctl add-port ovsbr0 enp1s0

这里的网卡名可以通过ip link 查看。我的网卡名是enp5s0和enp1s0,这个命名规则和PCIe插槽有关,总之定下来了就不会再变了。而OVS网桥和端口信息,都是会由ovsdb持久化下来的,所以这里的配置是一劳永逸,后续无需写启动脚本做配置。

配置后,OS其实可以看作没有网卡。两块网卡都是master ovs-system,表示由ovs接管。我们需要创建一个虚拟网卡,把OS接到网桥里。

这里使用veth。

创建一对veth,一端插在OS上,一端插在网桥里:

ip link add veth0 type veth peer name veth0-in-br
ip link set veth0-in-br up
ovs-vsctl add-port ovsbr0 veth0-in-br
ip link set veth0 address 5c:85:00:00:00:02 # 这里mac作了抹去处理,配置后面解释
ip link set veth0 up

这里有一个操作,是给虚拟网卡配置mac。注意,这里的mac需要和上联网卡保持一致。这个环境中,所谓的“上联”网卡就是接到光猫上的那块网卡。mac地址可以用ip link查看。

这里解释下为什么要给虚拟设备配mac。我们这块虚拟网卡的“角色”,其实就是“上联”网卡,把mac地址配成上联是一件很自然的事。mac地址还包含了厂商信息,使用物理网卡的mac地址,路由器可以分辨出这台机器是什么设备。遇到网络问题时(例如ip冲突),抓包看到源mac地址后,也可以去搜索该mac对应的厂商,进而猜测是哪台设备在发包。 一句话总结:对外透明、方便维护。

说个题外话,玩过OVS的应该知道OVS有一个type = internal的虚拟网络设备,它就是一个从网桥连出来的设备。但是我们不用它。internal口是一个和网桥强绑定的设备,而veth是两个设备,一个设备塞进网桥后,另一个设备依然可以自由操作。docker和k8s社区也有基于ovs实现网络方案的项目,一般也都不使用internal口,主要还是考虑netns和设备改名的问题。我们这里随大流,用veth这种更加广泛使用的方案。

虚拟网口现在正常了,接下来给veth0配置ip,并且配上默认路由,让这台机器能正常联网。

ip addr add 192.168.1.8/24 dev veth0
ip route add default via 192.168.1.1

假如之前通过dhcp获取过ip,那么最好是复用之前拿到过的ip,和光猫上的DHCP租约相一致。这么做的原因无他,唯洁癖耳。

网络配置完成,把sshd起起来。

systemctl enable sshd
systemctl start sshd

因为公钥比较长,输入麻烦,我们可以先允许密码登录,登进去复制粘贴公钥后,再换成公钥登录。这跟网络无关,就不展开了。

2. 配置netns: vproxy

我们把vproxy相关的网络环境配在一个单独的netns里,可以隔离网卡和iptables等规则,也可以防止误操作。

先看一下我们要实现的功能。客户端会对网关地址(也就是192.168.1.1)来发起DNS请求。 在接收到请求后,vproxy可能返回虚地址,也可能需要代理DNS请求给网关,并且,这个netns里面的程序正常运行时也可能要发起DNS请求。

这里我们用OVS把所有DNS请求(udp,目的端口53)全部导入这个netns里,并且在netns里配置一条iptables规则,在NAT表的PREROUTING链里,把所有目的地址为udp,192.168.1.1:53 的包都REDIRECT到本地。

由于PREROUTING是包刚从网卡出来时走的链,隐含意思就是“入方向”。在出方向并不会走这条链,所以出去的目的地址为192.168.1.1的包不会受影响。

流程如下:

Client ---192.168.1.1:53---> OVS -> NetNS --REDIRECT--127.0.0.1:53--> vproxy
vproxy ---192.168.1.1:53---> OVS -> Uplink ----> GW

首先配置netns和虚拟网卡。

ip netns add vproxy
ip link add vp-veth0 type veth peer name vp-veth0-in-br
ovs-vsctl add-port ovsbr0 vp-veth0-in-br
ip link set vp-veth0-in-br up
ip link set vp-veth0 netns vproxy
ip netns exec vproxy ip link set vp-veth0 address d6:00:00:00:00:bf
ip netns exec vproxy ip link set vp-veth0 up
ip netns exec vproxy ip addr add 192.168.1.9/24 dev vp-veth0
ip netns exec vproxy ip route add default via 192.168.1.1

这里创建了一个veth pair,把其中一端加到网桥里,另一端挂到netns里。并且在netns里配置ip和默认路由。这里注意,当虚拟网卡创建时,自动就会分配一个mac地址,这里可以记录一下这个mac地址,以后每次重启都给网卡分配这个mac。这样对其他设备来说,192.168.1.9这个ip对应的mac都不会有变化。这是一个习惯性问题,可以避免一些莫名其妙的网络异常,比如arp缓存失效之类的。此外,由于局域网内的设备总是想要给光猫发包,而其对应的mac地址显然和veth设备的mac地址不同。这时我们想把包发到这个veth设备上,就必须把包的目的mac改掉。这个操作会写成流表,把mac固定下来的话,写流表会更加方便。

接下来配置OVS。匹配下联网口进来的、目的端口53的udp包,把它送到netns的网口上。要送到netns的网口vp-veth0上,就要把包往网桥里面的vp-veth0-in-br上发。

ovs-ofctl add-flow ovsbr0 table=0,priority=500,\
    udp,in_port=vp-veth0-in-br,nw_dst=192.168.1.1,tp_dst=53,\
    actions=output:enp5s0
ovs-ofctl add-flow ovsbr0 table=0,priority=499,\
    udp,nw_dst=192.168.1.1,tp_dst=53,\
    actions=mod_dl_dst:d6:62:92:ee:ce:bf,output:vp-veth0-in-br

这两条流表,前一条的含义是:对于所有从vp-veth0出来的指向网关的DNS包,全部送去上联(也就是送去光猫),而对于其他所有DNS包,全部送去vp-veth0。 这里通过priority实现了类似于“if-else”的语义。

这时如果在netns:vproxy里抓包,就能收到外面进来的DNS请求了。并且可以使用dig测试,在这个netns中可以正常向网关发出DNS请求、接收DNS响应。

现在包虽然进来了,但是由于192.168.1.1并不是本地ip,所以不会被处理。默认ip_forward也是关的,所以这个包也不会路由出去(如果ip_forward打开的话,这个包会被送到OVS里,根据规则,从vp-veth0-in-br过来的指向网关的DNS包都会到上联去,DNS其实也是能走通的)。

我们要配一条iptables规则,让我们的程序能够处理这些DNS请求。

iptables -t nat -I PREROUTING -d 192.168.1.1 -p udp --dport 53 -j REDIRECT --to-ports 53

这里规则建在PREROUTING链上,表示入报文,匹配目的地址192.168.1.1,协议udp,目的端口53,执行REDIRECT,重定向到53端口。REDIRECT规则总是将包向本地导入。在回包的时候,会走conntrack,自动给它NAT回去。

此时,DNS配置完成。我们只要把进程启动起来就ok了,一般通过service文件即可做好配置,这里不详述。

3. 配置虚拟ip的网络环境

在之前的流量处理流程中讲到过,我的DNS代理进程会将要做流量处理的域名解析到一段虚拟的ip网段上。这里我选择的是100.96.0.0/11。 由于我的流量处理进程和DNS代理就是同一个进程,所以很自然的,这个虚拟ip网段也需要扔到netns:vproxy里面。在netns里面的iptables和路由表都可以参照上面Cloudflare的那篇博客进行配置。这里着重说一说OVS的配置。

其实OVS里配置也很简单。我们看一下网络会怎么走。

由于100.96.0.0/11在192.168.1.0/24之外,所以客户端在访问这些虚拟ip时,会直接把包发给网关,也就是192.168.1.1,就和访问互联网一样,所以我们从IP层开始考虑就够了。

我们需要匹配目的地址是100.96.0.0/11 的ip包,把它们全都发到vp-veth0网卡上。 此外,和上面DNS包一样,由于客户端想把包丢给网关,所以包的目的mac是网关的mac。为了把包发给netns: vproxy,这里还需要把mac地址改成vp-veth0的mac地址。

ovs-ofctl add-flow ovsbr0 table=0,priority=500,\
    ip,nw_dst=100.96.0.0/11,\
    actions=mod_dl_dst:d6:00:00:00:00:bf,output:vp-veth0-in-br

一条流表即可。

在回包时,由于目的地址是192.168.1.0/24内的,所以无需任何额外配置即可正常回去。

4. 遇到了一点小问题

在做配置时遇到过一个奇怪的现象。去ping192.168.1.8,也就是我的软路由时,发现有时能通有时不通,ssh也是如此,有时可通有时不通。于是我开着ping抓了个包,发现包能过去,但是响应没有发出。

我把-e打开一看,发现目的mac不是设备的mac。查了一下发现是下联网口的mac。在客户端看了下neighbor表,发现的确192.168.1.8指向的是下联网口。这就奇怪了。下联网口挂在OVS里面,也没有别的设备配置和它一样的mac,怎么会被arp学到呢。

于是我在软路由的veth0口、下联口,以及我的电脑上同时抓包。最终发现有一个61.54.25.98的ARP查询广播从下联口发出:who has 61.54.25.98 tell 192.168.1.8。其他设备看到广播后,就会直接把neighbor表中的192.168.1.8的mac指向下联。

这就很奇怪了。明明61.54.25.98是网段以外的ip,为什么会发ARP查它的mac呢?

这个ip对应的域名是packages.deepin.com。看起来是UOS自己内置程序的问题了。它会直接指定网卡发ARP,但是因为该网卡上没有绑ip,所以这个程序又扫了一遍已有的ip,然后选了它查到的第一个ip发送ARP,这就导致了上面看到的现象。

我们其实不需要知道是哪一个程序在指定网卡发ARP,因为即使找到对应程序,也不一定能关掉,即使能关掉,也不能保证后续其他程序不会触发类似的问题。一般来说,指定网卡发ARP时,肯定是会选择网卡上绑着的IP作为ARP的src。所以尝试给上联绑192.168.1.8,给下联绑192.168.1.7。

事实确实和预期的一致。现在下联看到的这种奇怪的ARP长这样,不会再扰乱neighbor表了:

网卡绑IP,保证ARP不要乱

不过话说回来,罪魁祸首还是得找的。

首先我观察了一波ARP包的时间,非常有规律,每分钟发一轮,不过并非在0秒时开始,所以不像是crontab。我也翻了一遍crontab相关命令,并没有看到可能会发ARP的命令。 接下来把发ARP时的进程列表,和无ARP时的进程列表存了下来,并做了diff,结果没有任何差异。也就是说,触发ARP的进程并不是新进程,猜测就是一个daemon进程。 结合这个域名,估计十有八九是一个自带的UOS系统服务触发的。

于是开始对进程逐一排查,优先排查和应用商店、更新、网络相关的进程。因为猜测是系统进程,所以按进程号增序排查。

这个进程/usr/sbin/NetworkManager映入眼帘。 很明显他是网络管理相关的进程。strace -p $PID -f 2>&1 | grep '61.54.25.98'。观察了五分钟,每一分钟都会和ARP抓包一起刷出日志。那就差不多了。看一眼完整的strace输出。

好家伙,它挨个网卡发请求。由于这几个网卡没有对应的路由表配置,于是不管目的ip是谁,它都会尝试走二层,就是说它会发ARP来做查询。

网络管理器的strace输出

直接在systemctl里查NetworkManager状态,果然能查到。虽然不知道具体是用来做什么的,不过先停了试试。停止后,NetworkManager进程消失,等待5分钟,也没有收到这样的ARP包了。

我怀疑这个进程是UOS上层UI里的网络配置相关的进程,所以VNC进去瞅了一眼。好家伙,网络配置管理中,几个网卡配置直接消失了~

停止NetworkManager后,显示设备被删除

网卡都消失了~

其实从结果来看挺不错的,本身我就不想让UOS来管理网卡。之前是禁用,现在直接消失不给配置,似乎更合适。我直接把这个服务disable掉,后续重启也不会再启动了。

systemctl disable NetworkManager

5. DHCP

上面我们配置了几个静态ip,其中一个是192.168.1.7 。当时配这个ip的时候arping和ping了一把,都没有找到这个ip。但是过了一会,发现有一个智能设备拉起。这个设备可能是在配置的空隙之间通过光猫的DHCP拿到了这个ip。后续续租时也没检查网段内的ip。所以它一直拿着这个ip不放。

本来其实不太想自己做DHCP的,但是既然遇到这个很坑的情况就不得不做了。

apt-get install isc-dhcp-server

在/etc/dhcp/dhcpd.conf中清空所有配置,并增加如下配置:

default-lease-time 86400;
max-lease-time 86400;

subnet 192.168.1.0 netmask 255.255.255.0 {
  range  192.168.1.65 192.168.1.191;
  option routers 192.168.1.1;
  option domain-name-servers 192.168.1.1;
}

此外还需在/etc/default/isc-dhcp-server中指定运行dhcp的网口:

INTERFACESv4="veth0"

接着拉起即可systemctl start isc-dhcp-server。

这里我是装了个dhcp server并直接运行。很明显,dhcp server的地址是veth0的ip地址,也就是192.168.1.8。 这样一来下联设备就会感知软路由(设备上会记录DHCP Server是192.168.1.8,而不是192.168.1.1),属于强迫症不能忍系列,我们需要让下联设备看到的dhcp服务器也是192.168.1.1才够优雅。

6. 虚拟网关

如果想让客户端看到的DHCP Server也是网关地址,那么需要做稍微比较hack的处理。

从协议内容的角度来说,在DHCP响应中,会附带DHCP Server的“Identifier”,一般来说就是DHCP Server的ip地址。而DHCP Server的代码不在我手里,不改代码的情况下,代码中肯定是直接拿工作网口的地址,所以如果想要针对DHCP也做到透明,就必须让网卡绑定192.168.1.1。 从网络包的角度来说,众所周知,DHCP是包括广播的协议,由于广播的存在,我们没办法对一次DHCP请求响应建立一条conntrack,而本地的出包也没法走iptables snat,所以这个功能没法简单地用iptables实现。

为了解决这个问题,我们要虚拟出一个网关,并且要绑定网关地址(192.168.1.1)。

可能有人会说,不就是本地绑一个ip吗有什么难的。实际上问题比这复杂的多。

相信这两个常识大伙都有:

  1. 同一个二层内,不能有两张网卡绑定相同的ip,否则ARP会乱。
  2. 同一个二层内,不能有两张网卡使用相同的mac,否则交换机会乱。

而我们要做的事情是,在同一个二层内,有两张网卡,不但绑定相同的ip,还绑定相同的mac。

为了让网络不崩掉,首先我们需要知道交换机如何工作。

众所周知,我们平常看见的包的二层都是Ethernet,会有目的mac地址和源mac地址。交换机从一个网口port0收到一个包时,会检查它的源mac,并在mac表中记录“mac0对应了port0” 。接着交换机会检查包的目的mac,在mac表中查询目的mac对应的网口,比方说目的mac1对应了port1,那么这个包就会从port1发出。 一进一出一存一查,这样便完成了包的“交换”。

那么如果同一个网络下,有两台完全相同的设备(ip、mac均相同),我们会遇到什么问题呢?

在做ARP请求的广播时,两台设备都会收到广播,两台设备都会回复ARP响应。而响应的二层源mac,就是设备自身的mac。因为两台设备接在交换机的两个网口上,所以这两个设备会相互踢掉对方的mac表规则,导致指向这两台设备的包会不确定地到达这两台设备的其中一台上,流量会发生异常。 于是我们要在软路由上主动处理这种场景,让流出软路由的包不要受到影响:对于上联,由于软路由上联和光猫在同一个广播域,所以绝对不能向上联发送源mac是光猫mac的包;而对于下联则没有该限制,下联无法辨别 源mac是光猫mac的包 到底是从软路由上发出的 还是从光猫上发出的。

首先我们建一个netns和一对veth设备,当作“虚拟网关”使用。目前仅DHCP功能使用了“虚拟网关”,但是后续如果有其他需要也可以把相关功能加到这个netns中。

ip netns add gw
ip link add gw0 type veth peer name gw0-in-br
ip link set gw0-in-br up
ovs-vsctl add-port ovsbr0 gw0-in-br
ip link set gw0 netns gw
ip netns exec gw ip link set gw0 address 18:75:00:00:00:e8 # 抹去了mac信息
ip netns exec gw ip link set gw0 up
ip netns exec gw ip addr add 192.168.1.1/24 dev gw0

这样我们就有了一个“虚拟网关”了。这里ip是网关的ip,mac是网关的mac(网关的mac可以通过ip neigh查看)。现在我们要手动处理一下网关的流量。

首先我们目前在虚拟网关netns中,只会跑DHCP Server。其次我们也希望下联所有设备都在虚拟网关中处理DHCP。那么我们需要添加几条流表:对于所有DHCP请求,都发往虚拟网关。

ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    udp,tp_src=68,tp_dst=67,
    actions=output:gw0-in-br
ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    udp,tp_src=67,tp_dst=68,
    actions=output:enp1s0 # downlink

这里对于所有DHCP Server发出的包,都送往下联,不考虑软路由内部的各个veth口。这是因为我们的软路由里面所有的ip都是静态ip,无需DHCP。

对于客户端查询192.168.1.1的ARP请求,可以一律送往上联,此时由上联返回它自己的mac地址。 这样,对于上联光猫来说,它可以看到客户端发来的ARP,可以看到客户端的mac地址,刷新它的neighbor表。

ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    arp,nw_dst=192.168.1.1,
    actions=output:enp5s0 # downlink

在DHCP Server进行响应时,是需要单播的。而单播就必须通过ARP获取ip对应的mac地址(也许DHCP Server实现时可以直接拿客户端发过来的mac地址作为目的mac,但是也有可能只是开UDP口,不感知底下的Ethernet头,这种情况就会发ARP)。 所以现在会有两个192.168.1.1发起的ARP请求,一个由光猫发出从上联进来,一个由netns:gw 发出,从gw0-in-br虚拟网口进来。并且ARP请求是一个广播。 这里我们必须手动处理源mac是网关mac的广播包。因为同一个mac不能让它出现在不同的交换机口上,所以我们要把源mac是网关mac的广播包发往“除了上联和gw0之外的所有网卡” 。

ovs-ofctl add-flow ovsbr0 table=0,priority=1000,
    dl_dst=ff:ff:ff:ff:ff:ff,dl_src=192.168.1.1,
    actions=output:enp1s0,output:veth0-in-br,output:vp-veth0-in-br
#                  donwlink      local              vproxy

既然有发就有收。当看到一个回复给网关的arp包时,我们不能确定这个响应包对应的请求是从网关发出的还是从我们的虚拟网关发出的。所以干脆把包复制两份,两个网口都发出去。

ovs-ofctl add-flow ovsbr0 table=0,priority=1000,
    arp,dl_dst=18:75:00:00:00:e8, # 抹去了mac信息
    actions=output:enp5s0,output:gw0-in-br
#           uplink        virtual gw

对于其他非ARP、非DHCP的包,如果目的mac地址是网关mac,那么就应该送到网关去,也就是应当从上联网口出去。我们配置一个低优先级流处理这些包。

ovs-ofctl add-flow ovsbr0 table=0,priority=200,
    dl_dst=18:75:00:00:00:e8, # 抹去了mac信息
    actions=output:enp5s0 # uplink

除此之外,我们还需要配置一些兜底的“安全规则”,彻底防止源mac是网关mac的包被发往网关或者虚拟网关。前面已经处理过源mac是网关mac的广播了,这里再处理一下单播即可。

ovs-ofctl add-flow ovsbr0 table=0,priority=1000,
    dl_src=18:75:00:00:00:e8, # 抹去了mac信息
    dl_dst=18:75:00:00:00:e8, # 抹去了mac信息
    actions=drop

到此我们关于“虚拟网关”的配置已经完成了。接下来我们需要在netns:gw里面启动dhcp server。

isc-dhcp-server 的启动配置位于/etc/init.d/isc-dhcp-server。在其start分支启动代码更改为如下内容

sleep 2s
/usr/sbin/ip netns exec gw start-stop-daemon --start ... # 后面省略

这里之所以要sleep 2s,是因为dhcp server的启动和我们配置网卡的脚本是同时执行的,通过简单的sleep就可以保证网卡配置早于dhcp server启动。

此外,网卡配置也要改一下,改成我们的gw0。

vim /etc/default/isc-dhcp-server
INTERFACESv4="gw0"

7. 其他配置

到此我们已经完成了所有预期功能的流表配置。但除此之外最好增加一些兜底容错流表,防止后续修改时改出问题。

首先是DHCP。光猫上的DHCP没有禁用,为了完全透明,我们不禁用光猫的DHCP,而是在OVS中配置流表丢掉光猫上的DHCP包。

ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    udp,in_port=enp5s0,tp_src=67,tp_dst=68,\
    actions=drop
ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    udp,in_port=enp5s0,tp_src=68,tp_dst=67,\
    actions=drop

此外我们还在netns:vproxy中处理了指向192.168.1.1的包。为了防止今后配置时误配ip或者local路由,我们最好把这个口发出的192.168.1.1arp禁用,以绝后患。

ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    arp,in_port=vp-veth0-in-br,nw_src=192.168.1.1,\
    actions=drop

由于电信没给我分配公网ipv6,我们干脆也把ipv6全禁用好了。

ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    ipv6,\
    actions=drop

8. 其他软件

为了更方便地操作图形界面,我装了一个VNC Server和一个NoVNC。其中VNC Server只跑在本地,NoVNC通过https暴露安全连接,在网页即可安全地使用图形界面了。

装起来也是比较简单。

apt-get install x11vnc novnc python-websockify

简单的配置一下即可。配置我都放在附录里了可供参考。

尾声

现在我们已经搭起了一个完整的软路由。这个软路由接在光猫和家庭设备之间,并且对光猫和家庭设备都透明:光猫感知不到软路由的存在,家庭设备也感知不到软路由的存在,但是DHCP、DNS和一部分需要处理的流量,皆由软路由接管。

这里我们用到的技术也比较简单,规则都比较直接。除了虚拟网关那里稍微有点hack,其他都是普普通通常见的技术:OVS、veth、netns、iptables,在大部分K8S环境中都在广泛使用。这个软路由的搭建经验也可以直接用于其他SDN领域。有用的经验又增加了!

附录

不使用OVS的搭建思路

把设备当作路由,打开ip_forward,上下联网口都配上ip。

由于没有OVS,所以上下联被划分为两个广播域,对于ARP、DHCP等广播信息就必须做中继。不过好在也有成熟的方案可用。

上联要配死/32的静态路由指向光猫,默认路由也是走上联,而广播域要走下联。

对于DNS,一条Prerouting链里配一条Redirect即可导入本地,和本文的方式一样处理;本文使用的虚拟网段也是直接按文中所述配置即可。

对于DHCP,理论上应该也可以实现,不过我没试过。首先开一个netns,挂一个veth pair,一端在ns里一端在ns外。ns里绑192.168.1.1/32,只配置一条/32的静态路由和一条默认路由,指向ns外面的ip。然后在ns里配置静态arp表,把外面的ip固定下来。这样就无需发送arp查询即可发包了。而ns外面也类似的,配一条路由和一条静态arp表。这里注意,路由和arp表里的ip最好不要写本网络(192.168.1.0/24)网段下的,而是随便选一个假ip,比如100.64.1.2。 大概如下:

# ns 外面
ip route add 100.64.1.2/32 dev veth0
arp -s 100.64.2.1 "00:00:00:00:00:02" # 这里配ns里面网口的mac地址

# ns 里面
ip route add 100.64.2.1/32 dev veth1
arp -s 100.64.1.2 "00:00:00:00:00:01" # 这里配ns外面网口的mac地址

然后在ns外面开dhcp中继,运行于下联网口上,把dhcp包中转给100.64.2.1。由于中继包都是单播,所以可以做conntrack,也就可以很方便地做NAT。 此外,netns里必须关闭arp,否则可能会扰乱neighbor表。

总结一下,不加上OVS,规则复杂度高、所有包都要走协议栈过一圈做三层转发性能差、配规则比较hack不简洁。唯一的好处可能就是网卡不需要托管给OVS,可以用系统内置的可视化网络界面做配置。

配置汇总

启动脚本

/etc/rc.local

#!/bin/bash

LOGGER="/usr/bin/logger"

function execute() {
  SCRIPT="$1"
  "$LOGGER" "==============================="
  "$LOGGER" "executing $SCRIPT"
  OUTPUT=`"$SCRIPT" 2>&1`
  EXIT_CODE=$?
  "$LOGGER" "$SCRIPT exit code $EXIT_CODE"
  "$LOGGER" "$OUTPUT"
  "$LOGGER" "==============================="
}

"$LOGGER" 'rc-local BEGIN'

execute /root/netscripts/init-nics.sh
execute /root/netscripts/init-flow.sh
execute /root/netscripts/init-vproxy0.sh
execute /root/netscripts/init-software.sh

"$LOGGER" 'rc-local END'

exit 0

/usr/lib/systemd/system/rc-local.service

[Unit]
Description=/etc/rc.local Compatibility
Documentation=man:systemd-rc-local-generator(8)
ConditionFileIsExecutable=/etc/rc.local
After=network.target

[Service]
Type=forking
ExecStart=/etc/rc.local start
TimeoutSec=0
RemainAfterExit=yes
GuessMainPID=no

DHCP Server

/etc/dhcp/dhcpd.conf

default-lease-time 86400;
max-lease-time 86400;

subnet 192.168.1.0 netmask 255.255.255.0 {
  range  192.168.1.65 192.168.1.191;
  option routers 192.168.1.1;
  option domain-name-servers 192.168.1.1;
}

/etc/default/isc-dhcp-server

INTERFACESv4="gw0"

/etc/init.d/isc-dhcp-server

# 修改start_daemon函数
sleep 2s
/usr/sbin/ip netns exec gw start-stop-daemon --start ... # 省略

网络配置

注意,这里mac信息作了抹去处理。

/root/netscripts/init-nics.sh

#!/bin/bash

IP="/usr/sbin/ip"
IPTABLES="/usr/sbin/iptables"

LOCAL_UP_IP="192.168.1.8"
LOCAL_DOWN_IP="192.168.1.7"
VPROXY_LOCAL="192.168.1.9/24"
VETH_UP_LOCAL="$LOCAL_UP_IP/24"
VETH_DOWN_LOCAL="$LOCAL_DOWN_IP/24"
UPLINK="enp5s0"
UPLINK_LOCAL="$LOCAL_UP_IP/32"
DOWNLINK="enp1s0"
DOWNLINK_LOCAL="$LOCAL_DOWN_IP/32"
GW="192.168.1.1"

"$IP" link set "$UPLINK" up
"$IP" link set "$DOWNLINK" up

"$IP" addr add "$UPLINK_LOCAL" dev "$UPLINK"
"$IP" addr add "$DOWNLINK_LOCAL" dev "$DOWNLINK"

echo "" > /etc/resolv.conf
echo "nameserver $GW" >> /etc/resolv.conf

"$IP" link add veth0 type veth peer name veth0-in-br
"$IP" link set veth0 address 5c:00:00:00:00:7d
"$IP" link set veth0 up
"$IP" link set veth0-in-br up
"$IP" addr add "$VETH_UP_LOCAL" dev veth0
"$IP" route add default via "$GW"

"$IP" link add veth1 type veth peer name veth1-in-br
"$IP" link set veth1 address 5c:00:00:00:00:7c
"$IP" link set veth1 up
"$IP" link set veth1-in-br up
"$IP" addr add "$VETH_DOWN_LOCAL" dev veth1

"$IP" netns add vproxy
"$IP" netns exec vproxy "$IP" link set lo up
"$IP" link add vp-veth0 type veth peer name vp-veth0-in-br
"$IP" link set vp-veth0-in-br up
"$IP" link set vp-veth0 netns vproxy
"$IP" netns exec vproxy "$IP" link set vp-veth0 address d6:00:00:00:00:bf
"$IP" netns exec vproxy "$IP" link set vp-veth0 up
"$IP" netns exec vproxy "$IP" addr add "$VPROXY_LOCAL" dev vp-veth0
"$IP" netns exec vproxy "$IP" route add default via "$GW"

# handle virtual gw
"$IP" link add gw0 type veth peer name gw0-in-br
"$IP" link set gw0-in-br up
"$IP" netns add gw
"$IP" link set gw0 netns gw
"$IP" netns exec gw "$IP" link set gw0 address 18:00:00:00:00:e8
"$IP" netns exec gw "$IP" link set gw0 up
"$IP" netns exec gw "$IP" addr add 192.168.1.1/24 dev gw0

/root/netscripts/init-flow.sh

#!/bin/bash

OF="/usr/bin/ovs-ofctl"

GW="192.168.1.1"
GW_MAC="18:00:00:00:00:e8"
UPLINK="enp5s0"
DOWNLINK="enp1s0"
VIRTUAL="vp-veth0-in-br"
VIRTUAL_MAC="d6:00:00:00:00:bf"
LOCAL="veth0-in-br"
VGW="gw0-in-br"

# clear all flows
"$OF" del-flows ovsbr0

# forbid ipv6 packets, we do not handle them currently
"$OF" add-flow ovsbr0 "table=0,priority=1000,ipv6,action=drop"
# prevent dl_src=gw_ mac packets go to gw
"$OF" add-flow ovsbr0 "table=0,priority=1000,dl_src=$GW_MAC,dl_dst=$GW_MAC,action=drop"
# specify gw broadcast output nics
"$OF" add-flow ovsbr0 "table=0,priority=1000,dl_dst=ff:ff:ff:ff:ff:ff,dl_src=$GW_MAC,action=output:$DOWNLINK,output:$LOCAL,output:$VIRTUAL"
# arp response to gw should go to both uplink and vgw
"$OF" add-flow ovsbr0 "table=0,priority=1000,arp,dl_dst=$GW_MAC,action=output:$UPLINK,output:$VGW"
# arp for gw go to uplink
"$OF" add-flow ovsbr0 "table=0,priority=999,arp,nw_dst=$GW,action=output:$UPLINK"
# dhcp req go to vgw
"$OF" add-flow ovsbr0 "table=0,priority=1000,udp,tp_src=68,tp_dst=67,action=output:$VGW"
# dhcp resp go to donwlink
"$OF" add-flow ovsbr0 "table=0,priority=1000,udp,tp_src=67,tp_dst=68,action=output:$DOWNLINK"
# forbid dhcp from uplink
"$OF" add-flow ovsbr0 "table=0,priority=1000,in_port=$UPLINK,udp,tp_src=67,tp_dst=68,action=drop"
"$OF" add-flow ovsbr0 "table=0,priority=1000,in_port=$UPLINK,udp,tp_src=68,tp_dst=67,action=drop"
# prevent virtual network sending out gw arp packets
"$OF" add-flow ovsbr0 "table=0,priority=1000,in_port=vp-veth0-in-br,arp,nw_src=$GW,action=drop"
# redirect dns requests to virtual network
"$OF" add-flow ovsbr0 "table=0,priority=499,udp,nw_dst=$GW,tp_dst=53,action=set_field:$VIRTUAL_MAC->dl_dst,output:$VIRTUAL"
# dns requests from virtual network go to gw (this is required because the priority=499 flow redirects all dns traffic)
"$OF" add-flow ovsbr0 "table=0,priority=500,in_port=vp-veth0-in-br,udp,nw_dst=$GW,tp_dst=53,action=output:$UPLINK"
# flow to virtual network
"$OF" add-flow ovsbr0 "table=0,priority=500,ip,nw_dst=100.96.0.0/11,action=set_field:$VIRTUAL_MAC->dl_dst,output:$VIRTUAL"
# normal flow to gw should go through uplink
"$OF" add-flow ovsbr0 "table=0,priority=200,dl_dst=$GW_MAC,action=output:$UPLINK"
# ensure normal flow exists
"$OF" add-flow ovsbr0 "table=0,priority=0,actions=NORMAL"

/root/netscripts/init-vproxy0.sh

#!/bin/bash

IP="/usr/sbin/ip"

"$IP" netns exec vproxy /root/netscripts/init-vproxy.sh

/root/netscripts/init-vproxy.sh

#!/bin/bash

IP="/usr/sbin/ip"

"$IP" netns exec vproxy /root/netscripts/init-vproxy.sh
root@wkgcass-LZX:~/netscripts# cat init-vproxy.sh
#!/bin/bash

IP="/usr/sbin/ip"
IPTABLES="/usr/sbin/iptables"

NETWORK="100.96.0.0/11"

"$IP" route add local "$NETWORK" dev lo src 127.0.0.1
"$IPTABLES" -t mangle -I PREROUTING -d "$NETWORK" -p tcp -j TPROXY --on-port=8888 --on-ip="127.0.0.1"
"$IPTABLES" -t nat -I PREROUTING -d 192.168.1.1 -p udp --dport 53 -j REDIRECT --to-port 53

source /root/.bashrc
HOME=root pm2 start /root/vproxy/vproxy.pm2.json

软件

/root/netscripts/init-software.sh

#!/bin/bash

/usr/bin/websockify -D --web=/usr/share/novnc/ --cert=/etc/novnc/novnc.pem 6080 --ssl-only localhost:5900

/lib/systemd/system/x11vnc.service

[Unit]
Description=x11vnc
After=multi-user.target

[Service]
Type=simple
ExecStart=/usr/bin/x11vnc -auth guess -forever -loop -noxdamage -repeat -rfbauth /etc/x11vnc/pass -rfbport 5900 -shared -listen 127.0.0.1 -no6

[Install]
WantedBy=multi-user.target

生成证书和私钥:

openssl req -x509 -nodes -newkey rsa:2048 -keyout novnc.pem -out novnc.pem -days 365