网络应用介绍及原理

NAPT介绍

NAT是将IP数据包头中的IP地址转换为另一个IP地址的过程。在实际应用中,NAT主要用于实现私有网络访问公共网络的功能。这种通过使用少量的公有IP地址代表较多的私有IP地址的方式,将有助于减缓可用IP地址空间的枯竭。

NAT的基本工作原理是当私有网主机和公共网主机通信的IP包经过NAT网关时,将IP包中的源IP或目的IP在私有IP和NAT的公共IP之间进行转换。

由于NAT实现是私有IP和NAT的公共IP之间的转换,那么,私有网中同时与公共网进行通信的主机数量就受到NAT的公共IP地址数量的限制。为了克服这种限制,NAT被进一步扩展到在进行IP地址转换的同时进行Port的转换,这就是网络地址端口转换NAPT(Network Address Port Translation)技术。NAPT也被称为“多对一”的NAT,或者叫PAT(Port Address Translations,端口地址转换)、地址超载(address overloading)。

NAPT与NAT的区别在于,NAPT不仅转换IP包中的IP地址,还对IP包中TCP和UDP的Port进行转换。这使得多台私有网主机利用1个NAT公共IP就可以同时和公共网进行通信。

防火墙介绍

防火墙是一种通过基于一组用户定义的规则过滤传入和传出网络流量来提供网络安全性的系统。通常,防火墙的目的是减少或消除不需要的网络通信的发生,同时允许所有合法通信自由流动。在大多数服务器基础架构中,防火墙提供了一个重要的安全层,与其他措施相结合,可以防止攻击者以恶意方式访问服务器。

有三种基本类型的网络防火墙:包过滤(无状态),有状态和应用层。

数据包过滤或无状态防火墙通过隔离检查单个数据包来工作。因此,他们不知道连接状态,并且只能根据各个数据包标头允许或拒绝数据包。

状态防火墙能够确定数据包的连接状态,这使得它们比无状态防火墙更灵活。它们通过收集相关数据包来工作,直到可以在将任何防火墙规则应用于流量之前确定连接状态。

应用程序防火墙通过分析传输的数据更进一步,这使得网络流量可以与特定于各个服务或应用程序的防火墙规则相匹配。这些也称为基于代理的防火墙。

除了在所有现代操作系统上都可用的防火墙软件之外,防火墙功能还可以由硬件设备提供,例如路由器或防火墙设备。

DHCP中继介绍

DHCP使用了租约的概念,或称为计算机IP地址的有效期。租用时间是不定的,主要取决于用户在某地连接Internet需要多久,这对于教育行业和其它用户频繁改变的环境是很实用的。透过较短的租期,DHCP能够在一个计算机比可用IP地址多的环境中动态地重新配置网络。DHCP支持为计算机分配静态地址,如需要永久性IP地址的Web服务器。

在大型的网络中,可能会存在多个网段。DHCP客户机通过网络广播消息获得DHCP服务器的响应后得到IP地址。但广播消息是不能跨越网段的。因此,DHCP客户机和服务器在不同的网段内时,客户机向服务器申请IP地址就要用到DHCP中继代理。DHCP中继代理实际上是一种软件技术,安装在DHCP中继代理的设备(路由器,交换机,服务器)称为DHCP中继代理服务器,它承担不同网段间的DHCP客户机和服务器的通信任务。

DHCP客户端启动并进行DHCP初始化时,它在本地网络广播配置请求报文。如果本地络存在DHCP服务器,则可以直接进行DHCP配置,不需要DHCP中继;如果本地网络没有DHCP服务器,则与本网络相连的且带DHCP中继功能的网络设备收到该广播报文后,进行适当的处理并转发给指定的在其它网络上的DHCP服务器。DHCP服务器根据客户端提供的信息进行相应的配置,并通过DHCP中继将配置信息发送给客户端,完成对客户端的动态配置。

VRRP介绍

现网中的主机使用缺省网关与外部网络联系时,如果Gateway出现故障,与其相连的主机将与外界失去联系,导致业务中断。VRRP的出现很好地解决了这个问题。VRRP将多台设备组成一个虚拟设备,通过配置虚拟设备的IP地址为缺省网关,实现缺省网关的备份。当网关设备发生故障时,VRRP机制能够选举新的网关设备承担数据流量,从而保障网络的可靠通信。如下图所示,当Master设备故障时,发往缺省网关的流量将由Backup设备进行转发。

VRRP备份组示意图

应用服务器介绍

Web服务器,是指驻留于因特网上某种类型计算机的程序。当Web浏览器(客户端)连到服务器上并请求文件时,服务器将处理该请求并将文件发送到该浏览器上,附带的信息会告诉浏览器如何查看该文件(即文WEB服务器件类型)。服务器使用HTTP(超文本传输协议)进行信息交流,这就是人们常把它们称为HTTPD服务器的原因。

Web服务器不仅能够存储信息,还能在用户通过Web浏览器提供的信息的基础上运行脚本和程序。

FTP服务器,全称File Transfer Protocol Server,是在互联网上提供文件存储和访问服务的计算机,它们依照FTP协议提供服务。FTP,文件传输协议(File Transfer Protocol)是用于在网络上进行文件传输的一套标准协议,使用客户/服务器模式。FTP是专门用来传输文件的协议。

FTP是一个客户机/服务器系统,用户通过使用一个支持FTP协议的客户端,连接到远程主机上的服务器程序上。用户在客户端发出命令,远程主机服务器接收到命令后执行用户所发出的命令,同时将执行结果返回到客户端。简单来说,就是用户对服务器发出一条命令,要求服务器向用户发送一份文件,服务器响应并发送文件到客户端,用户收到文件将其放置于用户工作目录中,这一过程就是FTP服务器进行的文件交流。

GRE隧道介绍

GRE 是在网络上建立直接点对点连接的一种方法,目的是简化单独网络之间的连接。它适用于各种网络层协议。将数据包封装在其他数据包中称为“隧道”。

GRE 隧道通常配置在两个路由器之间,每个路由器的作用好比隧道的一端。路由器设置为彼此直接发送和接收 GRE 数据包。两个路由器之间的任何路由器都不会打开封装的数据包;它们仅引用封装数据包外层的标头进行转发。

方案设计

NAPT设计

(一)需求分析

我们设计的网络中,为了节约公有地址的使用,因此我们选择利用NAPT技术对于我们的私网地址做一层转换,将我们的所有私有地址访问外网的时候,映射成同一个公有地址,实现地址的共享。并且利用NAPT技术也可以实现我们私有地址安全的进一步提升。而NAPT技术为了实现共用同一个公有地址,需要记录并改变报文的IP和TCP/UDP的端口号。因此我们需要利用流表项对于我们报文的IP和端口号进行改变。

并且我们发现在一些公司当中,会将服务器的IP暴露出来供他人进行ICMP访问,而我们这里并没有公网地址给我们的服务器使用,因此,我们需要也对我们的ICMP报文进行类似的NAPT技术应用,实现我们的ICMP报文的NAPT映射,而其中我们发现ICMP报文中标识这个参数可以进行修改,从而办到ICMP的地址映射技术。

(二)设计思路

在我们设计的网络中,将S7作为我们的NAPT地址映射的节点,在该节点下发流表实现我们的地址映射操作。并且由于NAPT需要一个公网地址,因此我们将我们的虚拟机网卡绑定在S7的一个端口上,并将该网卡的IP地址作为我们最终映射成的地址。由于我们采用的SDN网络架构,因此我们选择利用控制器编程实现地址映射,以及对应的流表下发功能。

其中我们所设计的NAPT流程可以划分为2种情况,一种是从外网接收发向内网的报文,一种是内网访问外网。

其中由内网发向外网流程如下所述:

  1. 在网关接收到一个报文后,判断其是否有对应的流表进行匹配,如有匹配项,按照匹配进行转发。如果没有则发往控制器。
  2. 控制器判断目的地址是否为外网,如果不是则按照内网寻路和转发的方式进行报文转发。如果是则查看arp表是否记录虚拟机的网关,如果没有记录,则缓存报文,并向虚拟机发起arp请求。
  3. 当控制器有虚拟机网关mac信息后,根据NAPT映射方法,计算当前报文的源信息的映射情况,并寻路,随后下发NAPT映射流表,mac地址更改流表和路报文发流表。
  4. 根据映射结果,将报文的IP和端口号进行修改(如果为ICMP报文则为标识),并修改mac地址,最终将其发送到虚拟机网关。

