DHCP,主机动态配置协议的代码实现第一步:实现设备请求和服务器应答

本节开始,我们代码实现DHCP的协议流程。其本质上是在相应的阶段,构造相应的数据包进行发送和接收,总体而言,DHCP数据包的格式如下:

它最复杂的其实是填写options字段,该字段种类及其繁杂,我们根据不同协议的不同阶段去搞清楚options字段的内容。根据我们前面描述,DHCP协议启动时,第一步是客户端在子网内广播dhcp discover消息,然后子网内相应的dhcp服务器回发dhcp offer消息,因此我们的代码先完成这一步骤。

首先我们要解析DHCP DISCOVER数据包结构。如果使用的是mac系统,在命令行控制台中输入命令:

sudo ipconfig set en0 DHCP

如果使用的是windows,那么在控制台中输入命令:

ipconfig renew

然后按照上一节使用wireshark抓包,我们就可以抓取DHCP DISCOVER消息:

点击打开该消息后,我们可以看到消息的字段结构:

这里我们需要把握几个option结构,第一个是option 53,它用来表明该数据包的类型,它的code值是53,数据长度1个字节,一般取值为1,数据内容用于表明数据包的类型,取值1表示消息类型为DHCPDISCOVERY,2为DHCPOFFER等,具体内容如下:

第2个option的code值为55,data_length 占据的字节数可变,数据区用于表明设备想从服务器获得哪些消息,从我抓包的情况看,它包含如下信息如下:

从上图看,我的设备向服务器请求一系列数据,例如子网掩码,路由器,域名服务器等,这些请求各自使用不同的数值来表示,例如数值1表示请求子网掩码,数值3请求路由器IP等。

第3个option的code值是57,data_length字节数是2,它用来确定相互交互的DHCP数据包的最大长度,数据区的内容是长度值。

第4个option的code值是61,它用来表示设备的身份标识,data_length的值根据具体情况而定,通常情况下它是1字节,在我们抓包中它取值7,数据区第一个字节表示硬件类型,接下来6个字节存储设备的mac地址。

第5个option的code值是51,它用来表示ip的租借时长。它的data_length字段占据4个字节,数据区存储的是一个数值,用于表示租借时长的秒数。

第6个option的code值是12,它用来表示设备名称,一般来说是你的主机名称,data_length占据长度根据名字长度而定,数据区存储的是设备名称字符串。

第7个option的code值是255,它表示结束,它只包含1个字节。

接下来我们看看如何使用代码组装该数据包:

package Application;

import java.nio.ByteBuffer;
import java.util.Random;

import datalinklayer.DataLinkLayer;
import protocol.ProtocolManager;

public class DHCPApplication extends Application{
    private static byte  HARDWARE_TYPE = 1;
    private static byte  HARDWARE_ADDR_LENGTH = 6;
    private static byte  DHCP_HOPS = 0;
    private static byte  MESSAGE_TYPE_REQUEST = 1;

    private short  secs_elapsed = 0;
    private short  bootp_flags = 0;

    private byte[] client_ip_address = new byte[4];
    private byte[] your_ip_address = new byte[4];
    private byte[] next_server_ip_address = new byte[4];
    private byte[] relay_agent_ip_address = new byte[4];

    private static byte[] MAGIC_COOKIE = new byte[] {63, 82, 52, 63};

    private static byte [] dhcp_first_part;
    private static int DHCP_FIRST_PART_LENGTH = 236;

    private int transaction_id = 0;

    public DHCPApplication() {
        Random rand = new Random();
        transaction_id = rand.nextInt();

        constructDHCPFirstPart();
    }

    private void constructDHCPFirstPart() {
        dhcp_first_part = new byte[DHCP_FIRST_PART_LENGTH];
        ByteBuffer buffer = ByteBuffer.wrap(dhcp_first_part);
        //设置数据包类型
        buffer.put(MESSAGE_TYPE_REQUEST);
        //设置网络类型
        buffer.put(HARDWARE_TYPE);
        //设置硬件地址长度
        buffer.put(HARDWARE_ADDR_LENGTH);
        //设置数据包跳转次数
        buffer.put(DHCP_HOPS);
        //设置会话id
        buffer.putInt(transaction_id);
        //设置等待时间
        buffer.putShort(secs_elapsed);
        //设置标志位
        buffer.putShort(bootp_flags);
        //设置设备ip
        buffer.put(client_ip_address);
        //设置租借ip
        buffer.put(your_ip_address);
        //设置下一个服务器ip
        buffer.put(next_server_ip_address);
        //设置网关ip
        buffer.put(relay_agent_ip_address);
        //设置硬件地址
        buffer.put(DataLinkLayer.getInstance().deviceMacAddress());
        //填充接下来的10个字节
        byte[] padding = new byte[10];
        buffer.put(padding);
        //设置64字节的服务器名称
        byte[] host_name = new byte[64];
        buffer.put(host_name);
        //设置128位的byte字段
        byte[] file = new byte[128];
        buffer.put(file);
    }

}

