1. 程式人生 > >自己動手寫一個小型的TCP/IP協議

自己動手寫一個小型的TCP/IP協議

TCP/IP協議大家都知道,但真正理解的人不多,不如動手寫一個小型的看看。

我知道看書很枯燥,看不懂,還打擊大家的信心,不是我們的腦袋不如人,是我們的方法錯了。

一切的技術都從應用中發展而來,所以要從下往上走,先動手完成一個任務吧。

需要準備的前提知識

  • linux驅動程式知識:原本理解網路協議是不一定非要懂linux驅動程式的,但由於這個例子是使用linux虛擬網絡卡作為基礎,為了看懂原始碼,需要簡單瞭解。目前沒有又短小又清楚的好文章推薦,以後可以補充。

  • 虛擬網絡卡 TUN/TAP 驅動程式設計原理 
    http://www.ibm.com/developerworks/cn/linux/l-tuntap/

     
    上面這個網址是我認為講解比較清楚全面的文章,推薦一下。

  • 太網報頭格式(L2層),ARP報頭格式,ARP協議功能和實現流程。下面這個英文網址的文章我認為講解還算比較好。

  •  
    • 首先明白所謂7層協議棧各層的報頭。這裡不準備7層全部講,就講三層,包括乙太網報頭(L1層),IP報頭(L2層),TCP/UDP(L3層)報頭。講多了,反而不容易理解。從一個個具體應用總結出整個協議棧的結構。
  • 然後再來看一個實際截獲的資料包。
  • 最後看一下整個資料包,包括資料和三個報頭,是如何生成的。分別由哪些協議生成和生成次序。包包生成之後,交給乙太網驅動程式發走。

IP包的報頭 
圖1:IP報頭結構 
除掉 IP Option這個欄位, IP報頭一共20個位元組,各欄位的含義如上圖所示。

下面再看一個實際截獲的UDP資料包: 
一個實際捕獲的UDP資料包 
圖2:一個實際截獲的UDP資料包

  • 乙太網報頭

有一個“型別”欄位(上面的例子中是0800,代表是從IP協議層傳送過來的資料包),其中各型別值的含義分別如下:

0x0600 XNS 
0x0800 IP 
0x0806 ARP 
0x6003 DECnet

  • IP報頭 
    紅色框框裡面就是20個位元組的IP報頭,各欄位的含義要仔細看。
  • UDP報頭

    IP報頭 
    緊接著後面8位元組的UDP報頭。它是被上層的UDP協議加上去的。

    首先資料(有效載荷)當然是應用層(打比方EPSON印表機應用軟體)生成的。(上面這個圖是我的EPSON印表機軟體和印表機的通訊包)。EPSON印表機應用軟體生成一個數據包,就丟給UDP層(L3),這個UDP協議就會在資料前面加個一個UDP的報頭(上圖中的藍色框框)。然後UDP協議接著往下傳遞,傳到了IP層(L2層),這個IP協議呢,又在前面加了一個IP報頭。又接著往下傳遞,傳遞到了乙太網卡層(L1層),又在前面加了一個乙太網報頭,然後整個資料包交給乙太網驅動程式,這個資料包包括三個報頭和要傳送的資料(有效載荷),最後乙太網驅動程式把整個資料包通過網線傳送出去。


看完一個具體的例子,再來看整個協議棧。 
這裡寫圖片描述

ARP協議(Address Resolution Protocol )是一個特定的網路標準協議。它是可選的。工作在L2層 。 
地址解析協議負責將更高級別的協議地址(IP地址)轉換為物理網路地址。 
http://network-panda.blogspot.jp/2015/06/brief-introduction-to-protocols-1-arp.html

http://www.tcpipguide.com/free/t_ARPMessageFormat.htm 
當主機收到ARP資料包(無論是廣播請求或點對點的回覆),接收的裝置驅動程式把這個包傳送到ARP協議模組,ARP協議模組按如下的流程處理: 
這裡寫圖片描述

好了,到此為止,我認為該準備的基礎知識已經準備好了。接下來動手實驗吧。


讓我們寫一個TCP / IP協議棧,1:乙太網、ARP

自己寫TCP / IP協議棧可能看起來像一個艱鉅的任務。事實上,TCP已經積累了超過三十年的壽命的規範。

然而核心規範,看起來是緊湊的,重要組成部分是:TCP報頭解析、狀態機、擁塞控制和重傳超時計算。 
最常用的2層和3層的協議,分別在乙太網和IP層,相對TCP的複雜性而言顯得簡單很多。

在這個部落格系列,我們將在Linux上實現一個最小的使用者空間TCP / IP協議棧。

這些是純粹的學習用,要深入學習,請更深層的學習網路和系統程式設計。

內容

TUN/TAP裝置 
乙太網幀格式 
乙太網幀分析 
地址解析協議 
地址解析演算法 
結論 
來源

TUN/TAP裝置

為了攔截來自於核心的底層的網路資料,我們將使用一個TUN/TAP裝置。

