Java 杂谈

ICMP timestamp 协议原理和实现

2019-02-09  本文已影响16人  望月从良

大量的设备连接到互联网,而设备之间需要通讯以及相互协作,这就要求不同设备之间需要保持时钟同步。然而无论是多么精准的时钟,一旦运行久了后,它指示的时间与真实时间会产生偏差,在假设不同设备启动时时间初始化不同,这就很难保证不同设备之间的时钟频率还能够保持一致。

TCP/IP协议在创建时就已经意识到,不同设备间一旦时钟不同频就可能导致相互协作出现问题。于是就想构造出一套协议让不同设备间相互协商,然后确保设备间在时间上能保持步调一致,而这个任务就落到ICMP协议头上。

发起同步的设备产生一个时间戳,然后利用ICMP消息体和协议规则,将时间戳发送给接收设备,这就是一个timestamp request消息。接收设备收到消息后返回自己的时间戳,这就是timestamp reply 消息。发出者的时间戳和接收者的时间戳就可以让两个设备之间保持时钟同步。

产生ICMP timestamp 消息的应用叫hping,一般来说系统可能不自带,需要我们自行安装,我们macos上安装hping后就可以通过下面命令产生timestamp消息:

sudo hping 192.168.2.1  --icmp --icmp-ts -V

通过wireshark抓包就可以看到消息发送和接收状况:

屏幕快照 2019-02-08 下午3.39.06.png

我们看看timestamp消息的结构:

屏幕快照 2019-02-08 下午3.40.00.png

它其实是我们上次实现ping应用的翻版,只不过多加了几个数据项。其中它的type值域对于request来说要设置成12,对于reply要设置成14,sequence number 和identifier 意义与上一节描述的一样,original timestamp 用于发起者填写自己的当前时间,接下来的两个时间戳由接收者填写,Receive timestamp是接收者收到消息时的时间,Transmit timestamp是接收者回发消息时的时间。

这里可能让人有点出乎意料的是,返回消息中包含两个时间戳而不是一个。这是因为发起者收到返回消息后,它能知道发出消息的接收时间和回应消息的发出时间,这样发起者就能知道消息传达到指定设备需要的时间,从而对当前网络流量有了解,同时指定收到设备的消息处理速度,从而能够根据具体情况决定下面消息发送的速度和流量。

我们看看消息组成的数据格式:


屏幕快照 2019-02-08 下午3.54.41.png

在实践上,即使有这样的协议,不同设备之间的同频依然难以实现。因为ICMP基于IP之上,而IP协议本质上是不可靠的,因此发出去的消息很可能会遗失,抵达不了目的设备,因此如此简单的交换两个设备的时间戳根本保障不了不同设备之间的同步,于是有更强大的协议用于保障时间同步,那就是NTP,以后我们会介绍到。

由于该协议与前面我们实现的ping协议非常相近,因此我们可以在上一节的基础上进行修改就可以完成。首先我们把上一节Ping应用里的createICMPEchoHeader改成createICMPHeader,以便用于继承,然后创建一个新类名为hping,继承自上一节的PingApp:

package Application;

import java.nio.ByteBuffer;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.HashMap;
import java.util.TimeZone;

import javax.xml.crypto.Data;

import protocol.IProtocol;
import protocol.ProtocolManager;

public class HPingApp extends PingApp{
    private int send_time = 0;
    
    public HPingApp(int times, byte[] destIP) {
        super(times, destIP);
    }
    
    protected byte[] createICMPHeader() {
        IProtocol icmpProto = ProtocolManager.getInstance().getProtocol("icmp");
        if (icmpProto == null) {
            return null;
        }
        //构造icmp echo 包头
        HashMap<String, Object> headerInfo = new HashMap<String, Object>();
        headerInfo.put("header", "timestamp");
        headerInfo.put("identifier", identifier);
        headerInfo.put("sequence_number", sequence);
        sequence++;
        //获取UTC时间
        Calendar rightNow = Calendar.getInstance();
        int hour = rightNow.get(Calendar.HOUR_OF_DAY);
        int minutes = rightNow.get(Calendar.MINUTE);
        int secs = rightNow.get(Calendar.SECOND);
        
        send_time = (hour * 3600 + minutes * 60 + secs) * 1000;
       
        headerInfo.put("original_time", send_time);
        int receive_time = 0, transmit_time = 0;
        headerInfo.put("receive_time", receive_time);
        headerInfo.put("transmit_time", transmit_time);
        
        byte[] icmpEchoHeader = icmpProto.createHeader(headerInfo);
        
        return icmpEchoHeader;
    }
    
    public void handleData(HashMap<String, Object> data) {
        short sequence = (short)data.get("sequence");
        int receive_time = (int)data.get("receive_time");
        System.out.println("receive time  for timestamp request " + sequence + "for  " + (send_time - receive_time) / 1000 + "secs");
        
        int transmit_time = (int)data.get("transmit_time");
        System.out.println("receive reply for ping request " + sequence + "for  " + (send_time - transmit_time) / 1000 + "secs");
    }
}

