简介: honeyd 可用来在单台或是多台机器上模拟出大量 IP 地址组成的虚拟网络。在此框架下,软件架构设计师或是软件测试人员可以开发出模拟真实被测服务的轻量级虚拟服务,从而高效的找到服务的接受方的性能瓶颈或是分布式服务拓扑结构的设计问题。

星型结构分布式软件是由一个集中节点和其他工作节点直连的软件拓扑结构。它的优点在于结构简单稳定,常见于中小型规模的软件上。缺点在于集中节点很容易成为负载的瓶颈,因此需要利用压力测试软件来配合调优。Honeyd 是一个轻量级的开源虚拟蜜罐框架。它可以被用来模拟主流的操作系统以及上面运行的网络服务。它还能模拟网络,实现在单个物理节点上的多 IP 虚拟节点的模拟。尽管设计之初它是用来捕捉***行为以提高网络安全。但是 honeyd 良好的扩展性以及不错的性能也提供给压力测试人员一个很好的平台,只需要开发 honeyd 上的虚拟网络服务,然后借助 honeyd 的虚拟网络功能,就能在一台主机上构建一个 C 或 B 类网段去提供该虚拟服务的工作节点,从而可以测试集中节点的压力负载情况。

本文主要是提供给压力测试人员一个基于 honeyd 实现测试框架的基本思路,这包括了配置 honeyd 构建虚拟网络,以及开发用于 honeyd 的特殊虚拟网络服务。因此,本文的描述包括 :

  • honeyd 的技术背景
  • honeyd 的安装以及虚拟网络的配置
  • 通过一个例子描述如何进行 honeyd 上虚拟服务的开发

 

honeyd 的技术背景

honeyd 是由 google 的软件工程师 Niels Provos 在 2003 年推出的一个 GPL 开源软件。它本身以 daemon 的方式,运行在 linux 操作系统上,可用来虚拟出大量的虚拟"主机",每台虚拟"主机"又可以被配置成安装了 Windows 或类 UNIX(Linux 或是 AIX)等操作系统,当然 honeyd 不是提供真实的操作系统,只是提供一些操作系统的特征。虚拟"主机"上还可以运行各种各样的网络服务 , 如 ssh, http,或是用户自己编写的特殊虚拟服务。根据 honeyd 的介绍文档所著,honeyd 被测试可以在一台物理机上模拟 65536 台虚拟"主机"及其服务。目前 honeyd 的版本为 1.5c。在 windows 平台上,Mike Davis 提供了一个基于 honeyd 0.5 的移植版本。


honeyd 的安装以及虚拟网络的配置


Honeyd 的安装

Honeyd 支持如下多种平台:基于 BSD(Berkeley Software Distribution)的 UNIX 系统、GNU/Linux 系统以及 Solaris 系统。目前 Honeyd 的最新版本是 1.5c。Honeyd 自身会依赖于以下一些软件库(Software Library)libevent,libdnet,libpcap,在安装 Honeyd 之前需要确保这些库已经正确安装。

下面分别介绍一下各个库的作用 :

  • libevent:是一个非同步事件通知(Asynchronous Event Notification)的函数库。通过使用 libevent,开发人员可以设定某些事件发生时所执行的函数,可以代替以往程序所使用的循环检查。该程序库的官方网站是 http://www.monkey.org/~provos/libevent。
  • Libdnet:是一个提供了跨平台的网络相关 API(Application Programming Interface)的函数库(主要是较底层的网络操作),包括 arp 缓存,路由表查询,IP 包及物理帧的传输等。官方网站是 http://libdnet.sourceforge.net。
  • Libpcap:是一个数据包捕获(Packet Sniffing)的函数库,大多数网络软件都以它为基础。官方网站是 http://sourceforge.net/projects/libpcap。

 


本节主要介绍一下 Honeyd 的安装过程,采用的操作系统平台为 RHEL 5.3(Red Hat Enterprise Linux Server release 5.3)x86 32 位版。Honeyd 在其他操作系统上的安装与 RHEL5.3 非常类似,在安装其他平台时也可按本文所介绍的进行安装。