[Connie Note] 
tun/tap 驅動程式實現了虛擬網絡卡的功能,tun表示虛擬的是點對點裝置,tap表示虛擬的是乙太網裝置,這兩種裝置針對網路包實施不同的封裝。 
利用tun/tap 驅動,可以將tcp/ip協議棧處理好的網路分包傳給任何一個使用tun/tap驅動的程序,由程序重新處理後再發到物理鏈路中。

簡單說,一個TUN/TAP裝置通常是由網路使用者空間的應用程式用來分別操縱L3或L2層的資料。

一個流行的例子是tunneling,其中一個包被包裹在另一個數據包的有效載荷中。

TUN/TAP裝置的優勢是,它們很容易在使用者空間的程式建立,它們已經被用於許多程式,如OpenVPN。

由於我們要從L2層建立自己的網路協議堆疊,我們需要一個TAP裝置。我們那樣例項化的那樣:

/ *
*從核心檔案/網路/ tuntap.txt取
*/

int tun_alloc(char *dev)
{
    struct ifreq ifr;
    int fd, err;

    if( (fd = open("/dev/net/tap", O_RDWR)) < 0 ) {
        print_error("Cannot open TUN/TAP dev");
        exit(1);
    }

    CLEAR(ifr);

    /* Flags: IFF_TUN   - TUN device (no Ethernet headers)
     *        IFF_TAP   - TAP device
     *
     *        IFF_NO_PI - Do not provide packet information
     */
    ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
    if( *dev ) {
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);
    }

    if( (err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0 ){
        print_error("ERR: Could not ioctl tun: %s\n", strerror(errno));
        close(fd);
        return err;
    }

    strcpy(dev, ifr.ifr_name);
    return fd;
}

接下來,返回的檔案描述符fd可以用來讀寫資料到虛擬裝置的乙太網緩衝。 
標誌IFF_NO_PI是至關重要的,否則,我們會新增到乙太網幀不必要的資料包資訊。你可以看看核心的裝置驅動程式的原始碼,並自己驗證。

乙太網幀格式

眾多不同的乙太網網路技術連線計算機到區域網(LANs)的主幹網。與所有的物理技術一樣,乙太網標準已經發生巨大變化,從1980年由Digital Equipment公司和英特爾和施樂出版的第一個方案。

乙太網的第一個版本是在今天的標準看來是很慢的,大概10Mb/s,採用半雙工通訊,這意味著你可以傳送或接收資料,但不能在同一時間。這就是為什麼一個媒體訪問控制(Media Access Control )(MAC)協議必須被納入組織的資料流。即使到今天,如果在半雙工模式下執行一個乙太網介面,載波偵聽多路訪問衝突檢測(CSMA/CD)依然是必須的,因為MAC。

採用雙絞線佈線的100BASE-T乙太網標準使全雙工通訊成為可能,也使通訊有了更高的吞吐速度。此外,同時增加了乙太網交換機的人氣,讓CSMA/CD過時。

IEEE 802.33工作組維護著不同的乙太網標準。

下一步,我們將看看乙太網幀頭。它可以被宣告為一個C的結構體:

#include <linux/if_ether.h>
struct eth_hdr
{
    unsigned char dmac[6];
    unsigned char smac[6];
    uint16_t ethertype;        //乙太網型別
    unsigned char payload[];    //有效載荷
} __attribute__((packed));

DMAC和Smac是相當不言自明的專案。它們分別包含通訊方(目的和源)的地址。 
過載專案ethertype,是一種2-octet專案,它的含義取決於它的值,可以是有效載荷的長度,也可以使有效載荷的型別。具體來說,如果該欄位的值大於或等於1536,該欄位表示有效載荷的型別(如IPv4,ARP)。如果值小於該值,則欄位表示有效負載的長度。

ethertype欄位後,乙太網幀中可以有幾種不同的標籤。這些標籤可以用來描述虛擬區域網(VLAN)、服務質量(QoS)的幀型別。乙太網幀標籤被排除在我們這次的程式碼之外,所以在我們的協議宣告中,相應的欄位也沒有出現。

有效載荷欄位包含指向乙太網幀有效負載的指標。在我們的例子中,這將包含一個ARP或IPv4資料包。如果有效負載長度小於所需的最小48位元組(不帶標籤),則將位元組數追加到有效負載的最末端,以滿足需求。 
我們還需要包括if_ether.h 這個Linux標頭檔案,它提供ethertypes和十六進位制值之間的對映。 
最後,乙太網幀格式還包括幀校驗欄位,它是用迴圈冗餘校驗(CRC)來檢查幀的完整性。在我們的例子中,我們將省略這一處理。

乙太網幀的解析

結構宣告的packed屬性是一個細節,它告訴編譯器不要優化資料結構的4位元組對齊的記憶體佈局。使用這個屬性是由於我們“解析”協議緩衝區的方式,這是一種適當的協議結構資料緩衝區: 
struct eth_hdr hdr = (struct eth_hdr ) buf;