内网发向外网NAPT流程图

其中由内网发向外网流程如下所述:

  1. 在网关接收到一个报文后,判断其是否有对应的流表进行匹配,如有匹配项,按照匹配进行转发。如果没有则发往控制器。
  2. 控制器接收报文之后判断是否为目的地址和源地址都是外网,如果不是,则按照内网转发或者是发往外网的流程进行处理。如果是则判断目的地址和端口号是否有地址映射对应(如果是ICMP报文则为标识),如果没有记录则丢弃报文,如果有记录则对报文进行地址映射,将报文地址映射成内网地址。
  3. 判断是否为arp报文,如果是则记录其信息。如果不是则判断该报文映射后目的地址是否有arp记录,如果没有记录则缓存并进行arp请求。如果有则修改报文目的mac地址,随后下发NAPT映射流表,mac地址修改流表,最终按照内网转发报文的流程进行处理。

外网发向内网NAPT流程图

在该过程当中我们的NAPT的IP和端口映射算法如下所述:

  1. 在控制器种我们首先利用一个映射表记录我们的NAPT映射情况。
  2. 随后将IP映射成虚拟机网卡的IP地址,端口首先映射成自己本身,检查是否这个映射的端口号是有记录的,如果有记录则将自己的映射端口号加一,直到没有记录过为止,随后记录该映射。

并且在过程中涉及到流表项的下发,其中由内网发向外网流表如下所述:

  1. 路径中的第一个openflow交换机:匹配为报文的目的IP地址,且目的mac为网关mac地址;作用为修改报文的mac地址为其虚拟机网关的mac地址以及转发该报文。
  2. 路径中的中间openflow交换机:匹配为报文的mac地址为虚拟机网关的mac地址(避免重复下发);作用为转发报文。
  3. 路径中的最后一个openflow交换机(S7):匹配为报文的IP地址为外网记录的的地址;作用为修改源IP地址和端口号做NAPT映射(如果是ICMP报文则为ICMP报文标识)和转发报文。

另外,由外网发向内网流表如下所述:

  1. 路径中的第一个openflow交换机:匹配为报文的目的IP地址为内网主机,且端口为指定端口;作用为修改报文的目的IP地址和端口号做NAPT映射(如果是ICMP报文则为ICMP报文标识)并修改mac地址为主机对应的mac地址,和实现报文转发。
  2. 路径中的后续openflow交换机:匹配为报文的目的IP地址;作用为转发报文。

对于上述需求分析中,我们提及的ICMP报文的情况。因此我们设计的NAPT是包含了对于ICMP报文的处理,但ICMP报文并没有传输层的端口号,因此我们选择利用其ICMP报文中的标识进行我们的映射如下图所示,由于一次ping操作中连续的ICMP的ICMP报文标识是相同的,因此我们选择将短时间内记录ICMP标识和IP地址的映射情况,从而类比TCP报文的端口实现其NAPT功能。

ICMP报文中的标识

并且对于我们的服务器需要供外网进行访问,因此,我们需要提前将我们的服务器IP地址和我们的对应的端口号进行NAPT地址映射的记录。

防火墙设计

(一)需求分析

在本组设计的SDN网络中,存在一个服务器网段与一个访客网段。因此我们选择配置防火墙对数据的传输进行过滤,以达成内外网的隔离及对服务器集群的保护。其中主要考虑在禁止访客网络和部分内部网络访问ftp服务器,以及允许服务器级群向外发送信息。同时还可以通过防火墙的设置对服务器的安全性进行一定提升。

(二)设计思路

由于本组的SDN网络是基于mininet和ryu控制器实现,所以选择使用ryu控制器自带的rest_firewall进行防火墙的实现。本方法对数据包的过滤是基于路由器的流表项,因此我们选择将控制器放在服务器集群的s11上便于专门对服务器的防火墙配置。

ryu的rest_firewall在开启后默认为禁止所有包的传输,因此我们要为这个交换机制订相应的规则打通数据传输的隧道。此处我们组制订的思路如下:

  1. 允许所有源地址为192.168.4.0网段的报文通过,使服务器集群的数据能正常流出。
  2. 允许所有目的地址为web服务器,端口为80的报文通过,这样设置在访问web服务器时只能通过80端口,避免端口扫描等。
  3. 禁止icmp报文的通过,以此避免ddos攻击使服务器负载过大。
  4. 为部分内部网络开通访问ftp服务器,避免外网以及无权限内网访问ftp服务器。

防火墙流程图

DHCP中继设计

(一)需求分析

在我们设计的网络中,分布着多个需要DHCP服务器分配IP地址的子网,而为了节省网络中搭建多个DHCP服务器造成的开支,我们选择在网络的汇聚层上搭建一个DHCP服务器复制分配多个子网的IP地址分配。这种网络搭建方案导致DHCP服务器和DHCP客户机位于不同网段;因此需要引入DHCP中继技术,将接入层的SDN交换机配置为DHCP中继代理,由其负责与DHCP服务器进行交互完成所属子网DHCP客户机的IP地址分配,最终实现网络中一个DHCP服务器完成不同子网IP地址的分配。

(二)设计思路

主干网拓扑图

如主干网拓扑图所示,在我们设计的网络中,DHCP服务器与汇聚层交换机s4、s5相连;需要DHCP服务的三个主机网络为与s1、s2、s3相连,所属网段分别为192.168.1.0/24、192.168.2.0/24、192.168.3.0/24。为了实现DHCP中继,我们将s1、s2、s3指定为DHCP中继代理交换机,并分别将其与二层交换机s8、s9、s10相连的接口IP设置为相应子网网关192.168.1.254、192.168.2.254、192.168.3.254。

由于我们采用的网络架构为SDN,该架构下的转发设备在路由转发过程中不是基于传统的路由表匹配而是基于流表项匹配,而传统的开源DHCP中继技术方案都是基于路由表的转发方式,因此此处无法采用传统开源方案配置。由此我们选择自主设计相应的SDN控制器,由控制器对报文进行修改来实现我们预期的DHCP中继功能。

以S1所属的子网主机实现DHCP服务为例分析,在SDN网络中实现DHCP中继的IP地址分配/租赁过程思路如下:

  1. 当h1发送DHCP Discover/Request报文时,会将DHCP Discover/Request报文广播发送到s1。
  2. 当s1收到DHCP Discover/Request报文后会将报文上发到SDN控制器处理。
  3. 当控制器收到s1上发的DHCP Discover/Request报文后,会查询所记录的s1对应的IP地址并替换报文中DHCP协议的giaddr字段;并修改以太网协议的源MAC为s1与二层交换机相连端口的MAC及目的MAC为所记录的DHCP服务器MAC地址(此处使用随机算法,随机选择DHCP服务器与s4、s5连接的其中一条链路发送,实现DHCP服务器的负载均衡)、IP协议的源IP为s1对应的IP地址及目的IP为DHCP中继服务器对应的IP地址;最终将修改完成的报文送往随机选定的DHCP服务器端口。
  4. 当DHCP服务器收到请求报文后,会在其预先配置的多个DHCP地址池中选择与报文中DHCP协议的giaddr字段对应的地址的地址池,并返回相应信息(IP信息、网关、租期等等)。
  5. 当s4或s5收到DHCP服务器发送的DHCP Offer/Reply报文时,会将该报文上发给控制器。
  6. 当控制器收到s4或s5上发的DHCP Offer/Reply报文后,会根据报文中DHCP协议的giaddr字段查询对应的DHCP中继代理交换机及该交换机与二层交换机连接的端口;并修改报文中以太网协议的源MAC为对应的DHCP中继代理交换机与二层交换机相连端口的MAC地址及目的MAC为所记录的DHCP服务器MAC地址、IP协议的源IP为对应的DHCP中继代理交换机与二层交换机相连端口的的IP地址及HCP协议的yiaddr字段;最终将修改完的报文从对应的DHCP中继代理交换机与二层交换机连接的端口送出,即送回原来的子网,完成DHCP服务的交互。