在安装 Honeyd 之前,先安装依赖库。RHEL 5.3 的默认安装中已经包括 libevent,但是这个版本较旧,不能支持 Honeyd,我们需要手工安装新的版本。从官方网站下载 1.4.13 稳定版本,文件名为 libevent-1.4.13-stable.tar.gz。(因为安装过程中需要编译 C 语言代码,所以需要事先在 RHEL 5.3 上安装好 C 语言编译器)

 tar – zxf libevent-1.4.13-stable.tar.gz 
 cd libevent-1.4.13-stable 
 ./configure 
 make 
 make isntall

libevent 会被安装在 /usr/local/lib 目录下,如图 1 所示:


图 1. libevent 安装后的目录
libevent 安装后的目录 

RHEL 5.3 的默认安装中没有包括 libdnet,我们需要手工安装。RHEL 5.3 安装光盘中并没有包括 libdnet 的 RPM 格式安装包,我们需要自己下载源代码并且编译安装。 下载地址:http://libdnet.sourceforge.net/,下载后的文件名为 libdnet-1.11.tar.gz。

 tar -zxf libdnet-1.11.tar.gz 
 cd libdnet-1.11 
 ./configure 
 make 
 make install

libdnet 会被安装在 /usr/local/lib 目录下,如图 2 所示:


图 2. libdnet 安装后的目录
libdnet 安装后的目录 

RHEL 5.3 的默认安装中已经包括 libpcap。查询命令 rpm -qa | grep libpcap,如图 3 所示:


图 3. 查询 libpcap
查询 libpcap 

Honeyd 编译过程中还会用到 libpcap-devel,需要安装该 RPM 包。RHEL 5.3 的发行光盘中包括这个 RPM 包,直接安装即可。

现在开始安装 Honeyd。Honeyd 同样没有提供诸如 RPM 格式的安装包,需要自己下载源代码并且编译、安装。下载地址:http://www.honeyd.org/release.php,目前最新版本为 1.5c,下载后文件名为 honeyd-1.5c.tar.gz。

 tar -zxf honeyd-1.5c.tar.gz 
 cd honeyd-1.5c 
 ./configure 
 make 
 make install

Honeyd 在默认情况下会被安装在 /usr/local/bin 目录下。


虚拟网络的配置

在启动 Honeyd 之前,需要提供一个配置文件。下面这条命令是用来启动 honeyd 的,当中指定了该配置文件为 honeyd.conf:

 Honeyd -d -l log.txt -f honeyd.conf -s service.log -i eth0 --fix-webserver-permissions

通过这个配置文件配置所要虚拟出的主机。下面是该配置文件的部分配置内容:

 route entry 9.186.96.250 network 9.186.96.0/24 
 route 9.186.96.250 add net 192.168.1.0/24 192.168.1.1 
 route 192.168.1.1 link 192.168.1.0/24 
 create bladecenter_slp 
 set bladecenter_slp default tcp action reset 
 set bladecenter_slp default udp action block 
 set bladecenter_slp default icmp action open 
 add bladecenter_slp udp port 427 proxy 127.0.0.1:9427 
 add bladecenter_slp tcp port 427 proxy 127.0.0.1:9427 
 bind 192.168.1.2 bladecenter_ssh 
 bind 192.168.1.3 bladecenter_ssh

第一行指定路由器的地址(该地址必须是 Honeyd 所在网段中的一个没有被分配的地址,该例中的 IP 地址为 9.186.96.250,网段为 9.186.96.0)。所有访问 Honeyd 模拟节点的都通过该路由器转发。网络中的主机可以通过这个路由器访问到 Honeyd 虚拟出的主机节点。

第二行增加一个新的路由器(IP 为 192.168.1.1,所在网段为 192.168.1.0)并且连接到原有的路由器上(IP 为 9.186.96.250)。这样通过原有路由器,网络主机就可以访问 192.168.1.0 网段中的主机。

第三行设置通过路由器(192.168.1.1)可以访问的节点,实例中该路由器可以访问 192.168.1.0 中的所有 IP 地址。

第四行到第九行创建一个 SLP 服务模板。第四行到第七行设置默认行为。 TCP reset 表示发送 RST 响应;UDP block 表示丢弃收到的请求,不发送响应;ICMP open 表示发送响应。第八行指定如何处理 UDP 427 上收到的请求。第九行指定如何处理 TCP 427 上收到的请求。Honeyd 提供了两种方式来处理收到的请求:一种是将该请求转发到另一个 IP 的某个端口上;一种是直接运行某个脚本来处理该请求。实例中的处理方式为第一种。Honeyd 会将收到请求转到本机上(IP 是 127.0.0.1)的 9427 端口。Honeyd 用户可以自己编制一个 SLP 服务端(监听端口为 9427)来处理发送过来的请求。如果采用自动运行一个脚本来处理请求,配置方式如下:

 add bladecenter_slp udp port 427 "perl scripts/slp.pl"