一種行動式,稍微費力的方法,將手動序列化協議資料。這樣,編譯器可以自由地新增填充位元組,以更好地適應不同處理器的資料對齊要求。

分析和處理乙太網幀快取的總體方案是簡單的:

if (tun_read(buf, BUFLEN) < 0) {
    print_error("ERR: Read from tun_fd: %s\n", strerror(errno));
}

struct eth_hdr *hdr = init_eth_hdr(buf);

handle_frame(&netdev, hdr);

handle_frame功能使根據以太幀的ethertype 欄位的值,決定其下一步的動作。

地址解析協議

地址解析協議(ARP)是用於動態對映一個48位的乙太網地址(MAC地址)到協議地址(例如IPv4地址)。這裡的關鍵是,ARP需要對應很多不同的L3協議,不僅僅是IPv4,還有像CHAOS這樣的協議。這些被宣告成16位元的協議地址。

通常的情況是,你知道在你的區域網中的一些服務的IP地址,但要建立實際的通訊,也需要知道硬體地址(MAC)。因此,ARP是用來廣播查詢網路,要求IP地址的所有者報告其硬體地址。

ARP資料包的格式相對比較簡單:

struct arp_hdr
{
    uint16_t hwtype;
    uint16_t protype;
    unsigned char hwsize;
    unsigned char prosize;
    uint16_t opcode;
    unsigned char data[];
} __attribute__((packed));

ARP報頭(arp_hdr)包含一個2-octet欄位 hwtype,它取決於鏈路層型別。這是在我們例子中,它的值一直是 0x0001。

2-octet欄位protype表示協議型別。在我們的例子中,這是IPv4,相應值是0x0800。

hwsize 和 prosize欄位都是1-octet大小,它們分別表示硬體尺寸和協議欄位。在我們的例子中,這些是6位元組的MAC地址,和4個位元組的IP地址。

2-octet欄位opcode 表示ARP報文型別。它可以是ARP請求(1)、ARP應答(2),RARP請求(3)或(4)RARP應答。

data欄位包含ARP報文的實際載荷,並在我們的例子中,這將包含IPv4的具體資訊:

struct arp_ipv4
{
    unsigned char smac[6];
    uint32_t sip;
    unsigned char dmac[6];
    uint32_t dip;
} __at

欄位smac和dmac分別包含傳送者和接收者的6位元組的MAC地址。 sip 和 dip分別包含傳送者和接收者的IP地址。

地址解析演算法

原始規範描述了這個簡單的演算法來解決地址的解析

?Do I have the hardware type in ar$hrd?

Yes: (almost definitely) 
[optionally check the hardware length ar$hln]

?Do I speak the protocol in ar$pro?

Yes: 
[optionally check the protocol length ar$pln] 
Merge_flag := false

If the pair <protocol type, sender protocol address> is
    already in my translation table, update the sender
    hardware address field of the entry with the new
    information in the packet and set Merge_flag to true.
?Am I the target protocol address?
Yes:
  If Merge_flag is false, add the triplet <protocol type,
      sender protocol address, sender hardware address> to
      the translation table.
  ?Is the opcode ares_op$REQUEST?  (NOW look at the opcode!!)
  Yes:
    Swap hardware and protocol fields, putting the local
        hardware and protocol addresses in the sender fields.
    Set the ar$op field to ares_op$REPLY
    Send the packet to the (new) target hardware address on
        the same hardware on which the request was received.

即,翻譯表用於儲存ARP協議的結果,使主機可以看看他們是否已經在他們的快取記憶體條目。這避免了冗餘ARP請求濫發網路 
該演算法在arp.c中實現了.

最後,一個ARP實現最終的考驗就是看它是否正確的回覆ARP請求

[[email protected] lvl-ip]$ arping -I tap0 10.0.0.4 
ARPING 10.0.0.4 from 192.168.1.32 tap0 
Unicast reply from 10.0.0.4 [00:0C:29:6D:50:25] 3.170ms 
Unicast reply from 10.0.0.4 [00:0C:29:6D:50:25] 13.309ms

[[email protected] lvl-ip]$ arp 
Address HWtype HWaddress Flags Mask Iface 
10.0.0.4 ether 00:0c:29:6d:50:25 C tap0

核心的網路堆疊識別了從我們自定義的網路協議棧來的ARP應答,填充它的ARP快取中的虛擬網路裝置的條目。成功!

結論 
乙太網幀的處理和ARP的最小實現相對容易,可在幾行程式碼完成。而鼓勵意義是相當高的,因為你可以在Linux主機的ARP快取有自己製作的乙太網裝置!

該專案的原始碼可以在GitHub找到。

在接下來的文章中,我們將繼續實現 ICMP echo和 reply (ping) 和IPv4資料包解析。 
如果你喜歡這個文章,你可以分享給你的粉絲,並在Twitter上跟蹤我!