对于IP地址更新流程和IP地址释放流程的思路与IP地址分配/租赁过程几乎一致,同样是将报文上发到控制器并修改报文,最终将报文发送到指定的位置实现DHCP服务。

DHCP中继流程图

应用服务器设计

(一)需求分析

在我们设计的网络中,为了更好为内部网络和外界提供服务,我们设置了一个服务器集群提供各种服务。其中设置了Web服务器用于提供门户网站、外界宣传等服务,设置FTP服务器用于为内部员工提供重要文件共享和传输服务。在Web服务器的搭建中,为了保障Web服务器的安全及实现负载均衡,我们使用了反向代理的机制,使用两台反向代理服务器用于代理Web服务器集群。

(二)设计思路

在我们设计的网络中,所有的主机和SDN交换机都是基于Mininet平台的虚拟设备,而对我们的服务器而言,为了更好的实现预期的服务功能,我们需要使用真实的虚拟机来配置。其中如何连接基于Mininet平台的拓扑和作为服务器的虚拟机是一个关键的问题。

针对这个问题,我们使用与课程实验平台类似的搭建方式,即在VMware中创建多个虚拟网络VMnet,并在Mininet拓扑所在的虚拟机中新建连接对应VMnet网络的网卡,将该网卡与拓扑中连接服务器的二层交换机连接,最终实现Mininet拓扑与真实虚拟机的联通。

其次是Web服务器的反向代理机制,我们在网络中配置了两个反向代理服务器及两个Web服务器用来模拟服务器集群。为了保障Web服务器的安全,我们设计由反向代理服务器与拓扑中的二层交换机连接,Web服务器只能反向代理服务器范围。对此我们需要在每个反向代理服务器配置两张网卡,一张用于连接拓扑中的二层交换机,另一张用于连接Web服务器;并在反向代理服务器中安装nginx,使用nginx的代理机制将特定IP地址(反向代理服务器的IP地址)的访问映射到我们指定的IP地址(Web服务器的IP地址)去,实现基于nginx的反向代理。同时可以配置代理映射地址池,将特定IP地址映射到指定的地址池中,按权重选择某一IP地址访问实现Web服务器的负载均衡。

服务器搭建流程

服务器访问流程

基于VRRP协议的高可用设计

(一)需求分析

在我们设计的网络中,Web服务器的配置采用反向代理的机制,但当反向代理服务器出现异常时,就会出现Web服务器无法访问的情况。为了应对这种情况,我们使用了两台反向代理服务器做主备服务器。两台主机通过VRRP协议组成一个服务器组,虚拟化为一台反向代理服务器,拥有对应的虚拟IP地址,当一台服务器出现异常时,另一台服务器可以通过VRRP协议检测到异常,并接替另一台服务器的虚拟IP地址继续完成服务,保障服务器组高可用。

常见基于VRRP协议的高可用配置有主从配置和双主配置, 主从配置中两台反向代理服务器共用一个虚拟IP地址,同一时刻只有一台主服务器工作,另一台备份服务器在主服务器不出现故障的时候,永远处于浪费状态;双主配置中两台反向代理服务器互为主备服务器,同一时刻两台服务器都在工作,当其中一台服务器出现故障时,两台服务器的请求都转移到一台服务器负担。出于充分利用服务器性能及增加访问负载能力等角度考虑,最终决定在我们的网络中采用主从配置实现高可用。

(二)设计思路

对于VRRP协议高可用配置的使用,我们决定使用现成比较成熟的技术方案配置,即keepalived软件。我们使用keepalived软件将两台位于同一子网并连通的反向代理服务器组成一个反向代理服务器组,并配置两台服务器互为主备服务器,分别设置相应的优先级、检测时延等基础参数,最终实现两台反向代理服务器之间的高可用服务。

GRE隧道设计

(一)需求分析

在我们设计的网络中,我们搭建了主干网和分支网,其中主干网和分支网分别位于不同的物理网络。为了保障主干网中内部资源与分支网的共享还有通信的安全性,我们计划在主干网和分支网之间搭建一条GRE隧道用于两个网络的内部连通,即通过隧道连接分支网的主机设备与主干网的服务器集群。分支网的主机既可以通过公网IP访问Web服务器也可以通过GRE隧道访问Web服务器;但主机只能通过GRE隧道访问FTP服务器,以保障内部资料的安全。

(二)设计思路

在我们设计的网络中,主干网和分支网位于不同的虚拟机中,对于GRE隧道的实现,我们需要搭建一条连通两个虚拟机中Miniet拓扑的GRE隧道。因为Linux系统支持系统级的GRE隧道配置,因此我们只需要在两个虚拟机间搭建一条系统级的GRE隧道连通相应网段,将拓扑中NAT交换机配置为隧道站点,当收到相应隧道报文时,将报文从NAT交换机与隧道相连的端口送出即可完成Mininet拓扑之间隧道的通信。

GRE隧道两端主机通信流程图

方案实现与结果分析

NAPT实现与分析

(一)实现

文本配置的读取并初始化控制器

文本配置中NAPT相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 # 读取NAT信息
self.nat_ip = content[0].replace('\n', '').replace(' ', '').split('=')[-1].split('/')[0]
print("-----------------------NAt_ip: {}---------------------".format(self.nat_ip))

# 读取NAT mask信息
self.nat_mask = content[0].replace('\n', '').replace(' ', '').split('=')[-1].split('/')[1]
print("-----------------------NAt_mask: {}---------------------".format(self.nat_mask))

# 读取NAT交换机编号信息
self.nat_switch_id = int(content[1].replace('\n', '').replace(' ', '').split('=')[-1])
print("-----------------------NAt_ip: {}---------------------".format(self.nat_switch_id))

# 读取NAT交换机端口信息
self.nat_switch_port = int(content[2].replace('\n', '').replace(' ', '').split('=')[-1])
print("-----------------------NAt_ip: {}---------------------".format(self.nat_switch_port))

# 读取网络网关信息
self.net_getaway_ip = content[3].replace('\n', '').replace(' ', '').split('=')[-1]
print("-----------------------NAt_ip: {}---------------------".format(self.net_getaway_ip))

# 读取网络信息
self.net_ip = content[4].replace('\n', '').replace(' ', '').split('=')[-1]
print("-----------------------Net_ip: {}---------------------".format(self.net_ip))

服务器初始映射

服务器初始映射

1
2
3
4
5
6
7
# 读取web 相关信息
self.serve_host = content[9].replace('\n', '').replace(' ', '').split('=')[-1].split(';')
for host in self.serve_host:
self.tcp_out[(host.split(',')[0].split(':')[0], int(host.split(',')[0].split(':')[1]))] = int(host.split(',')[1])
self.tcp_in[int(host.split(',')[1])] = (host.split(',')[0].split(':')[0], int(host.split(',')[0].split(':')[1]))
print("-----------------------server_host: {}---------------------".format(self.serve_host))

向外网发送报文做NAPT

当我们判断一个报文其目的地址为外网,源地址为内网的时候,他就是一个发往外网的报文,此时需要利用NAT_out函数进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def NAT_out(self, msg):
datapath = msg.datapath
in_port = msg.match['in_port']
pkt = packet.Packet(msg.data)
eth = pkt.get_protocols(ethernet.ethernet)[0]
parser = self.nat_switch.dp.ofproto_parser
pkt_ipv4 = pkt.get_protocol(ipv4.ipv4)
pkt_udp = pkt.get_protocol(udp.udp)
pkt_tcp = pkt.get_protocol(tcp.tcp)
pkt_icmp = pkt.get_protocol(icmp.icmp)

if pkt_icmp and pkt_icmp.type == icmp.ICMP_ECHO_REQUEST:
# 找到icmp未有记录
if (pkt_ipv4.src, pkt_icmp.data.id) not in self.icmp_out:
icmp_id = copy.copy(pkt_icmp.data.id)
# 使得变换后icmp不冲突
while icmp_id in self.icmp_in:
icmp_id += 1

self.icmp_out[(pkt_ipv4.src, pkt_icmp.data.id)] = icmp_id
self.icmp_in[icmp_id] = (pkt_ipv4.src, pkt_icmp.data.id)
self.ICMPTTLinit(icmp_id)

