原文連接git
IP數據包由IP頭部和數據負載兩部分組成。IP數據包長度不固定,其中頭部長度不固定,負載長度也不固定。IP包的格式以下圖:github
注意,圖中的位指的是bit,下文中說包的長度是單位是字節,1字節=8位。算法
接下來,依次介紹下每塊表明的含義。緩存
版本:佔4位,表示IP協議的版本。若是是IPv4,則取值爲0100,若是是IPv6,則取值爲0110安全
IP包頭長度:佔4位,從0000-1111,也就是最小爲0,最大爲15。實際上,IP包頭的長度至少爲20字節,最大爲60字節。IP包頭長度的值 * 4就是ip包頭所佔的字節數。bash
舉例來講,IP包頭長度的值是0110,值爲6,那麼IP包頭的長度就是6 * 4 = 24。固定區域20字節,可選區域4字節。服務器
服務類型:佔8位,包含優先級、標誌位等,實際中不多使用,不作介紹。網絡
IP包總長度:佔16位,表示該IP包的總長度。即IP包頭長度+IP包數據長度。dom
標識:佔16位,在數據包分段重組時,標識表示包的序列號。當數據包比較大時,會分紅多個IP包發送,每一個IP包到達目的地的時間是不肯定的,此時就須要根據標識進行重組。標識符與下面的標誌、偏移量結合使用。tcp
標誌:佔3位。從左至右依次是MF、DF、未使用字段。MF = 1,表示後面還有分段數據包,MF = 0表示後面沒有分段數據包,也就是最後一個。DF = 1,表示該數據包不能被分段,DF = 0表示數據包能夠被分段。
偏移量:佔13位。表示該數據段在上層初始數據報文中的偏移量。和標識符、標誌位結合使用。
生存時間:佔8位。生存時間由操做系統初始化,每通過一次轉發,生存時間減1,若是生存時間爲0,則該包丟棄。生存時間是爲了防止數據包在網絡中無休止發送,佔用網絡資源。
協議:佔8位。經常使用的UDP的值是17,TCP的值是6。
首部校驗和:佔16位。對IP數據包首部校驗獲得的值,校驗和有具體的算法。
源IP地址:佔32位
目的IP地址:佔32位。
上面的內容共佔20個字節,這些部分是固定的,每個IP包的頭部都包含這些,因此IP包頭的長度至少 20字節。
下面的可選部分包含安全處理機制、記錄路徑、時間戳等信息,長度爲0-40字節。
再下面就是IP包的數據部分。IP包的數據包含TCP包、UDP包兩種。
使用Wireshark抓取IP包,格式以下:
版本號值爲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包的格式和IP包格式相似,一樣由頭部和數據兩部分組成。TCP包的長度不固定,其中頭部長度不固定,數據部分長度不固定。
由IP部分的內容可知,TCP包實際上就是IP包的數據部分。二者的關係能夠用下圖表示:
TCP包的格式以下:
依次介紹每塊的含義。
TCP包固定頭部20字節,可選部分在0-40字節之間。剩下的就是TCP包的數據部分。
使用Wireshark抓取TCP的包,以下:
能夠看出,源端口是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
複製代碼
相對TCP來講,UDP是無序、無狀態的,所以UDP包相對TCP包來講要簡單一些。UDP包實際上也是IP包的數據部分。上圖中IP包和TCP包的關係,一樣也適用於IP包和UDP。
UDP包由頭部和數據兩部分組成,頭部長度固定,數據部分長度不固定。UDP包的格式以下:
能夠看到,UDP頭部的長度爲固定8字節,剩下的是數據部分。依次介紹每塊的含義。
剩下的就是UDP的數據部分。
注意UDP頭部中UDP長度,這個地方和TCP、IP包有明顯的區別。由於UDP包頭部長度是固定8個字節,因此該部分的取值最小爲1000。
使用Wireshark抓取UDP的包,以下圖:
能夠看到,該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
複製代碼
除了IP、TCP、UDP以外,DNS也是常常用到的協議,介紹下DNS包的格式。
DNS包屬於UDP包,實際上,DNS包是UDP包的數據部分,二者的關係以下:
DNS包一樣分爲頭部和數據部分,頭部長度固定,數據部分長度不固定。DNS包的格式以下:
依次介紹每塊的含義。
因爲域名的長度不固定,因此name區域長度不固定。
查詢類型 | 助記符 | 解釋 |
---|---|---|
1 | A | 得到IPv4地址 |
28 | AAAA | 得到IPv6地址 |
15 | MX | 郵件服務器 |
2 | NS | 指定域名服務器 |
5 | CNAME | 將域名指向另外一個域名時 |
回答區域的格式固定,格式以下:
介紹下每塊的含義。
使用偏移量來表示時,首字節固定爲C0,用於識別,後面字節用於表示偏移量。經過上面的介紹可知,DNS包的頭部固定佔12字節,頭部以後就是查詢問題區域,查詢問題區域的第一部分就是域名。所以,常見的偏移量是C00C,以下圖:
使用Wireshark抓取DNS請求包,以下圖:
能夠看到,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抓取的響應包,以下圖:
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、數據。
11000000 00001100 //對應的是C00C,說明使用的是偏移量表示
複製代碼
00000000 00000101 //值爲5,對應CNAME
複製代碼
00000000 00000000 00010001 01000110 // 對應4422
複製代碼
00000000 00010101 // 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個字節是什麼:
從第23個字節開始,正好拼成了域名"sslbaiduv6.jomodns.com"。
11000000 00101100 //首字節是C0,說明是偏移量,偏移值是44,第一個應答區域的域名首字節index正好是44
複製代碼
00000000 00000001 // 說明內容是IPv4地址
複製代碼
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地址,邏輯是相似的,再也不舉例。