協議包結構

原文連接git

IP包的格式

理論

IP數據包由IP頭部和數據負載兩部分組成。IP數據包長度不固定,其中頭部長度不固定,負載長度也不固定。IP包的格式以下圖:github

image

注意,圖中的位指的是bit,下文中說包的長度是單位是字節,1字節=8位。算法

接下來,依次介紹下每塊表明的含義。緩存

  1. 版本:佔4位,表示IP協議的版本。若是是IPv4,則取值爲0100,若是是IPv6,則取值爲0110安全

  2. IP包頭長度:佔4位,從0000-1111,也就是最小爲0,最大爲15。實際上,IP包頭的長度至少爲20字節,最大爲60字節。IP包頭長度的值 * 4就是ip包頭所佔的字節數。bash

    舉例來講,IP包頭長度的值是0110,值爲6,那麼IP包頭的長度就是6 * 4 = 24。固定區域20字節,可選區域4字節。服務器

  3. 服務類型:佔8位,包含優先級、標誌位等,實際中不多使用,不作介紹。網絡

  4. IP包總長度:佔16位,表示該IP包的總長度。即IP包頭長度+IP包數據長度。dom

  5. 標識:佔16位,在數據包分段重組時,標識表示包的序列號。當數據包比較大時,會分紅多個IP包發送,每一個IP包到達目的地的時間是不肯定的,此時就須要根據標識進行重組。標識符與下面的標誌、偏移量結合使用。tcp

  6. 標誌:佔3位。從左至右依次是MF、DF、未使用字段。MF = 1,表示後面還有分段數據包,MF = 0表示後面沒有分段數據包,也就是最後一個。DF = 1,表示該數據包不能被分段,DF = 0表示數據包能夠被分段。

  7. 偏移量:佔13位。表示該數據段在上層初始數據報文中的偏移量。和標識符、標誌位結合使用。

  8. 生存時間:佔8位。生存時間由操做系統初始化,每通過一次轉發,生存時間減1,若是生存時間爲0,則該包丟棄。生存時間是爲了防止數據包在網絡中無休止發送,佔用網絡資源。

  9. 協議:佔8位。經常使用的UDP的值是17,TCP的值是6。

  10. 首部校驗和:佔16位。對IP數據包首部校驗獲得的值,校驗和有具體的算法。

  11. 源IP地址:佔32位

  12. 目的IP地址:佔32位。

上面的內容共佔20個字節,這些部分是固定的,每個IP包的頭部都包含這些,因此IP包頭的長度至少 20字節。

下面的可選部分包含安全處理機制、記錄路徑、時間戳等信息,長度爲0-40字節。

再下面就是IP包的數據部分。IP包的數據包含TCP包、UDP包兩種。

抓包驗證

使用Wireshark抓取IP包,格式以下:

image

版本號值爲4,說明是IPv4,對應的二進制是

0100
複製代碼

IP包頭部長度值爲5,說明IP包頭部長度爲20字節,對應的二進制值是

0101
複製代碼

服務類型值爲0,對應的二進制是

00000000
複製代碼

IP包總長度爲40,對應的二進制值是

00000000 00101000
複製代碼

標識爲0,對應的二進制是

00000000 00000000
複製代碼

3位標識依次是0、一、0,表示該包後無分段數據,且該數據包不能被分段。

Time to live值爲64,對應的二進制是

01000000
複製代碼

協議的值是6,對應的二進制是

00000110 // 6,表示是TCP
複製代碼

頭部校驗和值爲0xcb59。

源IP值是 10.0.6.33,對應的二進制是

00001010 00000000 00000110 00100001
複製代碼

目的IP值是40.100.54.242,對應的二進制是

00101000 01100100 00110110 11110010
複製代碼

共20字節。

代碼驗證

使用代碼驗證下IP包的格式,打印出源ip、目的ip、頭部長度、總長度。代碼以下:

- (void)ipTest
{
    // 從文件中讀取IP數據流
    NSString *txtFilePath = [[NSBundle mainBundle] pathForResource:@"tcp" ofType:@"txt"];
    NSData *data = [NSData dataWithContentsOfFile:txtFilePath ];
    const uint8_t *bytes = data.bytes;
    
    // 由於確認是IP包,因此能夠直接轉爲struct ip類型
    struct ip *iphdr = (struct ip *)bytes;
    char src_ip[16];
    char dst_ip[16];
    uint32_t src_addr = iphdr->ip_src.s_addr;
    uint32_t dst_addr = iphdr->ip_dst.s_addr;
    
    inet_ntop(AF_INET,&src_addr,src_ip,sizeof(src_ip));
    inet_ntop(AF_INET,&dst_addr,dst_ip,sizeof(dst_ip));
    
    printf("src = %s dst = %s headLen = %d dataLen = %d protocol = %d",src_ip,dst_ip,iphdr->ip_hl,iphdr->ip_len,iphdr->ip_p);
}
複製代碼