src_ip = copy.copy(pkt_ipv4.src)
actions = [parser.OFPActionOutput(self.nat_switch_port)]

# 修改mac
if self.net_getaway_ip in self.arp_table and not (
self.ipInSubnet(pkt_ipv4.dst, "/".join((self.nat_ip, self.nat_mask)))):
eth.dst = self.arp_table[self.net_getaway_ip][0]
elif pkt_ipv4.dst in self.arp_table:
eth.dst = self.arp_table[pkt_ipv4.dst][0]
else:
self.stor(msg)
return
src_id = pkt_icmp.data.id
# 修改id
pkt_icmp.data.id = self.icmp_out[(src_ip, pkt_icmp.data.id)]
self.ICMPTTLinit(pkt_icmp.data.id)
# 修改原ip
pkt_ipv4.src = self.nat_ip
self.logger.info(
"send a ICMP Packet to internet changed_ip is from {} id:{} to {} SNAT to {} id:{}\n".format(src_ip,src_id, pkt_ipv4.src,self.nat_ip,pkt_icmp.data.id))
pkt_icmp.csum = 0
pkt.serialize()
self.send_out(self.nat_switch, actions, pkt.data)
return

elif pkt_tcp:
# 找到icmp未有记录
src_port = copy.copy(pkt_tcp.src_port)
if (pkt_ipv4.src, pkt_tcp.src_port) not in self.tcp_out:
# 使得变换后icmp不冲突
while src_port in self.tcp_in:
src_port += 1

self.tcp_out[(pkt_ipv4.src, pkt_tcp.src_port)] = src_port
self.tcp_in[src_port] = (pkt_ipv4.src, pkt_tcp.src_port)

# sleep(1)
src_ip = copy.copy(pkt_ipv4.src)
actions = [parser.OFPActionOutput(self.nat_switch_port)]
origin_mac = eth.dst
# 修改mac
if self.net_getaway_ip in self.arp_table and not (
self.ipInSubnet(pkt_ipv4.dst, "/".join((self.nat_ip, self.nat_mask)))):
eth.dst = self.arp_table[self.net_getaway_ip][0]
elif pkt_ipv4.dst in self.arp_table:
eth.dst = self.arp_table[pkt_ipv4.dst][0]
else:
self.stor(msg)
return

# 修改port
pkt_tcp.src_port = self.tcp_out[(src_ip, pkt_tcp.src_port)]
# 修改原ip
pkt_ipv4.src = self.nat_ip
self.logger.info(
"send a TCP Packet to internet changed_ip is from {}:{} to {}:{} SNAT to {}:{}\n".format(src_ip, src_port, pkt_ipv4.dst,
pkt_tcp.dst_port, pkt_ipv4.src,pkt_tcp.src_port))
pkt_tcp.csum = 0
pkt.serialize()
self.send_out(self.nat_switch, actions, pkt.data)

elif pkt_udp:
# 找到icmp未有记录
src_port = copy.copy(pkt_udp.src_port)

if (pkt_ipv4.src, pkt_udp.src_port) not in self.udp_out:
# 使得变换后icmp不冲突
while src_port in self.udp_in:
src_port += 1

self.udp_out[(pkt_ipv4.src, pkt_udp.src_port)] = src_port
self.udp_in[src_port] = (pkt_ipv4.src, pkt_udp.src_port)

src_ip = copy.copy(pkt_ipv4.src)
actions = [parser.OFPActionOutput(self.nat_switch_port)]
origin_mac = eth.dst

# 修改mac
if self.net_getaway_ip in self.arp_table and not (
self.ipInSubnet(pkt_ipv4.dst, "/".join((self.nat_ip, self.nat_mask)))):
eth.dst = self.arp_table[self.net_getaway_ip][0]
elif pkt_ipv4.dst in self.arp_table:
eth.dst = self.arp_table[pkt_ipv4.dst][0]
else:
self.stor(msg)
return
# 修改id
pkt_udp.src_port = self.udp_out[(src_ip, pkt_udp.src_port)]
# 修改原ip
pkt_ipv4.src = self.nat_ip
self.logger.info(
"send to internet changed_ip is from {}:{} to {}:{} SNAT to {}:{}\n".format(src_ip, src_port, pkt_ipv4.dst,
pkt_udp.dst_port,self.nat_ip,pkt_udp.src_port))
pkt_udp.csum = 0
pkt.serialize()
self.send_out(self.nat_switch, actions, pkt.data)
else:
return
# 找路
shortest_path = self.topo.Dijkstra(
datapath.id, self.nat_switch_id, in_port, self.nat_switch_port
) len(shortest_path)))
assert len(shortest_path) > 0
path_str = ''
for s, ip, op in shortest_path:
path_str = path_str + "--{}-{}-{}--".format(ip, s, op)
self.logger.info("Configure the shortset path from {} to {} —— {}\n".format(src_ip, pkt_ipv4.dst, path_str))
self.configure_path(shortest_path, msg, origin_mac, eth.dst, pkt_ipv4.dst)

内网接收外网发送的报文做NAPT

当我们判断一个报文其目的地址为外网,源地址也是外网的时候,他就可能是一个外网发往内网的报文,此时需要利用NAT_in函数进行处理,当他在映射表中有记录的时候,那么就进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def NAT_in(self, msg):
datapath = msg.datapath
in_port = msg.match['in_port']
pkt = packet.Packet(msg.data)
eth = pkt.get_protocols(ethernet.ethernet)[0]
parser = self.nat_switch.dp.ofproto_parser
pkt_ipv4 = pkt.get_protocol(ipv4.ipv4)
pkt_udp = pkt.get_protocol(udp.udp)
pkt_tcp = pkt.get_protocol(tcp.tcp)
pkt_icmp = pkt.get_protocol(icmp.icmp)
if pkt_icmp and pkt_icmp.type == icmp.ICMP_ECHO_REQUEST and pkt_ipv4.dst==self.nat_ip:
icmp_reply = packet.Packet()
echo = pkt_icmp.data
echo.data = bytearray(echo.data)
icmp_reply.add_protocol(ethernet.ethernet(
ethertype=ether.ETH_TYPE_IP,
dst=eth.src,
src=self.switch_adds[(self.nat_switch_id, self.nat_switch_port)]))
icmp_reply.add_protocol(ipv4.ipv4(version=4, header_length=5, tos=0, total_length=84,
identification=0, flags=0, offset=0, ttl=64,
proto=inet.IPPROTO_ICMP, csum=0,
src=self.nat_ip, dst=pkt_ipv4.src))
icmp_reply.add_protocol(icmp.icmp(icmp.ICMP_ECHO_REPLY, code=0, csum=0, data=echo))
icmp_reply.serialize()
actions = [parser.OFPActionOutput(in_port)]
out = parser.OFPPacketOut(
datapath=self.nat_switch.dp,
buffer_id=self.nat_switch.dp.ofproto.OFP_NO_BUFFER,
in_port=self.nat_switch.dp.ofproto.OFPP_CONTROLLER,
actions=actions, data=icmp_reply.data)
self.nat_switch.dp.send_msg(out)
return
# icmp
elif pkt_icmp and pkt_icmp.type == icmp.ICMP_ECHO_REPLY:
# 找到icmp未有记录
if pkt_icmp.data.id in self.icmp_in:

# 来个icmp报文就便利寿命减一
self.ICMPTTLdes()

