一个有趣的网络程序TraceRoute:记录数据包传送路径上的路由器IP

在大多数操作系统上都附带一个网络程序叫TraceRoute,它的作用是追踪数据包发送到指定对象前,在传送路径上经过了几个路由器转发,下图是用TraceRoute程序追踪从我这台主机发送数据包到百度服务器时所经过的各个路由器的ip:

屏幕快照 2019-02-21 下午5.38.48.png

其中14.215.177.38是域名www.baidu.com对应的服务器ip,从显示上看,数据包从我当前电脑发出,经过7个路由器后才能到达百度服务器,本节我们就看看traceroute应用程序的实现原理。

整个互联网其实是由一个个子网组成的,每个子网相当于一个孤岛,每个孤岛对应一个路由器,两个孤岛间的路由器如果相互连通,那么就相当于在孤岛上架起一座桥梁,于是两座孤岛就可以相互连通,整个互联网就是无数个孤岛通过路由器连接起来的一个巨大整体:

屏幕快照 2019-02-21 下午5.42.43.png

如上图当我们想把数据发送到远端服务器时,数据包从我们所在的“孤岛”通过路由器跳转到下一个孤岛,如果接收目标没有在进入的新孤岛,那么第二个孤岛的路由器会将数据包通过它的路由器提交到第三个孤岛,如此一直传递直到数据包抵达接收目标所在的孤岛,然后对应孤岛的路由器将数据包分发给接收目标。

在IP数据包头中有一个字段,用来记录数据包可以跳转的孤岛数量:


屏幕快照 2018-12-21 下午5.37.41.png

上面显示的是IP数据包头的格式,其中有一个字段是Time To Live,简称TTL,它规定了该数据包可以跳转的孤岛数量,数据包每跳转一个孤岛,该字段的值就减1,如果当该字段的值减到0数据包还没有抵达目标所在的孤岛,那么该孤岛对应的路由器就会向数据包的发送者发出一个由ICMP协议封装的数据包叫ICMP Time Exceeded Message,该数据包的格式如下:

屏幕快照 2019-02-21 下午5.50.39.png

其中type取值11,code取值为0.

traceroute就是利用这个特性来检测数据包发送路径上所经过的路由器。首先它构造一个UDP数据包发送给接收目标,并在数据包的IP报头里将TTL字段设置成1,于是数据包发送给第一个孤岛时,对方回发一个Time Exceeded消息,通过该消息的IP报头它就可以知道第一个孤岛路由器的ip,接着它再次构造同样的UDP数据包只是把其中IP报头的TTL字段改成2,于是同理获得第二个孤岛对应路由器的IP,如此反复进行,直到再也没有Time Exceeded消息返回为止,于是它便可以得知数据包发送给指定目标时,路径上经过了多少路由器转发。

我们看看如何动手实现traceroute程序功能,在这里我们需要使用一个新协议包头,也就是UDP包头,它的具体原理我们在以后的课程中会详细研究,现在我们只需要知道它的包头格式即可:

屏幕快照 2019-02-21 下午5.57.22.png

首先我们新建一个类成为TraceRoute,其代码如下:

package Application;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.HashMap;

import protocol.IProtocol;
import protocol.ProtocolManager;
import protocol.UDPProtocolLayer;

public class TraceRoute  extends Application{
    
    private char dest_port = 33434;
    private byte[] dest_ip = null;
    private byte time_to_live = 1;
    
    private static byte ICMP_TIME_EXCEEDED_TYPE = 1;
    private static byte ICMP_TIME_EXCEEDED_CODE = 0;
    
    public TraceRoute( byte[] destIP) {
        this.dest_ip = destIP;  
    }
    