輸出爲:

src = 192.0.2.1 dst = 216.58.200.234 headLen = 5 dataLen = 16384 protocol = 6
複製代碼

TCP包格式

理論

TCP包的格式和IP包格式相似,一樣由頭部和數據兩部分組成。TCP包的長度不固定,其中頭部長度不固定,數據部分長度不固定。

由IP部分的內容可知,TCP包實際上就是IP包的數據部分。二者的關係能夠用下圖表示:

image

TCP包的格式以下:

image

依次介紹每塊的含義。

  1. 源端口:佔16位。
  2. 目的端口:佔16位。
  3. 序號:sequence number,佔32位。表示從發送端向接收端發送的字節流編號。
  4. 確認號:Acknowledgement number,佔32位。表示接收端所指望接收到的下一序號。
  5. 數據偏移:佔4位。數據偏移其實就是TCP包的頭部長度,理論取值從0000-1111,最小爲0,最大爲15。同IP頭部同樣,TCP包的頭部長度至少爲20字節,最多爲60字節,且計算方式和IP頭部長度的計算方式也同樣。若是數據偏移的值爲6,那麼TCP包的頭部長度爲6 * 4 = 24。
  6. 保留值:佔6位。目前沒用,爲了以後新功能所保留。
  7. 6位標誌位。分別介紹:
    1. URG:緊急標誌位,爲1說明緊急指針有效。
    2. ACK:確認標誌位,爲1說明確認序號有效。
    3. PSH:值爲1,表示須要將數據馬上發送給應用程序。
    4. RST:值爲1時,表示須要重連。
    5. SYN:握手時使用。值爲1,表示鏈接請求報文。
    6. FIN:值爲1時,須要斷開鏈接。
  8. 窗口大小:佔16位,表示接收端的窗口大小,用於控制網絡流量速率。
  9. 校驗和:佔16位,和IP包頭部中的校驗和做用一致。
  10. 緊急指針:佔16位,和上面提到的URG字段結合使用。
  11. 可選部分,包含窗口擴大選項、時間戳等。最小爲0,最大爲40字節。

TCP包固定頭部20字節,可選部分在0-40字節之間。剩下的就是TCP包的數據部分。

抓包驗證

使用Wireshark抓取TCP的包,以下:

image

能夠看出,源端口是57119,對應的二進制是

11011111 00011111 // 對應571119
複製代碼

目的端口是443,對應的二進制是

00000001 10111011 // 對應443,說明是https請求
複製代碼

接下來是Sequence Number和Ack Number,各佔4個字節。

而後是頭部長度(偏移量),二進制是

0101 // 值爲5,說明該IP包頭部沒有附加部分,頭部長度爲20字節
複製代碼

而後是保留位和一些flag標誌位,標誌位中Ack爲1,說明Ack Number有效。Urgent爲0,說明沒有緊急指針無效。

窗口大小爲4095,二進制爲

00001111 11111111
複製代碼

校驗和的值爲0x268a。

緊急指針值爲0,對應的二進制爲

00000000 00000000
複製代碼

共20字節。

代碼驗證

使用代碼驗證tcp包格式,打印出tcp包的源端口、目的端口、頭部長度,代碼以下:

- (void)tcpTest
{
    // 從文件中讀取TCP數據流
    NSString *txtFilePath = [[NSBundle mainBundle] pathForResource:@"tcp" ofType:@"txt"];
    NSData *data = [NSData dataWithContentsOfFile:txtFilePath ];
    const uint8_t *bytes = data.bytes;
    
    // 由於確認是IP包,因此能夠直接轉爲struct ip類型
    struct ip *iphdr = (struct ip *)bytes;
    // 左移兩位,即*4,就是ip包的頭部長度
    const int ip_hlength = iphdr->ip_hl << 2;
    // 偏移ip_hlength個字節,到TCP數據部分
    const uint8_t *tcpPacket = bytes + ip_hlength;
    // 系統API提供的有tcp結構體,能夠直接轉爲 struct tcphdr類型
    struct tcphdr tcp;
    memcpy(&tcp,tcpPacket,sizeof(struct tcphdr));
    // 打印源端口、目的端口、頭部長度
    printf("sourceport = %d dstport = %d len = %d",ntohs(tcp.th_sport),ntohs(tcp.th_dport),tcp.th_off);
}
複製代碼

