從0到1用java再造tcpip協議棧:實現ARP協議層

經過前兩節的準備,我們完成了數據鏈路層,已經具備了數據包接收和發送的基礎設施,本機我們在此基礎上實現上層協議,我們首先從實現ARP協議開始。先簡單認識一下ARP協議,ARP是一種尋址協議,它要找尋目標的物理地址,連接在互聯網上的設備有兩種地址,一種叫IP,也就是我們常見的192.168.2.1這類地址,另一種叫物理地址,例如我們電腦上的mac地址。

為何要使用兩種地址呢?這類似與個人的名字與身份證號的區別,名字是可以重復的,例如叫”張三“的肯定不止一個人,當一件包裹寄到一棟樓里如果樓內有多個人叫張三,那么此時就需要身份證號來辨別哪個張三才是包裹的收件人。在網絡上傳輸數據時,ip對應一個區域網,該區域網內有多臺連接設備,當數據包根據ip找到對應區域網后,如何知道區域網上的哪臺設備是數據包的接收者呢?這時就得看硬件地址,只有與數據包附帶的硬件地址相符合的設備才應該接收到來的數據包,如此一來數據包要正確發生并接收,那就得知道兩個地址,一個是ip,一個是硬件地址,對應互聯網設備而言,通常就是mac地址,而ARP協議就是專門用來獲得接收對象的mac地址的。

網絡協議的本質其實就是填表單。ARP協議的實現也是填寫一系列表單,發給對方,對方根據表單要求也填寫一張表單發回來,我們看看這張表單的結構:

屏幕快照 2018-12-13 下午4.36.23.png

這張表上頭的0-32單位是比特位而不是字節,要注意。根據表單所示,前16個比特位也就是前2個字節對于的是硬件類型,也就是傳送數據的網絡,其取值情況如下:
1:10MB以太網;6:IEEE802網絡;7:ARCNET;16:ATM...
它還有其他取值,為了簡便我沒有羅列出來,由于我們默認在互聯網上收發數據,因此填表時這兩個字節寫死為1。

接下兩字節也就是protocoal type,表示數據傳輸使用的網絡協議,如果數據包使用IP定位接收目標所在的局域網,那么該值寫死為0x0800,我們實現的協議也是把這兩個字節寫死。

接下來是兩個單字節用于表示兩種地址的長度,我們默認收發數據包的設備都是mac地址,因此Hardware Adress Length這個字節寫死為6,同理我們默認設備都使用IP地址,因此protocal Address Length這個字節寫死為4.

接著兩字節是OpCode,用來表示消息目的。1表示請求,當A向B發出ARP請求希望獲得B的mac地址時,A構造這張表單時在該字節填寫1。2表示回應,當B收到請求,向A返回同樣格式的表單,此時它在該字節填寫2,同時把自己的硬件地址填寫在表單里。

接下來是Sender Hardware Address,它用來存儲發送者的硬件地址,其長度與Hardware Address Length中表示的一致,在我們實現中,它用來存儲發生者的mac地址,因此占據6個字節。

接著的Sender Protocol Address表示發送者的IP地址,因此占據4字節。

Target Hardware Address是接收者的mac地址,占據6字節,最后是接收者的IP地址,占據4字節。

當表單填好后,數據鏈路層在發送出去前還會再加上一個包頭,包頭有14個字節,前6個字節表示接收者的mac地址,接著6個字節表示發送者的mac地址,然后有2字節表示包的類型,如果發送的是ARP包,那么這2字節的值為0x0806,如果發送的是IP包,那么值為0x0800,當網卡接收到數據包后,它會檢測這兩個字節,根據數值把前14字節的數據鏈路包頭去除后,將剩下的數據提交給對應的網絡協議層,因此ARP包經過鏈路層封裝后發送時格式如下:

屏幕快照 2018-12-13 下午4.55.26.png

接下來我們看看代碼實現,首先我們需要對上節模擬的數據鏈路層做一些修改:

package datalinklayer;


import jpcap.NetworkInterfaceAddress;
import jpcap.packet.EthernetPacket;
import jpcap.packet.Packet;
import utils.IMacReceiver;
import utils.PacketProvider;