第十行和十一行是根据之前创建的模板定义两个具体节点:它们的 IP 地址分别是 192.168.1.2 和 192.168.1.3。外界可以直接访问到这两个节点。


本文随后将通过一个具体的实例展示如何在 Honeyd 上进行定制的虚拟服务开发。

 

honeyd 上的虚拟服务开发实例


对 honeyd 进行修改

为了能够让创建的虚拟服务更真实的对请求的虚拟节点做出响应,我们需要对现有的 honeyd 源代码进行部分功能的添加修改。因为一旦在配置文件当中设置转发请求到本地 IP 上的某个端口时,honeyd 就会对外部网络节点的请求进行转发,这使得创建的虚拟服务无法识别出真实的外部网络节点的 IP 地址。所以我们需要在 honeyd 进行转发请求时记录下来真实的 IP 地址,在虚拟服务做出响应前读出这个真实的 IP 地址,然后对外部节点做出相应的虚拟服务应答。当响应结束时,我们还需要把这条记录删除。


下面阐述一下怎样才能让 honeyd 记录下真实的外部节点的 IP 地址并且让虚拟服务能够读出来:

首先,当外部节点对虚拟节点进行访问时,并且该被访问的虚拟节点配置了请求转发,honeyd 就会对这样的节点访问请求进行转发。并且,honeyd 是可以知道真实的外部节点 IP 地址的。在转发过程中,honeyd 会生成一个此时系统没有被占用的端口,这个端口是用来给外部节点的访问请求使用的。这个时候我们可以在固定目录下(比如 /tmp/ 目录下)创建一个以端口号为标识符的临时文件,并把这个外部节点 IP 地址写入该文件当中。

然后,就是怎样让虚拟服务读出这个端口了。当被转发过来的请求访问我们创建出的虚拟服务时,这个请求是从本地 IP 地址上的某个端口过来的,这个端口就是在 honeyd 在转发过程中生成的那个没有被系统占用的端口,所以虚拟服务是可以知道哪个端口进行请求访问的,并且根据这个端口去那个固定目录下(比如 /tmp/ 目录下)读出相应的临时文件。这样真实的外部节点 IP 地址就可以被虚拟服务所读出。

最后,当 honeyd 释放该请求链接时,我们可以根据这个特有的端口号来删除记录了真实 IP 地址的临时文件。下图描述从请求,转发到虚拟服务进行响应的整个流程。


图 3. honeyd 的虚拟服务响应流程图
honeyd 的虚拟服务响应流程图 

修改后的代码,请查看下载部分。


使用 Netty 添加定制服务

下面是一个基于 JBoss Netty 框架开发出来的非常简单的定制服务,我们以它为列介绍如何开发 honeyd 上的虚拟服务。

Netty 为我们提供了一个创建异步的、事件驱动的网络应用框架,可用于快速开发可维护的、高性能的、高扩展性的服务器和客户端之间的协议。换句话说,Netty 是一个 NIO 客户端服务器框架,能够快速、轻松地开发网络应用例如服务器和客户端间的协议。它简化了网络编程如 TCP/IP socket 服务器。在这里我们利用 Netty 进行服务器端服务的开发。

第一步,创建定制服务的 channelHandler。扩展基类 SimpleChannelHandler,定义自己的 ChannelHandler 扩展类 MyServiceChannelHandler,然后根据虚拟定制服务的需要重写方法 channelConnected(),messageReceived 等来完成自己服务的响应代码。下面给出部分代码的片段,完整代码请查看参考资料部分。

 @Override 
 public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) { 
    channels.add(e.getChannel()); 
    /* For TCP traffic */ 
    final SocketAddress local = ctx.getChannel().getLocalAddress(); 
    final SocketAddress remote = ctx.getChannel().getRemoteAddress(); 
    this.sessionFuture = Utils.EXECUTOR_SERVICE 
            .submit(new Callable<String>() { 
                public String call() throws Exception { 
                    final String endpoint = findEndpointId(local, remote); 
                    //Create your response sessions 
                    .... 
                    return ....; 
                } 
            }); 
    ctx.sendUpstream(e); 	
 }