dst_ip = pkt_ipv4.dst
dst_id = pkt_icmp.data.id
pkt_ipv4.dst = self.icmp_in[pkt_icmp.data.id][0]
pkt_icmp.data.id = self.icmp_in[pkt_icmp.data.id][1]
pkt_icmp.csum = 0
# print("Change origin dst IP to {}".format(pkt_ipv4.dst))
pkt.serialize()
msg.data = pkt.data
# 找出在一个子网的有主机边缘交换机及mac
self.logger.info(
"Received a ICMP Packet to our network from internet {} to {} id:{} DNAT to {} id:{} \n".format(
pkt_ipv4.src,dst_ip,dst_id,pkt_ipv4.dst,pkt_icmp.data.id))
if self.find(pkt_ipv4.dst):
dst_mac, dst_switch_id, final_port = self.arp_table[pkt_ipv4.dst]
# print("find switch {} port {} ".format(dst_switch_id, final_port))
eth.dst = dst_mac
actions = [parser.OFPActionOutput(final_port)]
dst_switch = copy.copy(get_switch(self, dst_switch_id))[0]
pkt.serialize()
out = parser.OFPPacketOut(
datapath=dst_switch.dp,
buffer_id=dst_switch.dp.ofproto.OFP_NO_BUFFER,
in_port=dst_switch.dp.ofproto.OFPP_CONTROLLER,
actions=actions, data=pkt.data)
dst_switch.dp.send_msg(out)
else:
self.stor(msg)
return
# tcp
elif pkt_tcp:
# 找到tcpp未有记录
if pkt_tcp.dst_port in self.tcp_in:
# self.logger.info(
# "----------------Received a packet to our network from internet2 , src_ip: {} -----------------------------".format(
# pkt_ipv4.src))
dst_ip = pkt_ipv4.dst
dst_port = pkt_tcp.dst_port
pkt_ipv4.dst = self.tcp_in[pkt_tcp.dst_port][0]
pkt_tcp.dst_port = self.tcp_in[pkt_tcp.dst_port][1]
pkt_tcp.csum = 0
# print("Change origin dst IP to {}".format(pkt_ipv4.dst))
pkt.serialize()
msg.data = pkt.data
self.logger.info(
"Received a TCP Packet to our network from internet {}:{} to {}:{} DNAT to {}:{} \n".format(
pkt_ipv4.src,pkt_tcp.src_port,dst_ip,dst_port,pkt_ipv4.dst,pkt_tcp.dst_port))
# 找出在一个子网的有主机边缘交换机及mac
if self.find(pkt_ipv4.dst):
# print(self.arp_table)
dst_mac, dst_switch_id, final_port = self.arp_table[pkt_ipv4.dst]
# print("find switch {} port {} ".format(dst_switch_id, final_port))
eth.dst = dst_mac
# print(eth.dst)
actions = [parser.OFPActionOutput(final_port)]
pkt.serialize()

dst_switch = copy.copy(get_switch(self, dst_switch_id))[0]
out = parser.OFPPacketOut(
datapath=dst_switch.dp,
buffer_id=dst_switch.dp.ofproto.OFP_NO_BUFFER,
in_port=dst_switch.dp.ofproto.OFPP_CONTROLLER,
actions=actions, data=pkt.data)
dst_switch.dp.send_msg(out)
else:
self.stor(msg)
return
else:
return
# udp
elif pkt_udp:
# 找到udp未有记录
if pkt_udp.dst_port in self.udp_in:
dst_ip = pkt_ipv4.dst
dst_port = pkt_udp.dst_port
pkt_ipv4.dst = self.udp_in[pkt_udp.dst_port][0]
pkt_udp.dst_port = self.udp_in[pkt_udp.dst_port][1]
pkt_udp.csum = 0
pkt.serialize()
msg.data = pkt.data
# 找出在一个子网的有主机边缘交换机及mac
self.logger.info(
"Received a UDP Packet to our network from internet {}:{} to {}:{} DNAT to {}:{}\n".format(
pkt_ipv4.src,pkt_udp.src_port,dst_ip,dst_port,pkt_ipv4.dst,pkt_udp.dst_port))
if self.find(pkt_ipv4.dst):
dst_mac, dst_switch_id, final_port = self.arp_table[pkt_ipv4.dst]
eth.dst = dst_mac
actions = [parser.OFPActionOutput(final_port)]
dst_switch = copy.copy(get_switch(self, dst_switch_id))[0]
pkt.serialize()
out = parser.OFPPacketOut(
datapath=dst_switch.dp,
buffer_id=dst_switch.dp.ofproto.OFP_NO_BUFFER,
in_port=dst_switch.dp.ofproto.OFPP_CONTROLLER,
actions=actions, data=pkt.data)
dst_switch.dp.send_msg(out)
else:
self.stor(msg)
return
else:
return
else:
return
# 找路
shortest_path = self.topo.Dijkstra(
datapath.id, dst_switch.dp.id, in_port, final_port
)
assert len(shortest_path) > 0

path_str = ''

# (s1,inport,outport)->(s2,inport,outport)->...->(dest_switch,inport,outport)
for s, ip, op in shortest_path:
path_str = path_str + "--{}-{}-{}--".format(ip, s, op)
self.logger.info("Configure the shortset path from {} to {} —— {}\n".format(pkt_ipv4.src, pkt_ipv4.dst, path_str))

self.configure_path(shortest_path, msg, None, eth.dst, self.nat_ip)

(二)结果分析

  • ICMP

    我们分别用h1与h2去ping www.baidu.com可以发现都能够成功ping通。此时查看抓包结果发现两次ping源IP是相同的,均是192.168.31.89。

    抓包

    不同的地方在于两者的identifier。

    二者具体报文

    说明可以用不同的标识进行区分而用同一个ip实现对外网(baidu)的访问。

  • TCP与UDP

    这里我们使用h1打开Google,通过Google访问4399小游戏网站,可以发现我们能对该网站进行正常的访问与操作。

    网站访问

    查看流表可以发现s7作为NAPT访问的一个节点,将源ip与端口转化为对应的NAPT的ip与端口的报文来通过下发流表的方式实现NAPT。

    流表

    同时从抓包分析中也可以看到,从192.168.31.89返回的不同包中端口是不同的,也可以证明我们tcp和udp的NAPT操作正确。

    抓包分析

防火墙实现与分析

(一)实现

防火墙指令

在mininet的拓扑环境搭建完毕后,建立一个新的xterm来操作专用于防火墙的controller。

xterm c0

然后在switch的xterm上将OpenFlow版本设定为1.3。

ovs-vsctl set Bridge s1 protocols=OpenFlow13

最后在controller的xterm上启动rest_firewall。

ryu-manager ryu.app.rest_firewall

防火墙启动后默认为全部网络无法连接的状态,我们首先使防火墙生效。

curl -X PUT http://localhost:8080/firewall/module/enable/0000000000000009

启动完毕后对防火墙的规则进行设定:

curl -X POST -d'{"nw_src":"192.168.4.0/24"}' http://0.0.0.0:8080/firewall/rules/0000000000000009

//允许所有源IP为192.168.4.0的报文通过

curl -X POST -d '{"nw_dst": "192.168.4.200/32", "tcp_dst":"8O"}' http://0.0.0.0:8080/firewall/rules/0000000000000009

curl -X POST -d '{"nw_dst": "192.168.4.210/32", "tcp_dst":"8O"}' http://0.0.0.0:8080/firewall/rules/0000000000000009

//允许所有目的IP为Web服务器,目的端口为80的报文通过

curl -X POST -d '{"nw_proto": "ICMP", "actions": "DENY", "priority": "2"}' http://0.0.0.0:8080/firewall/rules/0000000000000009

//禁止ICMP报文通过对服务器造成负担

curl -X POST -d '{"nw_src": "192.168,1.0/24","nw_dst":"192.168.4.5/32"}’ http://0.0.0.0:8080/firewall/rules/0000000000000009

curl -X POST -d '{"nw_src": "192.168,2.0/24","nw_dst":"192.168.4.5/32"}’ http://0.0.0.0:8080/firewall/rules/0000000000000009

curl -X POST -d '{"nw_src": "192.168,6.0/24","nw_dst":"192.168.4.5/32"}’ http://0.0,0.0:8080/firewall/rules/0000000000000009

//只允许部分内部网络访问FTP服务器 访客网络禁止访问FTP服务器

(二)结果分析

  • 启动前

    在防火墙启动之前h1可以正常ping通web服务器和ftp服务器。

    ping通展示

  • 启动后

    由于我们的防火墙是基于流表项的防火墙,因此通过查看流表项可以发现s11(防火墙)已经存在我们添加的规则流表项。

    流表项

  • 内网测试

    此时h1再次ping web服务器与ftp服务器,发现无法ping通。这是因为我们设置了禁止转发icmp报文所致。

    ping不通展示

    再通过h1去访问web服务器与ftp服务器发现可以正常访问。

    正常访问服务器应用

  • 外网测试

    然后打开h9(访客网络)的终端对ftp服务器进行访问,发现访问正常。

    正常访问FTP服务器

    再用h9访问ftp服务器发现无法访问,因此可以证明我们的防火墙功能正常。

    已无法访问FTP服务器