import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;

import ARPProtocolLayer.ARPProtocolLayer;
import jpcap.JpcapCaptor;
import jpcap.JpcapSender;
import jpcap.NetworkInterface;

public class DataLinkLayer extends PacketProvider implements jpcap.PacketReceiver, IMacReceiver{   
        //change 1
        private static DataLinkLayer instance = null;
        private NetworkInterface device = null;
        private Inet4Address ipAddress = null;
        private byte[] macAddress = null;
        JpcapSender sender = null;
        
        private DataLinkLayer() {
           
        }
        
        public static DataLinkLayer getInstance() {
            if (instance == null) {
                instance = new DataLinkLayer();
            }
            
            return instance;
        }
        
        // change 2
        public void initWithOpenDevice(NetworkInterface device) {
            this.device = device;   
            this.ipAddress = this.getDeviceIpAddress();
            this.macAddress = new byte[6];
            this.getDeviceMacAddress();
            
            JpcapCaptor captor = null;
            try {
                captor = JpcapCaptor.openDevice(device,2000,false,3000);
            } catch (IOException e) {
                e.printStackTrace();
            }
            
            this.sender = captor.getJpcapSenderInstance();
            
            //測試arp協議
            this.testARPProtocol();
        }
        
        private Inet4Address getDeviceIpAddress() {
            for (NetworkInterfaceAddress addr  : this.device.addresses) {
                  //網卡網址符合ipv4規范才是可用網卡
                  if (!(addr.address instanceof Inet4Address)) {
                      continue;
                  }
                  
                  return (Inet4Address) addr.address;
            }
            
            return null;
        }
        
        private void getDeviceMacAddress() {
            int count = 0;
            for (byte b : this.device.mac_address) {
                this.macAddress[count] = (byte) (b & 0xff);
                count++;
            }
        }
        
        // change 3
        public  byte[] deviceIPAddress() {
            return this.ipAddress.getAddress();
        }
        
        public byte[] deviceMacAddress() {
            return this.macAddress;
        }
        
       
        @Override
        public void receivePacket(Packet packet) {
            //將受到的數據包推送給上層協議
            this.pushPacketToReceivers(packet);
        }
        
        public void sendData(byte[] data, byte[] dstMacAddress, short frameType) {
            /*
             * 給上層協議要發送的數據添加數據鏈路層包頭,然后使用網卡發送出去
             */
            if (data == null) {
                return;
            }
            
            Packet packet = new Packet();
            packet.data = data;
            
            /*
             * 數據鏈路層會給發送數據添加包頭:
             * 0-5字節:接受者的mac地址
             * 6-11字節: 發送者mac地址
             * 12-13字節:數據包發送類型,0x0806表示ARP包,0x0800表示ip包,
             */
            
            EthernetPacket ether=new EthernetPacket();
            ether.frametype = EthernetPacket.ETHERTYPE_ARP;
            ether.src_mac= this.device.mac_address;
            ether.dst_mac= dstMacAddress;
            packet.datalink = ether;
            
            sender.sendPacket(packet);
        }
        
     private void testARPProtocol() {
         ARPProtocolLayer arpLayer = new ARPProtocolLayer();
         this.registerPacketReceiver(arpLayer);
         
         byte[] ip;
        try {
            ip = InetAddress.getByName("192.168.2.1").getAddress();
            arpLayer.getMacByIP(ip, this);
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
         
     }

    @Override
    public void receiveMacAddress(byte[] ip, byte[] mac) {
        System.out.println("receive arp reply msg with sender ip: ");
        for (byte b: ip) {
            System.out.print(Integer.toUnsignedString(b & 0xff) + ".");
        }
        System.out.println("with sender mac :");
        for (byte b : mac)
            System.out.print(Integer.toHexString(b&0xff) + ":");
        
    }
}

首先它改成單子模式,一次只生成一個實力。它通過jpcap獲得網卡對象,然后得到本機mac地址和ip地址,同時導出一個接口叫sendData,該接口從上層接收要發送的數據,然后封裝一個數據鏈路層包頭后,調用網卡將數據發送出去。它繼承PacketProvider類,后者是一個觀察者模式的實現,所有想獲得網絡數據包的對象都通過PacketProvider注冊,一旦網卡收到數據后,PacketProvider就會把數據包推送給所有觀察者,后者實現如下:

package utils;

public interface IPacketProvider {
    public void registerPacketReceiver(jpcap.PacketReceiver receiver);
}


package utils;

import java.util.ArrayList;

import jpcap.PacketReceiver;
import jpcap.packet.Packet;

public class PacketProvider implements IPacketProvider{
    private ArrayList<PacketReceiver> receiverList = new ArrayList<PacketReceiver>();
    