上面的代码用于构建数据包不包含option部分的数据。由于他们的内容变动不大,因此单独抽出来构建,option字段部分变动频繁,所以我们要专门处理。接着我们按照DISCOVER数据包构建对应的多个options字段:

private void constructDHCPOptionsPart() {
        //option 53 DHCP Message Type
        byte[] option_msg_type = new byte[OPTION_MSG_TYPE_LENGTH];
        ByteBuffer buffer = ByteBuffer.wrap(option_msg_type);
        buffer.put(OPTION_MSG_TYPE);
        buffer.put(OPTION_MSG_DATA_LENGTH);
        buffer.put(OPTION_MSG_TYPE_DISCOVERY);
        //option 55 Parameter Request List
        byte[] parameter_request_list = new byte[OPTION_PARAMETER_REQUEST_LENGTH];
        buffer = ByteBuffer.wrap(parameter_request_list);
        buffer.put(OPTION_PARAMETER_REQUEST_LIST);
        buffer.put(OPTION_PARAMETER_REQUEST_DATA_LENGTH);
        byte[] option_buffer = new byte[] {OPTIONS_PARAMETER_SUBNET_MASK, OPTIONS_PARAMETER_STATIC_ROUTER,
                OPTIONS_PARAMETER_ROUTER, OPTIONS_PARAMETER_DOMAIN_NAME_SERVER,
                OPTIONS_PARAMETER_DOMAIN_NAME, OPTIONS_PARAMETER_DOMAIN_SEARCH,OPTIONS_PARAMETER_PROXY,OPTIONS_PARAMETER_LDPA,
                OPTIONS_PARAMETER_IP_NAME_SERVER,OPTIONS_PARAMETER_IP_NODE_TYPE};
        buffer.put(option_buffer);

        //option 57 Maximum DHCP Message Size
        byte[] maximun_dhcp_msg_size = new byte[OPTION_MAXIMUN_DHCP_MESSAGE_SIZE_LENGTH];
        buffer = ByteBuffer.wrap(maximun_dhcp_msg_size);
        buffer.put(OPTION_MAXIMUM_DHCP_MESSAGE_SIZE_TYPE);
        buffer.put(OPTION_MAXIMUN_DHCP_MESSAGE_SIZE_DATA_LENGTH);
        buffer.putShort(OPTION_MAXIMUN_DHCP_MESSAGE_SIZE_CONTENT);

        //option 61 Client identifier
        byte[] client_identifier = new byte[OPTION_CLIENT_IDENTIFIER_LENGTH];
        buffer = ByteBuffer.wrap(client_identifier);
        buffer.put(OPTION_CLIENT_IDENTIFIER);
        buffer.put(OPTION_CLIENT_IDENTIFIER_DATA_LENGTH);
        buffer.put(OPTION_CLIENT_IDENTIFIER_HARDWARE_TYPE);
        buffer.put(DataLinkLayer.getInstance().deviceMacAddress());

        //option 51 ip address lease time
        byte[] ip_lease_time = new byte[OPTION_IP_LEASE_TIME_LENGTH];
        buffer = ByteBuffer.wrap(ip_lease_time);
        buffer.put(OPTION_IP_LEASE_TIME);
        buffer.put(OPTION_IP_LEASE_TIME_DATA_LENGTH);
        buffer.putInt(OPTION_IP_LEASE_TIME_CONTENT);

        //option 12 Host Name
        byte[] host_name = new byte[OPTION_HOST_NAME_LENGTH];
        buffer = ByteBuffer.wrap(host_name);
        buffer.put(OPTION_HOST_NAME);
        buffer.put(OPTION_HOST_NAME_DATA_LENGTH);
        buffer.put(OPTION_HOST_NAME_CONTENT);

        //option end
        byte[] end = new byte[1];
        end[0] = OPTION_END;
        byte[] padding = new byte[13];
        dhcp_options_part = new byte[ + option_msg_type.length + parameter_request_list.length + 
                                         maximun_dhcp_msg_size.length + client_identifier.length +
                                         ip_lease_time.length + host_name.length + end.length + padding.length];

        buffer = ByteBuffer.wrap(dhcp_options_part);
        buffer.put(option_msg_type);
        buffer.put(parameter_request_list);
        buffer.put(maximun_dhcp_msg_size);
        buffer.put(client_identifier);
        buffer.put(ip_lease_time);
        buffer.put(host_name);
        buffer.put(end);
        buffer.put(padding);
    }