輸出爲:

sourceport = 60495 dstport = 443 len = 11
複製代碼

UDP包格式

理論

相對TCP來講,UDP是無序、無狀態的,所以UDP包相對TCP包來講要簡單一些。UDP包實際上也是IP包的數據部分。上圖中IP包和TCP包的關係,一樣也適用於IP包和UDP。

image

UDP包由頭部和數據兩部分組成,頭部長度固定,數據部分長度不固定。UDP包的格式以下:

image

能夠看到,UDP頭部的長度爲固定8字節,剩下的是數據部分。依次介紹每塊的含義。

  1. 源端口:佔16位
  2. 目的端口:佔16位
  3. UDP長度:包含頭部和數據包總長度,注意和TCP包、IP包的區別
  4. 校驗和:佔16位,同IP包、TCP包校驗和。

剩下的就是UDP的數據部分。

注意UDP頭部中UDP長度,這個地方和TCP、IP包有明顯的區別。由於UDP包頭部長度是固定8個字節,因此該部分的取值最小爲1000。

抓包驗證

使用Wireshark抓取UDP的包,以下圖:

image

能夠看到,該UDP包的源端口是53,對應的二進制是

00000000 11101010 // 對應53
複製代碼

目的端口是60155,對應的二進制是

11101010 11111011 // 對應60153
複製代碼

長度是84,對應的二進制是

00000000 01010100 //對應84
複製代碼

校驗和是0xc061,對應的二進制是

11000000 01100001 //對應0xc061
複製代碼

共8個字節。

代碼驗證

使用代碼驗證普通UDP包格式,打印出UDP包的源端口、目的端口、長度,代碼以下:

- (void)udpTest
{
    // 從文件中讀取UDP數據流
    NSString *txtFilePath = [[NSBundle mainBundle] pathForResource:@"dns" ofType:@"txt"];
    NSData *data = [NSData dataWithContentsOfFile:txtFilePath ];
    const uint8_t *bytes = data.bytes;
    
    // 由於確認是IP包,因此能夠直接轉爲struct ip類型
    struct ip *iphdr = (struct ip *)bytes;
    // 左移兩位,即*4,就是ip包的頭部長度
    const int ip_hlength = iphdr->ip_hl << 2;
    // 偏移ip_hlength個字節,到UDP數據部分
    const uint8_t *udpPacket = bytes + ip_hlength;
    // 系統API提供的有udp結構體,能夠直接轉爲 struct udphdr類型
    struct udphdr udp;
    memcpy(&udp, udpPacket, sizeof(struct udphdr));
    printf("sourceport = %d dstport = %d len = %d",ntohs(udp.uh_sport),ntohs(udp.uh_dport),udp.uh_ulen);
}
複製代碼

輸出:

sourceport = 63354 dstport = 53 len = 10496
複製代碼

DNS包格式

理論

除了IP、TCP、UDP以外,DNS也是常常用到的協議,介紹下DNS包的格式。

DNS包屬於UDP包,實際上,DNS包是UDP包的數據部分,二者的關係以下:

image

DNS包一樣分爲頭部和數據部分,頭部長度固定,數據部分長度不固定。DNS包的格式以下:

image

依次介紹每塊的含義。

  1. TranscationID:會話標識,佔2個字節。DNS請求報文和DNS應到報文的TranscationID是相同的。
  2. Flags:佔2個字節,包含多個標誌位,結構以下

image

  • QR:佔1位。0查詢報文,1響應報文
  • Opcode:佔4位。0標準查詢,1反向查詢,2服務器狀態查詢,3~15保留未用
  • AA:佔1位。爲1,表示受權回答
  • TC:佔1位。爲1,表示報文被截斷
  • RD:佔1位。0,表示客戶端指望域名解析服務器採用迭代的方式解析;1表示客戶端指望域名解析服務器採用遞歸的方式解析
  • RA:佔1位。0,表示域名解析服務器採用迭代的方式解析;1表示域名服務器採用遞歸的方式解析
  • Zero:佔3位。全0,保留位。
  • Rcode:佔4位。響應碼,0表示無差錯;1表示查詢格式錯誤;2表示服務器失效;3表示域名錯誤;4表示查詢沒有被執行;5表示查詢被拒絕。6~15保留。
  1. Questions:佔2個字節,表示查詢問題區域的數量
  2. Answers RRS:佔2個字節,表示回答區域的數量
  3. Authority RRs:佔2個字節,表示受權區域的數量
  4. Additional RRs:佔2個字節,表示附加區域的數量
  5. Quries:問題區域,長度不固定,能夠有多個。Quries區域的格式以下:

image

  • Name區域,就是查詢的域名,如"www.baidu.com",Name區域也有固定的格式。以百度域名舉例,格式以下:

image

因爲域名的長度不固定,因此name區域長度不固定。

  • Type,查詢類型,佔2個字節。DNS有多種查詢類型,常見的有
查詢類型 助記符 解釋
1 A 得到IPv4地址
28 AAAA 得到IPv6地址
15 MX 郵件服務器
2 NS 指定域名服務器
5 CNAME 將域名指向另外一個域名時
  • Class,查詢類,一般爲1,表示Internet數據。
  1. Answers:回答區域,長度不固定。回答區域的格式和受權區域、附加區域的格式是相似的,所以,只介紹回答區域的格式。

回答區域的格式固定,格式以下:

image

介紹下每塊的含義。

  • Name:查詢的域名,同問題區域的域名。可是格式和問題區域的域名不一樣,爲了減少包大小,當包中出現重複域名的時候,回答區域的域名部分使用偏移量來表示。當使用偏移量來表示時,佔2個字節。不使用偏移量時,長度不固定(和域名自己長度有關)。

使用偏移量來表示時,首字節固定爲C0,用於識別,後面字節用於表示偏移量。經過上面的介紹可知,DNS包的頭部固定佔12字節,頭部以後就是查詢問題區域,查詢問題區域的第一部分就是域名。所以,常見的偏移量是C00C,以下圖:

image

  • Type:同請求部分
  • Class:同請求部分
  • Time To Live:生存時間,佔4個字節。其實是客戶端緩存的時間。一般狀況,域名所對應的IP不會常常改變。所以,爲了提升網絡傳輸效率,能夠將結果緩存,下次訪問時跳過域名解析。
  • 數據長度:佔2個字節,下文數據部分的長度。
  • 數據:不定長。
  1. Authoritative:同回答區域。
  2. Additional:同回答區域。

抓包驗證

請求包

使用Wireshark抓取DNS請求包,以下圖:

image

能夠看到,Transcation ID爲 0xa090,多對應的二進制是:

10100000 10010000 //對應a090
複製代碼

接下來是標誌區域,佔2個字節,對應的二進制是:

00000001 00000000 //首位爲0,表示是一個請求包
複製代碼

問題區域,佔兩個字節:

00000000 00000001 // 只有1個問題
複製代碼

響應區域、受權區域、附加區域都爲0,各佔2個字節:

00000000 00000000
複製代碼

以後是正文,首先是Queries,也就是請求區域的內容。包含三部分:Name、Type、Class。

Name是dss1.baidu.com,對應的二進制是:

00000100 01100100 01110011 01110011 00110001 00000101 01100010 01100001 01101001 01100100 01110101 00000011 01100011 01101111 01101101 00000000
複製代碼

共十六個字節,對應的內容依次是:

4 d s s 1 5 b a i d u 3 c o m 0
複製代碼

Type爲A,佔2個字節:

00000000 00000001 // 表示得到IPv4地址
複製代碼

Class 爲IN,佔2個字節:

00000000 00000001 // 表示網絡數據
複製代碼

該請求包的回答區域、受權區域、附加區域都爲0,因此下面沒有數據。

響應包

使用Wireshark抓取的響應包,以下圖:

image

TranscationID的值爲0xa090,說明和上文的請求包是一對。

Flags的二進制值爲:

10000001 10000000 //首位爲1,說明是響應包;後四位爲0,表示沒有錯誤。
複製代碼

Questions爲1和上文保持一致;Answer RRs爲2,說明有2個應答區域。附加區域和受權區域都爲0。

Quries和請求報文一致,再也不介紹。

Answers區域,分爲6部分,分別是Name、Type、Class、Time To Live、Data length、數據。

  1. 第一個回答區域
  • Name:對應的二進制是