    @Override
    public void registerPacketReceiver(PacketReceiver receiver) {
        if (this.receiverList.contains(receiver) != true) {
            this.receiverList.add(receiver);
        }
    }
    
    @SuppressWarnings("unused")
    protected void pushPacketToReceivers(Packet packet) {
        for (int i = 0; i < this.receiverList.size(); i++) {
            PacketReceiver receiver = (PacketReceiver) this.receiverList.get(i);
            receiver.receivePacket(packet);
        }
    }

}

接著我們實現ARP協議層。我們在實現ARP協議時,除了按規定填表和讀表外,我們還需要做的工作是提供緩存機制。由于發送數據包再等待回應是一種非常耗時的工作,因此完成后要把結果緩存起來,下次需要時不用再進行耗時的數據收發工作,因此我們在實現時會準備一個映射表,將ip和mac地址緩存起來,當查找指定ip設備的mac地址時,現在表中查找,如果找不到在進行數據包的發送接收,相關的代碼實現如下:

package ARPProtocolLayer;

import java.net.Inet4Address;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

import datalinklayer.DataLinkLayer;
import jpcap.PacketReceiver;
import jpcap.packet.ARPPacket;
import jpcap.packet.EthernetPacket;

import jpcap.packet.Packet;
import utils.IMacReceiver;

public class ARPProtocolLayer implements PacketReceiver {
    /*
     */
    private HashMap<byte[], byte[]> ipToMacTable = new HashMap<byte[], byte[]>();
    private HashMap<Integer, ArrayList<IMacReceiver>> ipToMacReceiverTable = new   HashMap<Integer, ArrayList<IMacReceiver>>();
    /*
     * 數據包含數據鏈路層包頭:dest_mac(6byte) + source_mac(6byte) + frame_type(2byte)
     * 因此讀取ARP數據時需要跳過開頭14字節
     */
    private static int ARP_OPCODE_START = 20;
    private static int ARP_SENDER_MAC_START = 22;
    private static int ARP_SENDER_IP_START = 28;
    private static int ARP_TARGET_IP_START = 38;
    
   
    @Override
    public void receivePacket(Packet packet) {
        if (packet == null) {
            return;
        }
        
        //確保收到數據包是arp類型
        EthernetPacket etherHeader = (EthernetPacket)packet.datalink;
        /*
         * 數據鏈路層在發送數據包時會添加一個802.3的以太網包頭,格式如下
         * 0-7字節:[0-6]Preamble , [7]start fo frame delimiter
         * 8-22字節: [8-13] destination mac, [14-19]: source mac 
         * 20-21字節: type
         * type == 0x0806表示數據包是arp包, 0x0800表示IP包,0x8035是RARP包
         */
        if (etherHeader.frametype != EthernetPacket.ETHERTYPE_ARP) {
            return;
        }
        byte[] header = packet.header;
        analyzeARPMessage(header);
    }
    