接下来我们创建一个构造ICMP tiemstamp request 协议包头的类:

package protocol;

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

import jpcap.packet.Packet;
import utils.Utility;

public class ICMPTimeStampHeader implements IProtocol{
    private static int ICMP_TIMESTAMP_HEADER_LENGTH = 20;
    private static byte ICMP_TIMESTAMP_REQUEST_TYPE = 13;
    private static byte ICMP_TIMESTAMP_REPLY_TYPE = 14;

    private static short ICMP_ECHO_IDENTIFIER_OFFSET = 4;
    private static short ICMP_ECHO_SEQUENCE_NUM_OFFSET = 6;
    private static short ICMP_ORIGINAL_TIMESTAMP_OFFSET = 8;
    private static short ICMP_RECEIVE_TIMESTAMP_OFFSET = 12;
    private static short ICMP_TRANSMIT_TIMESTAMP_OFFSET = 16;
    
    
    @Override
    public byte[] createHeader(HashMap<String, Object> headerInfo) {
        String headerName = (String)headerInfo.get("header");
        if (headerName != "timestamp") {
            return null;
        }
        
        int bufferLen = ICMP_TIMESTAMP_HEADER_LENGTH;
        byte[] buffer = new byte[bufferLen ];
        ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
        
        byte type = ICMP_TIMESTAMP_REQUEST_TYPE;
        byteBuffer.put(type);
        byte code = 0;
        byteBuffer.put(code);
        
        short checkSum = 0;
        byteBuffer.order(ByteOrder.BIG_ENDIAN);
        byteBuffer.putShort(checkSum);
        
        short identifier = 0;
        if (headerInfo.get("identifier") == null) {
            Random ran = new Random();
            identifier = (short) ran.nextInt();
            headerInfo.put("identifier", identifier);
        }
        identifier = (short) headerInfo.get("identifier");
        byteBuffer.order(ByteOrder.BIG_ENDIAN);
        byteBuffer.putShort(identifier);
        
        short sequenceNumber = 0;
        if (headerInfo.get("sequence_number") != null) {
            sequenceNumber = (short) headerInfo.get("sequence_number");
        }
        headerInfo.put("sequence_number", sequenceNumber);
        byteBuffer.order(ByteOrder.BIG_ENDIAN);
        byteBuffer.putShort(sequenceNumber);
    
        
        if (headerInfo.get("original_time") != null) {
            int original_time = (int)headerInfo.get("original_time");
            byteBuffer.putInt(original_time);
        }
        
    
        if (headerInfo.get("receive_time") != null) {
            int receive_time = (int)headerInfo.get("receive_time");
            byteBuffer.putInt(receive_time);
        }
        
        if (headerInfo.get("transmit_time") != null) {
            int transmit_time = (int)headerInfo.get("transmit_time");
            byteBuffer.putInt(transmit_time);
        }
        
        
        checkSum = (short) Utility.checksum(byteBuffer.array(), byteBuffer.array().length);
        byteBuffer.order(ByteOrder.BIG_ENDIAN);
        byteBuffer.putShort(2, checkSum);
        System.out.println("ICMP timestamp header, checksum: " + String.format("0x%08x", checkSum));
        
        return byteBuffer.array();
    }

    @Override
    public HashMap<String, Object> handlePacket(Packet packet) {
        ByteBuffer buffer = ByteBuffer.wrap(packet.header);
        if (buffer.get(0) != ICMP_TIMESTAMP_REPLY_TYPE) {
            return null;
        }
        
        HashMap<String, Object> header = new HashMap<String, Object>();
        header.put("identifier", buffer.getShort(ICMP_ECHO_IDENTIFIER_OFFSET));
        header.put("sequence", buffer.getShort(ICMP_ECHO_SEQUENCE_NUM_OFFSET));
        
        header.put("original_time", buffer.getInt(ICMP_ORIGINAL_TIMESTAMP_OFFSET));
        header.put("receive_time", buffer.getInt(ICMP_RECEIVE_TIMESTAMP_OFFSET));
        header.put("transmit_time", buffer.getInt(ICMP_TRANSMIT_TIMESTAMP_OFFSET));
        
        return header;
    }

}

然后在ICMPProtocolLayer中添加上面的对象:

 public ICMPProtocolLayer() {
        //增加icmp echo 协议包头创建对象
        protocol_header_list.add(new ICMPEchoHeader());
        
        protocol_header_list.add(new ICMPTimeStampHeader());
    }

完成代码后,我们将请求发送给本地路由器,然后通过wireshark抓包可以看到发出去的消息如下图:

屏幕快照 2019-02-09 下午5.42.44.png

回复消息如下图:

屏幕快照 2019-02-09 下午5.44.15.png

更多视频讲解和代码调试演示请参看视频:
更详细的讲解和代码调试演示过程,请点击链接

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:


这里写图片描述
上一篇下一篇

猜你喜欢

热点阅读