上面代码目的在于构造DHCP Discover消息的options字段,这些字段包含设备与服务器交互的各种信息,例如规定了数据包的最大长度,设备需要请求哪些网络配置信息等,完成上面代码后,一个DHCP数据请求包就构造完毕,我们再构造IP包头和UDP包头,包裹住上面构造的数据广播到网络上就可以完成DHCP协议的第一步,代码如下:

 public void dhcpDiscovery() {
        byte[] dhcpDiscBuffer = new byte[dhcp_first_part.length + MAGIC_COOKIE.length + dhcp_options_part.length];
        ByteBuffer buffer = ByteBuffer.wrap(dhcpDiscBuffer);
        buffer.put(dhcp_first_part);
        buffer.put(MAGIC_COOKIE);
        buffer.put(dhcp_options_part);

        byte[] udpHeader = createUDPHeader(dhcpDiscBuffer);
        byte[] ipHeader = createIP4Header(udpHeader.length);

        byte[] dhcpPacket = new byte[ udpHeader.length + ipHeader.length];
        buffer = ByteBuffer.wrap(dhcpPacket);
        buffer.put(ipHeader);
        buffer.put(udpHeader);
        //将消息广播出去
        ProtocolManager.getInstance().broadcastData(dhcpPacket);
    }

接着我们在代码主入口处调用上面代码发送我们自己构造的数据包:

   try {
               DHCPApplication dhcpApp = new DHCPApplication();
               dhcpApp.dhcpDiscovery();
           } catch(Exception e) {
               e.printStackTrace();
           }

此时通过wireshark发现,我们发出的数据包以及路由器的应答数据包都被抓取到:

当服务器返回数据包后,我们的程序要接收它,并对它进行读取。DHCPApplication程序设定的端口是68,因此当路由器返回DHCP Offer消息后会被ProtocolManager抓取到,它会根据数据包指向的端口号,将该数据包分发给DHCPApplication,然后后者再对收到的数据进行解读,在ProtocoalManager中增加代码如下:

private void handleUDPPacket(Packet packet, HashMap<String, Object> infoFromUpLayer) {
        IProtocol udpProtocol = new UDPProtocolLayer();
        HashMap<String, Object> headerInfo = udpProtocol.handlePacket(packet);
        short dstPort = (short)headerInfo.get("dest_port");
        //根据端口获得应该接收UDP数据包的程序
        IApplication app = ApplicationManager.getInstance().getApplicationByPort(dstPort);
        app.handleData(headerInfo);
    }

然后进入UDPProtocolLayer增加解析UDP数据包的代码:

@Override
    public HashMap<String, Object> handlePacket(Packet packet) {
        ByteBuffer buffer= ByteBuffer.wrap(packet.header);
        HashMap<String, Object> headerInfo = new HashMap<String, Object>();

        headerInfo.put("src_port", buffer.getShort(UDP_SRC_PORT_OFFSET));
        headerInfo.put("dest_port", buffer.getShort(UDP_DST_PORT_OFFSET));
        headerInfo.put("length", buffer.getShort(UDP_LENGTH_OFFSET));
        headerInfo.put("data", packet.data);

        return headerInfo;
    }

如此一来,UDP协议层从收到的数据包中拿到接收程序对应的端口和数据,ProtocolManager能根据端口将数据返回给对应程序。当UDP层协议对接收数据分析完后,把分析结果提交给DHCPApplication,后者会解析DHCP数据包中的内容:

public void handleData(HashMap<String, Object> headerInfo) {
        byte[] data = (byte[])headerInfo.get("data");
        boolean readSuccess = readFirstPart(data);
        if (readSuccess) {
            readOptions(data);
        }

    }