    public void startTraceRoute() {
        try {
            byte[] packet = createPackage(null);
            ProtocolManager.getInstance().sendData(packet, dest_ip);
            
            ProtocolManager.getInstance().registToReceiveICMPPacket(this);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    private byte[] createPackage(byte[] data) throws Exception {
        byte[] udpHeader = this.createUDPHeader();
        if (udpHeader == null) {
            throw new Exception("UDP Header create fail");
        }       
        byte[] ipHeader = this.createIP4Header(udpHeader.length);
        
        //分别构建ip包头和icmp echo包头后,将两个包头结合在一起
        byte[] packet  = new byte[udpHeader.length + ipHeader.length];
        ByteBuffer packetBuffer = ByteBuffer.wrap(packet);
        packetBuffer.put(ipHeader);
        packetBuffer.put(udpHeader);
        
        return packetBuffer.array();
    }
    
    private byte[] createUDPHeader() {
        IProtocol udpProto = ProtocolManager.getInstance().getProtocol("udp");
        if (udpProto == null) {
            return null;
        }
        
        HashMap<String, Object> headerInfo = new HashMap<String, Object>();
        char udpPort = (char)this.port;
        headerInfo.put("source_port", udpPort);
        headerInfo.put("dest_port", dest_port);
        
        byte[] data = new byte[24];
        headerInfo.put("data", data);
        
        return udpProto.createHeader(headerInfo);
    }
    
    protected byte[] createIP4Header(int dataLength) {
        IProtocol ip4Proto = ProtocolManager.getInstance().getProtocol("ip");
        if (ip4Proto == null || dataLength <= 0) {
            return null;
        }
        //创建IP包头默认情况下只需要发送数据长度,下层协议号,接收方ip地址
        HashMap<String, Object> headerInfo = new HashMap<String, Object>();
        headerInfo.put("data_length", dataLength);
        ByteBuffer destIP = ByteBuffer.wrap(this.dest_ip);
        headerInfo.put("destination_ip", destIP.getInt());
        byte protocol = UDPProtocolLayer.PROTOCOL_UDP;
        headerInfo.put("protocol", protocol);
        headerInfo.put("identification", (short)this.port);
        //该值必须依次递增
        headerInfo.put("time_to_live", time_to_live);
        byte[] ipHeader = ip4Proto.createHeader(headerInfo);
        
        
        return ipHeader;
        
    }
    
    public void handleData(HashMap<String, Object> data) {
        if (data.get("type") == null || data.get("code") == null) {
            return;
        }
        
        if ((byte)data.get("type") != ICMP_TIME_EXCEEDED_TYPE ||
            (byte)data.get("code") != ICMP_TIME_EXCEEDED_CODE) {
            //收到的不是icmp_time_exceeded类型的消息
            return;
        }
        
        //获得发送该数据包的路由器ip
        byte[] source_ip = (byte[])data.get("source_ip");
        try {
            String routerIP = InetAddress.getByAddress(source_ip).toString();
            System.out.println("ip of the " + time_to_live + "th router in sending route is: " + routerIP );
            dest_port++;
            time_to_live++;
            startTraceRoute();
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    
    }

}

它的实现与我们前面开发的HPing类似,都是构造好相应协议包头后,让网卡将数据包发送出去,在这里有一点不同之处在于,它需要构造UDP包头,同时它在构造IP包头的时候,特意把time to live字段的值设置为1,这样能让数据包进入下一个孤岛时收到对应路由器发回来的time exceeded limit 消息。

一旦对应的icmp消息发回来并被本机接收后,handleData接口会被调用,它把发送消息的路由器ip打印出来,然后让time_to_live的值加1,并再次发送数据包,于是数据包能连续进入新孤岛,那么第二个孤岛的路由器回发time exceeded limit消息时,我们就能获得它的ip,这个过程一直进行,直到再也没有time exceeded limit消息回来为止。

接着我们增加一个名为UDPProtocoalLayer的对象,它负责构造UDP包头,其实现代码如下:

package protocol;

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

import jpcap.packet.Packet;

public class UDPProtocolLayer implements IProtocol{

    private static short  UDP_LEHGTH_WITHOUT_DATA = 8;
    public static byte PROTOCOL_UDP = 17;
    
    @Override
    public byte[] createHeader(HashMap<String, Object> headerInfo) {
        short total_length = UDP_LEHGTH_WITHOUT_DATA;
        byte[] data = null;
        if (headerInfo.get("data") != null) {
            data = (byte[])headerInfo.get("data");
            total_length += data.length;
        }
        
        byte[] buf = new byte[total_length];
        ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
        
        if (headerInfo.get("source_port") == null) {
            return null;
        }
        char srcPort = (char)headerInfo.get("source_port");
        byteBuffer.order(ByteOrder.BIG_ENDIAN);
        byteBuffer.putChar(srcPort);
        
        if (headerInfo.get("dest_port") == null) {
            return  null;
        }
        char  destPort = (char)headerInfo.get("dest_port");
        byteBuffer.order(ByteOrder.BIG_ENDIAN);
        byteBuffer.putChar(destPort);
        
        byteBuffer.order(ByteOrder.BIG_ENDIAN);
        byteBuffer.putShort(total_length);
        //UDP包头的checksum可以直接设置成0xFFFF
        char checksum = 65535;
        byteBuffer.putChar(checksum);
        
        if (data != null) {
            byteBuffer.put(data);
        }
        
        
        return byteBuffer.array();
    }
    


    @Override
    public HashMap<String, Object> handlePacket(Packet packet) {
        // TODO Auto-generated method stub
        return null;
    }

}

它的实现功能简单,仅仅是按照UDP数据包头的格式组装包头而已。然后我们增加一个解读icmp time exceeded limit错误消息的对象,其代码如下:

package protocol;

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

import jpcap.packet.Packet;

public class ICMPTimeExceededHeader implements IProtocol{
    private static byte ICMP_TIME_EXCEEDED_TYPE = 1;
    private static byte ICMP_TIME_EXCEEDED_CODE = 0;
    private static int ICMP_TIME_EXCEEDED_DATA_OFFSET = 8;
    
    @Override
    public byte[] createHeader(HashMap<String, Object> headerInfo) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public HashMap<String, Object> handlePacket(Packet packet) {
        ByteBuffer buffer = ByteBuffer.wrap(packet.header);
        if (buffer.get(0) != ICMP_TIME_EXCEEDED_TYPE &&
            buffer.get(1) !=    ICMP_TIME_EXCEEDED_CODE) {
            return  null;
        }
        
        HashMap<String, Object> headerInfo = new HashMap<String, Object>();
        headerInfo.put("type", ICMP_TIME_EXCEEDED_TYPE);
        headerInfo.put("code", ICMP_TIME_EXCEEDED_CODE);
        
        byte[] data = new byte[packet.header.length - ICMP_TIME_EXCEEDED_DATA_OFFSET];
        buffer.position(ICMP_TIME_EXCEEDED_DATA_OFFSET);
        buffer.get(data, 0, data.length);
        headerInfo.put("data", data);
        return headerInfo;
    }

}

它的实现原理也很简单,仅仅是按照给定格式抽取出相应字段而已。由于篇幅所限,还有很多代码没有展现出来,更详细的讲解和代码调试演示过程,请点击链接

当上面代码完成后,运行程序,并用wireshark抓包,得到情况如下:


屏幕快照 2019-02-21 下午6.08.34.png

它表明我们的代码正确的构造了数据包,并准确的触发icmp time exceeded limit数据包的回发,然后我们观察到程序运行时会将路径上锁经过的路由器IP打印出来:

屏幕快照 2019-02-21 下午6.10.27.png

由此可见,我们通过程序实现准确的发包和收包,进而完美的再现traceroute的内部运行原理。

更详细的讲解和代码调试演示过程,请点击链接

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


这里写图片描述
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容