DHCP中继实现与分析

(一)实现

DHCP相关配置

在DHCP服务器的搭建中,我们先在Ubuntu中安装DHCP Server,即执行指令pip install dhcp。在安装完成并检测安装成功后,紧接着我们在etc/dhcp/dhcpd.conf文件中进行DHCP服务的相关配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
subnet 192.168.1.0 netmask 255.255.255.0 {
range 192.168.1.2 192.168.1.253;
option routers 192.168.1.254;
default-lease-time 6000;
max-lease-time 72000;
INTERFACES = "dhcp-eth1 dhcp-eth2";
}
// 192.168.1.0/24网络的地址池,范围为192.168.1.2-192.168.1.253,网关为192.168.1.254,默认地址释放时间为6000s,最大释放时间为720000s,绑定接口为dhcp-eth1和dhcp-eth2。

subnet 192.168.2.0 netmask 255.255.255.0 {
range 192.168.2.2 192.168.2.253;
option routers 192.168.2.254;
default-lease-time 6000;
max-lease-time 72000;
INTERFACES = "dhcp-eth1 dhcp-eth2";
}
// 192.168.2.0/24网络的地址池,范围为192.168.2.2-192.168.2.253,网关为192.168.2.254,默认地址释放时间为6000s,最大释放时间为720000s,绑定接口为dhcp-eth1和dhcp-eth2。

subnet 192.168.3.0 netmask 255.255.255.0 {
range 192.168.3.2 192.168.3.253;
option routers 192.168.3.254;
default-lease-time 6000;
max-lease-time 72000;
INTERFACES = "dhcp-eth1 dhcp-eth2";
}
// 192.168.3.0/24网络的地址池,范围为192.168.3.2-192.168.3.253,网关为192.168.3.254,默认地址释放时间为6000s,最大释放时间为720000s,绑定接口为dhcp-eth1和dhcp-eth2。

Mininet拓扑连接

主干网拓扑图

如主干网拓扑图所示,我们将s8 dhcp作为网络中DHCP服务器,并将该交换机的dhcp-eth1端口与s4交换机相连,设置该端口IP为192.168.0.1;dhcp-eth2端口与s5交换机相连,设置该端口IP为192.168.0.2,以此作为冗余链路,负载均衡的同时保障网络的健壮性。其次我们将拓扑中的s1、s2、s3交换机指定为DHCP中继代理交换机,将s1-eth1端口与二层交换机s8相连,设置该端口IP为192.168.1.254;将s1-eth1端口与二层交换机s9相连,设置该端口IP为192.168.2.254;将s3-eth1端口与二层交换机s10相连,设置该端口IP为192.168.3.254。

相关信息的导入RYU

通过文件配置读取的形式向控制器导入DHCP的相关信息。

文本配置中DHCP相关信息

编写代码实现文本配置的读取并初始化控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 读取DHCP IP地址
self.dhcp_ip = content[6].replace('\n', '').replace(' ', '').split('=')[-1].split(';')
print("-----------------------DHCP_IP: {}---------------------".format(self.dhcp_ip))

# 读取DHCP switch 相关信息
self.dhcp_switch = int(content[7].replace('\n', '').replace(' ', '').split('=')[-1])
print("-----------------------DHCP_Switch: {}---------------------".format(self.dhcp_switch))

# 读取DHCP 中继相关信息
dhcp_relay_str = content[8].replace('\n', '').replace(' ', '').split('=')[-1]
for item in dhcp_relay_str.split(';'):
self.dhcp_relay.append((int(item.split(',')[0]), int(item.split(',')[1]), item.split(',')[2]))
print("-----------------------DHCP_Relay: {}---------------------".format(self.dhcp_relay))

RYU报文的获取及初步处理
1
2
3
4
5
6
7
8
9
# 读取报文DHCP协议信息
pkt_dhcp = pkt.get_protocol(dhcp.dhcp)

# 判断是否为DHCP报文
if pkt_dhcp:
# 判断上发该报文交换机ID是否为DHCP服务器 若是则丢弃 不是则进行DHCP中继处理
if dpid != self.dhcp_switch:
self.dhcp_relay_handler(msg)
return
RYU对DHCP报文解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 读取上发该报文的交换机ID和端口号
dpid = msg.datapath.id
in_port = msg.match['in_port']

# 报文解析
pkt = packet.Packet(msg.data)
pkt_dhcp = pkt.get_protocol(dhcp.dhcp)
eth = pkt.get_protocols(ethernet.ethernet)[0]
pkt_ipv4 = pkt.get_protocol(ipv4.ipv4)
pkt_udp = pkt.get_protocol(udp.udp)

# 找出上发该报文交换机对应的DHCP中继服务信息 若找到 记录网关IP地址及端口
for s, p, g in self.dhcp_relay:
if dpid == s:
gateway = g
link_port = p