第二步,创建定制服务的 ChannelPipelineFactory。执行抽象接口 ChannelPipelineFactory,定义自己的 ChannelPipelineFactory 执行类 MyServiceChannelPipelineFactory,添加 channelHandler 到 ChannelPipeline 当中。

 private ChannelHandler handler; 	

 public MyServiceChannelPipelineFactory(ChannelHandler handler) { 
    this.handler = handler; 
 } 

 // @Override 
 public ChannelPipeline getPipeline() { 
    ChannelPipeline pipeline = Channels.pipeline(); 
    pipeline.addLast("decoder", new ObjectDecoder());// up 
    pipeline.addLast("encoder", new ObjectEncoder());// down 
    pipeline.addLast("myhandler", handler);// my handler include up and down 
    return pipeline; 
 }

第三步,绑定定制服务到本地 IP 上某个固定端口上。创建定制服务类 MyService,这里使用端口 8080 来绑定服务。

 public static String LOCALHOST = "127.0.0.1"; 
 public static int port = 8088; 

 public static void main(String[] args) throws Exception{     	
    ChannelFactory factory = new NioServerSocketChannelFactory(  
                Executors.newCachedThreadPool(),  
                Executors.newCachedThreadPool());  
    
    ServerBootstrap bootstrap = new ServerBootstrap(factory);  
    MyServiceChannelHandler handler = new MyServiceChannelHandler();  
      
    bootstrap.setPipelineFactory(new 
	          MyServiceChannelPipelineFactory(handler));  
    bootstrap.setOption("child.tcpNoDelay", true);  
    bootstrap.setOption("child.keepAlive", true);  
    
    InetAddress iAddr = InetAddress.getByName(LOCALHOST); 
    
    System.out.println("Service bound to the localhost 
	                    on the port " + port + "."); 
    
    bootstrap.bind(new InetSocketAddress(iAddr, port)); 
 }

这样一个简单的定制服务框架便完成了,如果需要添加更多的内容,可以在此框架下对 channelConnected() 方法进行扩展,实现更多的自定义内容响应服务。


发布定制服务到 honeyd 服务器上

做完上述内容的开发之后,还需要把上面的代码打成 jar 包(比如 MyService.jar)发布到 honeyd 的运行环境当中,并对 honeyd 进行简单的转发配置。下面一段是对 honeyd 的配置文件做的部分修改 :

 route 192.168.111.200 add net 192.168.111.0/24 192.168.111.1 
 route 192.168.111.1 link 192.168.111.0/24 

 create suse80 
 set suse80 personality "Linux 2.4.7 (X86)"
 set suse80 default tcp action reset 
 set suse80 default udp action block 
 set suse80 default icmp action open 
 set suse80 uptime 79239 
 set suse80 droprate in 4 

 add suse80 tcp port 21 "sh /home/gk/work/ftp.sh"
 add suse80 tcp port 22 "sh /home/gk/work/ssh.sh $ipsrc $sport $ipdst $dport"
 add suse80 tcp port 8088 proxy 127.0.0.1:8088

 bind 192.168.111.13 suse80 
 bind 192.168.111.14 suse80 
 bind 192.168.111.15 suse80

配置完成之后,启动 honeyd。与此同时,还需要手动启动我们开发的虚拟服务,这里我们使用 java 进程来运行该服务(比如”java -jar MyServer.jar“),服务端口是 8080。

使用 tcp 协议对多个模拟节点(比如节点 192.168.111.13/14/15 等)的 8080 端口进行访问,虚拟服务便可对这些请求进行响应。


 

结论

使用 honeyd 来模拟网络服务,这种方案与使用真实的虚机来模拟网络服务,从技术的角度来讲,需要开发额外的虚拟网络服务,因此技术难度会高一些。但是好处在于,honeyd 模拟出来的仅仅是网络交互行为,并不是真实的虚拟机及其操作系统,因此虚拟的额外开销要小的多。这就可以实现用一台机器模拟百台甚至千台机器上虚拟服务的目的。目前,云计算以渐成趋势,分布式服务的规模和范围也越来越大,利用 honeyd 的这种测试技术,将有助于该类软件的开发和测试效率。