    private  boolean analyzeARPMessage(byte[] data) {
        /*
         * 解析獲得的APR消息包,從中獲得各項信息,此處默認返回的mac地址長度都是6
         */
        //先讀取2,3字節,獲取消息操作碼,確定它是ARP回復信息
        byte[] opcode = new byte[2];
        System.arraycopy(data, ARP_OPCODE_START, opcode, 0, 2);
        //轉換為小端字節序
        short op = ByteBuffer.wrap(opcode).getShort();
        if (op != ARPPacket.ARP_REPLY) {
            return false;
        }
        
        //獲取接受者ip,確定該數據包是回復給我們的
        byte[] ip = DataLinkLayer.getInstance().deviceIPAddress();
        for (int i = 0; i < 4; i++) {
            if (ip[i] != data[ARP_TARGET_IP_START + i]) {
                return false;
            }
        }
        
        //獲取發送者IP
        byte[] senderIP = new byte[4];
        System.arraycopy(data, ARP_SENDER_IP_START, senderIP, 0, 4);
        //獲取發送者mac地址
        byte[] senderMac = new byte[6];
        System.arraycopy(data, ARP_SENDER_MAC_START, senderMac, 0, 6);
        //更新arp緩存表
        ipToMacTable.put(senderIP, senderMac);
        
        
        //通知接收者mac地址
        int ipToInteger = ByteBuffer.wrap(senderIP).getInt();
        ArrayList<IMacReceiver> receiverList = ipToMacReceiverTable.get(ipToInteger);
        if (receiverList != null) {
            for (IMacReceiver receiver : receiverList) {
                receiver.receiveMacAddress(senderIP, senderMac);
            }
        }
        return true;
    }
    
     
    public void  getMacByIP(byte[] ip, IMacReceiver receiver) {
        if (receiver == null) {
            return;
        }
        //查看給的ip的mac是否已經緩存
        int ipToInt = ByteBuffer.wrap(ip).getInt();
        if (ipToMacTable.get(ipToInt) != null) {
            receiver.receiveMacAddress(ip, ipToMacTable.get(ipToInt));
        }
        
        if (ipToMacReceiverTable.get(ipToInt) == null) {
            ipToMacReceiverTable.put(ipToInt, new ArrayList<IMacReceiver>());
            //發送ARP請求包
            sendARPRequestMsg(ip);
        }
        ArrayList<IMacReceiver> receiverList = ipToMacReceiverTable.get(ipToInt);
        if (receiverList.contains(receiver) != true) {
            receiverList.add(receiver);
        }
        
        return;
    }
    