private boolean readFirstPart(byte[] data) {
        ByteBuffer buffer = ByteBuffer.wrap(data);
        byte reply = buffer.get(DHCP_MSG_TYPE_OFFSET);
        if (reply != DHCP_MSG_REPLY) {
            return false;
        }

        byte[] your_addr = new byte[4];
        buffer.position(DHCP_YOUR_IP_ADDRESS_OFFSET);
        buffer.get(your_addr, 0, your_addr.length);
        System.out.println("available ip offer by dhcp server is: ");
        try {
            InetAddress addr = InetAddress.getByAddress(your_addr);
            System.out.println(addr.getHostAddress());
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }


        buffer.position(DHCP_NEXT_IP_ADDRESS_OFFSET);
        byte[] next_server_addr = new byte[4];
        buffer.get(next_server_addr, 0, next_server_addr.length);
        try {
            InetAddress addr = InetAddress.getByAddress(next_server_addr);
            System.out.println(addr.getHostAddress());
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return true;
    }

DHCPApplication解析接收到的回复数据时分两部分,因为DHCP数据包逻辑上可以分成两部分,一部分是options,另一部分是上面的一系列信息。因此我们解读时也相应分两步走,第一步解读options上面的各种ip信息,第二步解读optins信息:

private void readOptions(byte[] data) {
        ByteBuffer buff = ByteBuffer.wrap(data);
        buff.position(DHCP_OPTIONS_OFFSET);
        while (true) {
            byte type = buff.get();
            if (type == OPTION_END) {
                break;
            }

            switch(type) {
            case DHCP_MSG_TYPE:
                //越过长度字段
                buff.get();
                if (buff.get() == DHCP_MSG_OFFER) {
                    System.out.println("receive DHCP OFFER message from server");
                }
                break;
            case DHCP_SERVER_IDENTIFER:
                printOptionArray("DHCP server identifier:", buff);
                break;
            case DHCP_IP_ADDRESS_LEASE_TIME:
                //越过长度字段
                buff.get();
                int lease_time_secs = buff.getInt();
                System.out.println("The ip will lease to us for " + lease_time_secs + "seconds" );
                break;
            case DHCP_RENEWAL_TIME:
                //越过长度字段
                buff.get();
                int renew_time = buff.getInt();
                System.out.println("we need to renew ip after " + renew_time + "seconds");
                break;
            case DHCP_REBINDING_TIME:
                //越过长度字段
                buff.get();
                int rebinding_time = buff.getInt();
                System.out.println("we need to rebinding new ip after  " + rebinding_time + "seconds");
                break;
            case DHCP_SUBNET_MASK:
                printOptionArray("Subnet mask is : ", buff);
                break;
            case DHCP_BROADCAST_ADDRESS:
                printOptionArray("Broadcasting Address is : ", buff);
                break;
            case DHCP_ROUTER:
                printOptionArray("Broadcasting Address is : ", buff);
                break;
            case DHCP_DOMAIN_NAME_SERVER:
                printOptionArray("Domain name server is : ", buff);
                break;
            case DHCP_DOMAIN_NAME:
                int len = buff.get();
                for(int i = 0; i < len; i++) {
                    System.out.print((char)buff.get() + " ");
                }
                break;
            }
        }
    }

    private void printOptionArray(String content, ByteBuffer buff) {
        System.out.println(content);
        int len = buff.get();
        if (len == 4) {
            byte[] buf = new byte[4];
            for (int i = 0; i < len; i++) {
                buf[i] = buff.get();
            }

            try {
                InetAddress addr = InetAddress.getByAddress(buf);
                System.out.println(addr.getHostAddress());
            } catch (UnknownHostException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } else {
            for (int i = 0; i < len; i++) {
                System.out.print(buff.get() + ".");
            }
        }
        System.out.println("\n");
    }

由于options字段由多个option结构提组合在一起,因此我们用循环依次遍历整个options字段,每次抽取出一个option结构进行解读,一旦解读到code值为255的option时,我们知道所有结构都解读完毕。

上面代码运行后,我们解读DHCP Offer数据包的结果如下:

通过抓包比对可以发现,我们解读的信息与wireshark抓包获得的信息完全一致,如此我们就完成了DHCP协议第一步:设备询问服务器,服务器回应设备查询!在此基础上我们可以进一步完成后续协议内容。

原文发布于微信公众号 - Coding迪斯尼(gh_c9f933e7765d)

原文发表时间:2019-03-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券