# 遍历报文DHCP协议中options字段 找出并记录该报文对应的DHCP类型
for option in pkt_dhcp.options:
if option.tag == dhcp.DHCP_MESSAGE_TYPE_OPT:
dhcp_type = int(str(binascii.hexlify(option.value))
break

# 输出相关信息
self.logger.info(
"Received a DHCP packet in dhcp-relay switch:{} port:{} type:{}"
"\n".format(dpid, in_port, dhcp_type))
DHCP Discover 报文和DHCP Request报文的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 判断DHCP报文类型是否为DISCOVER和REQUEST及保证报文接收端口正确
if (dhcp_type == dhcp.DHCP_DISCOVER or dhcp_type == dhcp.DHCP_REQUEST) and in_port == link_port:
if len(self.dhcp_link_stats) > 0:
# 随机选择一条与DHCP服务器连接的链路 并记录对应信息
index = random.randint(0, len(self.dhcp_link_stats) - 1)
send_switch = self.dhcp_link_stats[index][1]
send_port = self.dhcp_link_stats[index][3]
eth.dst = self.switch_adds[(self.dhcp_switch, self.dhcp_link_stats[index][2])]

# 判断源IP地址和目的IP地址 用于区分IP地址分配过程、更新过程、释放过程
if pkt_ipv4.src == "0.0.0.0" and pkt_ipv4.dst == "255.255.255.255":
# 修改相应信息
pkt_dhcp.hops += 1
pkt_dhcp.giaddr = gateway
pkt_ipv4.src = gateway
pkt_ipv4.dst = self.dhcp_ip[int(self.dhcp_link_stats[index][2]) - 1]
# 序列化修改后的报文并重新添加Padding
pkt_udp.csum = 0
pkt.serialize()
msg.data = pkt.data + self.addPadding(len(msg.data) - len(pkt.data))

# 判断hops字段是否小于等于16
if pkt_dhcp.hops <= 16:
# 将报文从特定交换机的指定端口发送并输出信息
actions = [get_switch(self, send_switch)[0].dp.ofproto_parser.OFPActionOutput(send_port)]
self.send_out(get_switch(self, send_switch)[0], actions, msg.data)
self.logger.info("Send out a DHCP packet in dhcp-relay switch:{} port:{} type:{} \n".format(send_switch, send_port, dhcp_type))
5. DHCP Offer 报文和DHCP Ack报文的处理
# 判断DHCP报文类型是否为OFFER和ACK
elif dhcp_type == dhcp.DHCP_OFFER or dhcp_type == dhcp.DHCP_ACK:
# 找出对应的中继交换机相关信息
for s, p, g in self.dhcp_relay:
if pkt_dhcp.giaddr == g:
send_switch = s

# 判断DHCP协议的flags字段是否为广播报文
if pkt_dhcp.flags == 0:
eth.dst = pkt_dhcp.chaddr
else:
eth.dst = "ff:ff:ff:ff:ff:ff"
# 判断DHCP协议的yiaddr字段与目的IP地址是否相等 用于区分IP地址分配过程、更新过程、释放过程

if pkt_dhcp.yiaddr != pkt_ipv4.dst:
pkt_ipv4.src = pkt_dhcp.giaddr
pkt_ipv4.dst = pkt_dhcp.yiaddr
# 修改目的端口正确
pkt_udp.dst_port = 68
# 序列化修改后的报文并重新添加Padding
pkt_udp.csum = 0
pkt.serialize()
msg.data = pkt.data + self.addPadding(len(msg.data) - len(pkt.data))

# 将报文从特定交换机的指定端口发送并输出信息
actions = [get_switch(self, send_switch)[0].dp.ofproto_parser.OFPActionOutput(p)]
self.send_out(get_switch(self, send_switch)[0], actions, msg.data)
self.logger.info("Send out a DHCP packet in dhcp-relay switch:{} port:{} type:{} \n".format(send_switch, p, dhcp_type))
break

(二)结果分析

  • DHCP服务请求

    对h1进行DHCP服务的请求,并查看h1的IP地址。如下图,h1成功分配到IP地址192.168.1.3。

    查看h1的IP地址

  • 抓包分析

    在h1进行DHCP服务请求的过程中,分别对h1-eth1、dhcp-eth1和dhcp-eth2三个端口进行抓包。

    首先,如下图,在dhcp-eth2端口接收到了DHCP Discover报文,在dhcp-eth1端口接收到了DHCP Offer、DHCP Request和DHCP ACK报文,说明实现了DHCP服务器的负载均担。

    报文分析

    如下图,在h1的Discover报文中,可以看到中继服务器的IP地址被标识为0.0.0.0,而经过控制器的修改,发送到DHCP服务器的中继服务器的IP地址被修改为192.168.1.254。

    报文分析

    如下图,在DHCP服务器发送的Offer报文中,源IP地址是192.168.0.1,目的IP地址是192.168.1.254。经过控制器的修改后,h1收到的Offer报文中源IP地址被成功修改为中继交换机的IP,即192.168.1.254,目的IP地址被修改为分配给h1的IP地址,即192.168.1.3。

    报文分析

    后续的Request报文和ACK报文也由控制器进行相应的修改,与上述两个报文类似。

  • 连通性验证

    对h1进行DHCP请求后,再次对不同子网下的h5进行同样的DHCP请求操作,成功获取IP地址后,测试h1和h5的连通性,如下图,可以发现能够成功ping通。

    ping通展示

服务器实现与分析

(一)实现

在主干网的服务器集群中,我们需要对三种服务器进行配置,即反向代理服务器、Web服务器、FTP服务器。其中反向代理服务器拥有两张网卡分别与Mininet拓扑和Web服务器集群连接,我们将两台反向代理服务器与Mininet拓扑连接的网卡IP地址配置为192.168.4.1、192.168.4.2,将其与Web服务器集群连接的网卡IP配置为172.168.1.3、172.168.1.4;Web服务器和FTP服务器都只拥有一张网卡,将两台Web服务器IP配置为172.168.1.1、172.168.1.2,FTP服务器IP配置为192.168.4.5。下面对各种服务器进行单独的配置说明。

反向代理服务器配置

在反向代理服务器中,我们基于nginx的代理机制实现对Web服务器集群的反向代理,成功安装nginx后,修改/etc/nginx/conf.d/default.conf文件配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 配置Web服务器地址池
upstream phpserver1 {
server 172.168.1.1:80;
server 172.168.1.2:80;
}

// 配置反向代理服务器映射方法(其中200210为虚拟IP在VRRP部分会进一步介绍)
server {
listen 80;
server_name 192.168.4.200;
server_name 192.168.4.210;
location / {
proxy_pass http://phpserver1;
index index.html index.htm;
}
}
Web服务器配置

在Web服务器中,我们同样是基于nginx的代理机制实现对Web服务,成功安装nginx后,修改/etc/nginx/conf.d/default.conf文件配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 配置Web服务器端口和文件位置
server {
listen 80;
server_name localhost;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}

}
FTP服务器配置

在FTP服务器中,我们基于开源软件vsftpd实现FTP服务,成功安装vsftpd后,修改/etc/vsftpd/vsftpd.conf文件配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 是否开启匿名用户,匿名都不安全,默认NO
anonymous_enable=NO

# 允许本机账号登录FTP
local_enable=YES

# 允许账号都有写操作
write_enable=YES

# 本地用户创建文件或目录的掩码
local_umask=022

# 进入某个目录的时候,是否在客户端提示一下
dirmessage_enable=YES

# 当设定为YES时,使用者上传与下载日志都会被记录起来
xferlog_enable=YES

# 日志成为std格式
xferlog_std_format=YES

# 上传与下载日志存放路径
xferlog_file=/var/log/xferlog

# 开放port模式的20端口的连接
connect_from_port_20=YES

# 与上一个设定类似的,只是这个设定针对上传而言,预设是NO
ascii_upload_enable=NO
ascii_download_enable=NO

# 限制用户只能在自己的目录活动
chroot_local_user=YES
chroot_list_enable=NO
chroot_list_file=/etc/vsftpd/chroot_list

# 监听ipv4端口,开了这个就说明vsftpd可以独立运行,不用依赖其他服务
listen=NO

# 监听ipv6端口
listen_ipv6=YES

# 打开主动模式
port_enable=YES

# 启动被动式联机(passivemode)
pasv_enable=YES

# 被动模式起始端口,0为随机分配
pasv_min_port=64000

# 被动模式结束端口,0为随机分配
pasv_max_port=65000

# 这个是pam模块的名称,我们放置在/etc/pam.d/vsftpd,认证用
pam_service_name=vsftpd

# 使用允许登录的名单,在/etc/vsftpd/user_list文件中添加新建的用户ftpuser
userlist_enable=YES

# 限制允许登录的名单,前提是userlist_enable=YES,其实这里有点怪,禁止访问名单在/etc/vsftpd/ftpusers
userlist_deny=NO

# 允许限制在自己的目录活动的用户拥有写权限
allow_writeable_chroot=YES

# FTP访问目录
local_root=/data/ftp/ftpuser
pasv_promiscuous=YES

(二)结果分析

  • Web服务器与负载分担

    此处我们采用了负载均担技术,即可以将外部的Web请求映射到两台相同的Web服务器上。

    两台Web服务器的IP地址分别为172.168.1.1和172.168.1.2,如下图。

    IP展示

    在h1的浏览器中访问192.168.4.200,可以看到负载会被分担到两台Web服务器上,如下图。在正常情况下,访问时会出现相同的页面,这里为了区分设置了两种不同的页面来代表访问不同的Web服务器。

    web页面

  • FTP服务器

    我们FTP服务器的IP地址为192.168.4.5。首先登录FTP服务器,查看目录,找到相应需要接收的文件1.txt。

    进入目录

    在命令行中接收该文件,并在文件夹中查看,可以看到文件被准确无误地从FTP服务器上接收。

    FTP文件接收

    打开文件

VRRP高可用实现与分析

(一)实现

反向代理服务器之间的高可用配置

在反向代理服务器中,我们基于开源软件Keepalived实现了反向代理服务器之间基于VRRP协议的高可用配置,在成功安装Keepalived软件后,修改/etc/keepalived/keepalived.conf文件进行配置。

前面提到我们有两台反向代理服务器,且与Mininet拓扑连接的网卡IP配置分别为192.168.4.1和192.168.4.2,我们指定前一台反向代理服务器为router_id Nginx_01、后一台为router_id Nginx_02。在此时的高可用配置中,我们使用双主配置的方式,创建两个虚拟IP 192.168.4.200和192.168.4.210,且配置router_id Nginx_01为192.168.4.200的主服务器(即优先级更高),router_id Nginx_02为192.168.4.210的主服务器,各自配置文件如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
global_defs {               
router_id Nginx_01
}
vrrp_script check_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
weight -5
fall 3
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface ens32
virtual_router_id 51
priority 150
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.4.200
}
track_script {
check_nginx
}
}
vrrp_instance VI_2 {
state BACKUP
interface ens32
virtual_router_id 52
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.4.210
}
track_script {
check_nginx
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
global_defs {               
router_id Nginx_02
}
vrrp_script check_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
weight -5
fall 3
rise 2
}
vrrp_instance VI_1 {
state BACKUP
interface ens32
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.4.200
}
track_script {
check_nginx
}
}
vrrp_instance VI_2 {
state MASTER
interface ens32
virtual_router_id 52
priority 150
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.4.210
}
track_script {
check_nginx
}
}
反向代理服务器对服务器的代理