    private void sendARPRequestMsg(byte[] ip) {
        if (ip == null) {
            return;
        }
        
        DataLinkLayer dataLinkInstance = DataLinkLayer.getInstance();
        byte[] broadcast=new byte[]{(byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255};
        int pointer = 0;
        byte[] data = new byte[28];
        data[pointer] = 0;
        pointer++;
        data[pointer] = 1;
        pointer++;
        //注意將字節序轉換為大端
        ByteBuffer buffer = ByteBuffer.allocate(2);
        buffer.order(ByteOrder.BIG_ENDIAN);
        buffer.putShort(ARPPacket.PROTOTYPE_IP);
        for (int i = 0; i < buffer.array().length; i++) {
            data[pointer] = buffer.array()[i];
            pointer++;
        }
    
        data[pointer] = 6;
        pointer++;
        data[pointer] = 4;
        pointer++;
        //注意將字節序轉換為大端
        buffer = ByteBuffer.allocate(2);
        buffer.order(ByteOrder.BIG_ENDIAN);
        buffer.putShort(ARPPacket.ARP_REQUEST);
        for (int i = 0; i < buffer.array().length; i++) {
            data[pointer] = buffer.array()[i];
            pointer++;
        }
        
        byte[] macAddress = dataLinkInstance.deviceMacAddress();
        for (int i = 0; i < macAddress.length; i++) {
            data[pointer] = macAddress[i];
            pointer++;
        }
        
        byte[] srcip = dataLinkInstance.deviceIPAddress();
        for (int i = 0; i < srcip.length; i++) {
            data[pointer] = srcip[i];
            pointer++;
        }
        for (int i = 0; i < broadcast.length; i++) {
            data[pointer] = broadcast[i];
            pointer++;
        }
        for (int i = 0; i < ip.length; i++) {
            data[pointer] = ip[i];
            pointer++;
        }

        dataLinkInstance.sendData(data, broadcast, EthernetPacket.ETHERTYPE_ARP);
    }
}

getMacByIP是它提供給上層協議的接口,當上層協議需要獲得指定ip設備的mac地址時就調用該接口,它先從緩存表中看指定ip對應的mac地址是否存在,如果不存在就調用sendARPRequestMsg發送ARP請求包。

sendARPRequestMsg的實現其實就是按照我們前面描述的填表規則進行填表。值得注意的是,我們把接收者的mac地址設置成[0xff, 0xff, 0xff, 0xff, 0xff, 0xff],這是一個廣播硬件地址,于是所有設備都可以讀取這個消息,如果接收設備的IP與數據包里對應的target ip相同,那么它就應該構造同一個表,把自己的硬件地址存儲在表中,返回給消息的發起者。

ARPProtocolLayer繼承了PacketReceiver接口,這意味著它希望鏈路層收到數據包后,把數據包推送給它。如果接收者收到我們發出的ARP請求包后,構造一個回復消息發送到我們網卡上,鏈路層就會調用ARPProtocolLayer的PacketReceiver接口來解讀數據包。數據就存儲在packet.head里面,我們調用analyzeARPMessage接口來讀取返回的ARP包。

在解析數據包時,我們注意packet.head對應的內容包含著鏈路層包頭,也就是前面講到的14字節,因此我們要讀取相應的字節時,在計算偏移時要跳過開始14字節,在代碼里定義ARP_OPCODE_START這些常量時,注釋中提到這一點。在接收到數據包時,它先從鏈路層包頭確定該包是ARP包,然后再調用analyzeARPMessage解析包的內容。在后者的實現中,我們先取出opcode兩字節,看看它是否是2,也就是ARP回應包,如果是那么再從target protocoal address對應4字節里讀取數據包接收者的ip地址,如果該地址與我們的地址相同,那就能確定數據包是發給我們的,然后我們從sender hardware address中獲得發送者的mac地址。

ARPProtocolLayer要求所有通過它獲取mac地址的對象都必須實現IMacReceiver接口,有可能很多個上層協議對象都需要獲得同一個ip對應設備的mac地址,它會把這些對象存儲在一個隊里中,一旦給定ip設備返回包含它mac地址的ARP消息后,ARPProtocolLayer從消息中解讀出mac地址,它就會把該地址推送給所有需要的接收者,IMacReceiver的定義如下:

package utils;

public interface IMacReceiver {
    public void  receiveMacAddress(byte[] ip, byte[] mac);
}

在我們的代碼中,DataLinkLayer就繼承了這個接口,它在初始化ARPProtocolLayer時把自己進行了注冊,病調用getMacByIP去獲取對應設備的mac地址,代碼如下:

private void testARPProtocol() {
         ARPProtocolLayer arpLayer = new ARPProtocolLayer();
         this.registerPacketReceiver(arpLayer);
         
         byte[] ip;
        try {
            ip = InetAddress.getByName("192.168.2.1").getAddress();
            arpLayer.getMacByIP(ip, this);
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
         
     }

    @Override
    public void receiveMacAddress(byte[] ip, byte[] mac) {
        System.out.println("receive arp reply msg with sender ip: ");
        for (byte b: ip) {
            System.out.print(Integer.toUnsignedString(b & 0xff) + ".");
        }
        System.out.println("with sender mac :");
        for (byte b : mac)
            System.out.print(Integer.toHexString(b&0xff) + ":");
        
    }

代碼中192.168.2.1對應我家路由器ip,一旦DataLinkLayer接收到路由器回復的ARP數據包,從中解讀出mac地址后,就調用上面的receiveMacAddress,把mac地址推送過來。

上面代碼運行后,情況如下,我們用wireshark抓到了代碼發送的數據包和接收到路由器返回的ARP包:

屏幕快照 2018-12-13 下午5.43.59.png

第一行時我們代碼發出的數據包,第二行是路由器返回的數據包,我們點開第一行得到數據包內容如下:

屏幕快照 2018-12-13 下午5.45.22.png

其中sender mac address是我機器的mac地址,sender ip address是我機器的ip,opcode值是1表示它是一個arp請求包,我們點開第二行,起內容如下:

屏幕快照 2018-12-13 下午5.47.12.png

其中sender ip address正是路由器的ip,sender mac address 是路由器的mac地址,我們程序接收到這個數據包,并進行解讀后得到結果如下:

屏幕快照 2018-12-13 下午5.50.33.png

更詳細的講解和代碼調試演示過程,請點擊鏈接

更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公眾號:


這里寫圖片描述
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容