11000000 00001100 //對應的是C00C,說明使用的是偏移量表示
複製代碼
  • Type: 對應的二進制是
00000000 00000101 //值爲5,對應CNAME
複製代碼
  • Class 爲1,同請求包
  • Time to live: 對應的二進制是
00000000 00000000 00010001 01000110 // 對應4422
複製代碼
  • Data length,對應的二進制是
00000000 00010101 // 21,表示後面數據的長度
複製代碼
  • 數據部分,數據部分對應的內容是 sslbaiduv6.jomodns.com。存儲域名使用的仍是上面介紹的方式,須要注意的是,由於上面已經存儲過"com"了,因此這裏存儲"com"時使用的是偏移量,這也是爲什麼data length是21的緣由。數據部分對應的二進制是:
00001010 01110011 01110011 01101100 01100010 01100001 01101001 01100100 01110101 01110110 00110110 00000111 01101010 01101111 01101101 01101111 01100100 01101110 01110011 11000000 00010111
複製代碼

前19個字節對應的內容分別是

10 s s l b a i d u v 6 7 j o m o d n s
複製代碼

第20個字節是0xc0,表示後面一個字節是偏移量;第21個字節表示的內容是23,說明偏移量是23。看一下包中第23個字節是什麼:

image

從第23個字節開始,正好拼成了域名"sslbaiduv6.jomodns.com"。

  1. 第二個回答區域
  • Name,對應的二進制是:
11000000 00101100 //首字節是C0,說明是偏移量,偏移值是44,第一個應答區域的域名首字節index正好是44
複製代碼
  • Type,二進制值爲:
00000000 00000001 // 說明內容是IPv4地址
複製代碼
  • Class、Time to live、Data length和第一個應答區域相似
  • 數據區域:返回的是IP地址,對應的二進制是:
01110001 01100000 00011110 00100001
複製代碼

對應的十進制分別是:

113 96 30 33 // 正好是IP地址
複製代碼

該包沒有附加區域和受權區域。

代碼驗證

項目中有從DNS請求包中獲取請求域名的需求,寫代碼簡單驗證一下。爲了方便測試,將DNS流以文件的形式存儲在本地,代碼中直接讀取文件。代碼以下:

- (void)dnsTest
{
    // 從文件中讀取DNS數據流
    NSString *txtFilePath = [[NSBundle mainBundle] pathForResource:@"dns" ofType:@"txt"];
    NSData *data = [NSData dataWithContentsOfFile:txtFilePath ];
    const uint8_t *bytes = data.bytes;
    // 由於確認是IP包,因此能夠直接轉爲struct ip類型
    struct ip *iphdr = (struct ip *)bytes;
    // 左移兩位,即*4,就是ip包的頭部長度
    const int ip_hlength = iphdr->ip_hl << 2;
    // 偏移ip_hlength個字節,到UDP數據部分。UDP包頭部固定8個字節,再偏移8個字節,便是DNS數據部分
    const uint8_t *dnsPacket = bytes + ip_hlength + 8;
    size_t dnsLen = data.length - ip_hlength - 8;
    // 用於存儲域名
    char *domain = (char *) malloc(sizeof(char) * 256);
    if (!domain)
        return ;
    memset(domain, 0, 256);
    domain[0] = '\0';
    int domain_len = 0;
    const uint8_t *ptr = dnsPacket;
    // 偏移12,由於DNS包的頭部固定12字節
    ptr += 12;
    for (int n = 0; n < dnsLen;) {
        uint8_t c = ptr[n];
        // 全爲0的字節,就是域名的最後一位
        if (c == 0x00)
            break;
        if ((n + c + 1) > dnsLen)
            break;
        if ((n + c + 1) > 256)
            break;
        n += 1;
        // 以"baidu.com"舉例,首先把"baidu"加到domain中
        strncat(domain, (const char *) (ptr + n), c);
        // 而後加"."
        strncat(domain, ".", 1);
        domain_len += (c + 1);
        // 依次循環
        n += c;
    }
    if (domain_len >= 1)
        domain[domain_len - 1] = '\0';
    printf("domain = %s",domain);
}

複製代碼

輸出結果爲:

domain = ccdace.hupu.com
複製代碼

由於只是驗證,因此已經確保了該包是DNS包。實際在項目中,還應該判斷包是UDP包,且是DNS請求。

另外還能夠從響應包中得到返回的IP地址,邏輯是相似的,再也不舉例。

相關文章
相關標籤/搜索