这一配置在服务器部分有提及,此处进行进一步解释。在我们的反向代理服务器中,由于实现了高可用的配置,对反向代理服务器的访问需要通过虚拟IP访问才能保障高可靠服务的有效性。因此我们在配置反向代理时也需要将nginx代理的IP地址修改为我们定义的虚拟IP地址,即192.168.4.200和192.168.4.210。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 配置Web服务器地址池
upstream phpserver1 {
server 172.168.1.1:80;
server 172.168.1.2:80;
}

// 配置反向代理服务器映射方法(其中200210为虚拟IP)
server {
listen 80;
server_name 192.168.4.200;
server_name 192.168.4.210;
location / {
proxy_pass http://phpserver1;
index index.html index.htm;
}
}

(二)结果分析

  • VRRP验证

    对于两台反向代理服务器nginx1和nginx2,他们映射有VRRP机制下的虚拟IP地址。如下图,nginx1有真实IP地址192.168.4.1,也有虚拟IP地址192.168.4.200;nginx2有真实IP地址192.168.4.2,也有虚拟IP地址192.168.4.210。

    IP展示

    在h1的xterm下打开浏览器,依次访问192.168.4.200和192.168.4.210,如下图,均可以访问到Web服务器。

    访问web服务器

    暂停nginx1的反向代理服务器来检测VRRP的有效性,即关闭nginx代理服务和keepalived服务。

    关闭代理服务

    查看nginx1的IP地址,可以看到虚拟IP地址已经消失,只剩下真实IP地址192.168.4.1。这相当于nginx1这台反向代理服务器出现差错,无法继续工作,正常情况下,另一台代理服务器会接替这台出现差错的代理服务器。

    IP展示

    我们再查看nginx2的IP地址,可以看到,在原来两个IP地址的基础上,多出了192.168.4.200这一虚拟IP地址,说明其接替了nginx1的工作。

    IP展示

    再次在h1的浏览器中测试两个虚拟IP地址,均成功访问,VRRP技术验证成功。

    web服务器访问

GRE隧道实现与分析

(一)实现

反向代理服务器对服务器的代理

image-20230717234325368

在我们设计的网络中,主干网和分支网是位于两台虚拟机上的Mininet拓扑,想要实现两者通过隧道的通信,首先我们需要在两台虚拟机之间搭建隧道连接。对我们的拓扑分析,外面需要连通的是主干网的服务器集群网络192.168.4.0/24和分支网的主机网络192.168.6.0/24,我们需要搭建用于传输这两个网段的隧道。由于Linux内核已经系统级的支持GRE隧道,因此我们只需要在系统中编写并执行脚本便可以实现隧道的搭建。两台虚拟机的脚本文件如下。

主干网脚本:

1
2
3
4
5
6
7
// 在主干网和分支网虚拟机IP之间搭建GRE隧道并启动
ip tunnel add Tunnel-1 mode gre remote 192.168.106.242 local 192.168.106.171
ifconfig Tunnel-1 up
// 配置隧道的网段
ip addr add 192.168.6.1/24 dev Tunnel-1
// 配置隧道的路由表项
ip route add 192.168.4.0/24 dev Tunnel-1

分支网脚本:

1
2
3
4
5
6
7
// 在主干网和分支网虚拟机IP之间搭建GRE隧道并启动
ip tunnel add Tunnel-1 mode gre remote 192.168.106.171 local 192.168.106.242
ifconfig Tunnel-1 up
// 配置隧道的网段
ip addr add 192.168.4.1/24 dev Tunnel-1
// 配置隧道的路由表项
ip route add 192.168.6.0/24 dev Tunnel-1
主干网和分支网通过隧道的连通

通过文件配置读取的形式向控制器导入隧道的相关信息。

隧道相关信息的配置

编写代码实现文本配置隧道信息的读取并初始化控制器:

1
2
3
4
5
6
7
# 读取隧道连接的内部网络信息
self.vpn_innet = content[10].replace('\n', '').replace(' ', '').split('=')[-1].split(';')
print("-----------------------vpn_innet: {}---------------------".format(self.vpn_innet))
# 读取隧道连接的外部网络信息
self.vpn_outnet = content[11].replace('\n', '').replace(' ', '').split('=')[-1].split(';')
print("-----------------------vpn_innet: {}---------------------".format(self.vpn_outnet))

报文的获取及初步处理:

1
2
3
# 源和目的IP都为隧道网段 进行处理
if self.ipINvpn(pkt_ipv4.src) and self.ipINvpn(pkt_ipv4.dst):
self.vpn_handler(msg)

隧道报文的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def vpn_handler(self, msg):
# 读取报文相关信息
datapath = msg.datapath
parser = datapath.ofproto_parser
in_port = msg.match['in_port']
pkt = packet.Packet(msg.data)
eth = pkt.get_protocols(ethernet.ethernet)[0]
pkt_ipv4 = pkt.get_protocol(ipv4.ipv4)

# 寻找最短路并输出
shortest_path = self.topo.Dijkstra(
datapath.id, self.nat_switch_id, in_port, self.nat_switch_port
)
assert len(shortest_path) > 0
path_str = ''
for s, ip, op in shortest_path:
path_str = path_str + "--{}-{}-{}--".format(ip, s, op)
self.logger.info("Configure the shortset path from {} to {} —— {}\n".format(pkt_ipv4.src, pkt_ipv4.dst, path_str))

# 下发流表项
self.configure_path(shortest_path, msg, eth.src, eth.dst, pkt_ipv4.dst)

# 将报文从NAT交换机发送
actions = [parser.OFPActionOutput(self.nat_switch_port)]
self.send_out(self.nat_switch, actions, pkt.data)

(二)结果分析

  • 隧道连通性验证

    启动GRE隧道,在隧道两端,我们连接主干网的服务器部分(网段为192.168.4.0)和分支网的主机部分(网段为192.168.6.0)。

    启动GRE隧道

    用IP为192.168.6.1的主机ping IP为192.168.4.1的服务器,可以看到能够成功ping通。

    ping通展示

  • 抓包分析

    可以看到,抓取到的报文具有两层IP结构,即还有一层IP结构为Generic Routing Encapsulation(GRE),证明GRE隧道有效。

    抓包分析

  • 主干、分支网连通性验证

    启动拓扑,接下来验证分支网可以通过两种方式来访问主干网的服务器。

    首先进行ICMP测试,可以看到,分支网可以成功ping通主干网的Web服务器(192.168.4.1)和FTP服务器(192.168.4.5)。

    连通性验证

    再验证TCP服务和UDP服务。在分支网打开浏览器,先使用公网IP 192.168.106.242进行访问,可以看到访问成功,且负载分担正常。

    访问web服务器

    采用私网IP(192.168.4.200和192.168.4.210),即隧道的方式进行访问,可以看到两个虚拟IP均可以正常访问。

    访问web服务器

    用隧道的方式连接FTP服务器,可以成功登录,使用get指令后文件成功获取,GRE隧道验证成功。

    登录服务器

    获取文件成功

总结

本次项目设计中,首先通过我们自己设计并实现的协议,完成了SDN网络的配置,验证了拓扑中各个主机的联通性,同时实现了流量的监控与管理;

其次实现了通过NAPT对外网的访问,在这一点上,我们优化了本身实现的NAT,完成了NAPT的搭建;

接着实现了DHCP中继,使各个主机可以通过向DHCP服务器申请的方式获得自身的IP地址,减少了网络管理员手动配置的工作量;

然后实现了包含负载均担的服务器的搭建,并且通过防火墙的设置,完成了不同子网访问服务器的限制;

再然后实现了基于VRRP协议的高可用配置,为我们设计实现的中小型网络增加了出故障之后的处理能力,提高了鲁棒性;

最后,通过实现GRE隧道,实现了不同网络之间的相互访问。

到此为止,我们完成了整个项目设计目标——中小型网络的搭建,完成了在有限的计算机和网络设备的条件下,通过选择合适的硬件和软件,构建一个适合中小规模组织使用的网络系统。