在 iOS 平臺實現Ping 和 traceroute

iosmedia發表於2019-03-04

ping 命令

Ping是為了測試另一臺主機是否可達,現在已經成為一種常用的網路狀態檢查工具。

常見的ping命令:

/**** 往目的追擊傳送固定包數 ****/
ping -c 3 www.baidu.com   // ping百度傳送3個包

/**** 設定兩次發包之間的等待時間 ****/
ping -i 5 www.baidu.com   // 兩包之間的時間間隔為5s
ping -i 0.1 www.baidu.com // 兩包之間的時間間隔為0.1s

/**** 檢查本地網路介面是否已經啟動並正在執行  ****/
ping 127.0.0.1  (linux: ping 0) 
ping localhost 

/**** 超級使用者可以利用 -f 幾秒鐘傳送數十萬個包給主服務造成壓力 *****/
sudo ping -f www.baidu.com 

/**** 讓電腦發出蜂鳴聲: 響應包到達目時,會發出聲音  ****/
ping -a www.baidu.com 

/**** 只列印ping的彙總結果  ****/
ping -c 5 -q www.baidu.com

/**** 修改ping包(icmp包)的大小 ****/
ping -s 100 -c 5 www.baidu.com




複製程式碼

示例:

macdeiMac:PhoneNetSDK ethan$ ping www.baidu.com

PING www.a.shifen.com (61.135.169.121): 56 data bytes
64 bytes from 61.135.169.121: icmp_seq=0 ttl=49 time=32.559 ms
64 bytes from 61.135.169.121: icmp_seq=1 ttl=49 time=32.413 ms
64 bytes from 61.135.169.121: icmp_seq=2 ttl=49 time=32.489 ms
^C
--- www.a.shifen.com ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 32.413/32.487/32.559/0.060 ms
macdeiMac:PhoneNetSDK ethan$ 
複製程式碼

分析以上結果:

  • 傳送端資訊

    • www.a.shifen.com (61.135.169.121): 對域名做了自動DNS解析
    • 56 data bytes: 向該主機傳送大小是56位元組的資料包。
  • 主機響應的資訊

    • icmp_seq: 響應包的序列號。
    • ttl: ip資料包的ttl值。
    • time:請求往返耗時。
    • 64 bytes:響應資料包的大小是64個位元組。
  • 統計總結資訊

    • 0.0% packet loss: 總共發了3個包丟包率是0%
    • min/avg/max = 32.413/32.487/32.559:最小/平均/最大往返時間32.413/32.487/32.559

TTL(Time to live): IP資料包的生存時間,單位是hop(跳)。比如64,每過一個路由器就把該值減1,如果減到0 就表示路由已經太長了仍然找不到目的主機的網路,就丟棄該包。

問題:在發包時,為什麼傳送的是56位元組的包,主機響應的卻是64位元組的包? 在這裡的56和64是同一個概念嗎?

icmp

網際網路控制訊息協議(英語:Internet Control Message Protocol,縮寫:ICMP)是網際網路協議族的核心協議之一。它是TCP/IP協議族的一個子協議,它用於TCP/IP網路中傳送控制訊息,提供可能發生在通訊環境中的各種問題反饋,通過這些資訊,使管理者可以對所發生的問題作出診斷,然後採取適當的措施解決。

控制訊息有:目的不可達下次,超時資訊,重定向訊息,時間戳請求和時間戳響應訊息,回顯請求和回顯應答訊息。

ICMP [1]依靠IP來完成它的任務,它是IP的主要部分。它與傳輸協議(如TCP和UDP)顯著不同:它一般不用於在兩點間傳輸資料。它通常不由網路程式直接使用,除了ping和traceroute這兩個特別的例子。 IPv4中的ICMP被稱作ICMPv4,IPv6中的ICMP則被稱作ICMPv6。

icmp技術細節

CMP是在RFC 792中定義的網際網路協議族之一。通常用於返回的錯誤資訊或是分析路由。ICMP錯誤訊息總是包括了源資料並返回給傳送者。 ICMP錯誤訊息的例子之一是TTL值過期。每個路由器在轉發資料包的時候都會把IP包頭中的TTL值減1。如果TTL值為0,“TTL在傳輸中過期”的訊息將會回報給源地址。 每個ICMP訊息都是直接封裝在一個IP資料包中的,因此,和UDP一樣,ICMP是不可靠的。

雖然ICMP是包含在IP資料包中的,但是對ICMP訊息通常會特殊處理,會和一般IP資料包的處理不同,而不是作為IP的一個子協議來處理。在很多時候,需要去檢視ICMP訊息的內容,然後傳送過當的錯誤訊息到那個原來產生IP資料包的程式,即那個導致ICMP資訊被傳送的IP資料包。

很多常用的工具是基於ICMP訊息的。traceroute是通過傳送包含有特殊的TTL的包,然後接收ICMP超超訊息和目標不可達訊息來實現的。ping則是用ICMP的”Echo request”(類別程式碼:8)和”Echo reply”(類別程式碼:0)訊息來實現的。

icmp報文結構

報頭

ICMP報頭從IP報頭的第160位開始(ip首部20位元組)

在 iOS 平臺實現Ping 和 traceroute

  • Type: ICMP的型別,標識生成的錯誤報文
  • Code: 進一步割分ICMP的型別,該欄位用來查詢產生錯誤的原因;例如ICMP的目標不可達型別可以把這個位設定為1-15等來表示不同的意思。
  • Checksum : 校驗碼部分,這個欄位包含有從ICMP報頭和資料部分計算得來,用於檢查錯誤的資料,其中此校驗碼欄位的值視為0
  • ID :這個欄位包含了ID值,在Echo Reply型別的訊息中要返回這個欄位
  • Sequence : 這個欄位包含一個序號,同樣要在Echo Reply型別的訊息中要返回這個欄位

填充資料

填充的資料緊接在ICMP報頭的後面(以8位為一組):

  • Linux的ping工具填充的ICMP除了8個8位元組的報頭以外,預設情況下還另外填充資料使得總大小位64位元組。
  • Windows的ping.exe填充的ICMP除了8個8位元組的報頭以外,預設情況下還另外填充資料使得總大小位40位元組。

ping

ping實現原理

Ping是為了測試另一臺主機是否可達,現在已經成為一種常用的網路狀態檢查工具。該程式傳送一份 ICMP回顯請求報文給遠端主機,並等待返回 ICMP回顯應答。

ping 使用的是ICMP協議,它傳送icmp回送請求訊息給目的主機。ICMP協議規定:目的主機必須返回ICMP回送應答訊息給源主機。如果源主機在一定時間內收到應答,則認為主機可達。大多數的 TCP/IP 實現都在核心中直接支援Ping伺服器,ICMP回顯請求和回顯應答報文如下圖所示。

image

ping的原理:

在 iOS 平臺實現Ping 和 traceroute

ping的原理是用型別碼為8的ICMP發請求,收到請求的主機則用型別碼為0的ICMP回應。通過計算ICMP應答報文數量和與接受與傳送報文之間的時間差,判斷當前的網路狀態。這個往返時間的計算方法是:ping命令在傳送ICMP報文時將當前的時間值儲存在ICMP報文中發出,當應答報文返回時,使用當前時間值減去存放在ICMP報文資料中存放傳送請求的時間值來計算往返時間。ping返回接收到的資料包文位元組大小、TTL值以及往返時間。

利用wireshark檢視ping

我在命令列中ping www.baidu.com 以下是顯示結果:

image

image

如上圖所示,icmp包的type是8 , 是request請求; icmp的包type是0 ,是reply.

計算機網路基礎知識

TCP/IP協議棧與資料包封裝

OSI七層模型以及TCP/IP模型:

在 iOS 平臺實現Ping 和 traceroute

兩臺計算機通過TCP/IP的通訊過程如下:

在 iOS 平臺實現Ping 和 traceroute

傳輸層及其以下的機制由核心提供,應用層由使用者程式提供,應用程式對通訊資料的含義進行解釋,而傳輸層及其以下處理通訊的細節,將資料從一臺計算機通過一定的路徑傳送到另一臺計算機。應用層資料通過協議棧發到網路上時,每層協議都要加上一個資料首部(header),稱為封裝(Encapsulation)。

TCP/IP資料包的封裝:

在 iOS 平臺實現Ping 和 traceroute

目的主機收到資料包後,經過各層協議棧最後到達應用程式。

在 iOS 平臺實現Ping 和 traceroute

乙太網驅動程式首先根據乙太網首部中的“上層協議”欄位確定該資料幀的有效載荷是IP、ARP還是RARP協議的資料包,然後交給相應的協議處理。假如是IP資料包,IP協議再根據IP首部中的“上層協議”欄位確定該資料包的有效載荷是TCP、UDP、ICMP還是IGMP,然後交給相應的協議處理。假如是TCP段或UDP段,TCP或UDP協議再根據TCP首部或UDP首部的“埠號”欄位確定應該將應用層資料交給哪個使用者程式。IP地址是標識網路中不同主機的地址,而埠號就是同一臺主機上標識不同程式的地址,IP地址和埠號合起來標識網路中唯一的程式。

注意,雖然IP、ARP和RARP資料包都需要乙太網驅動程式來封裝成幀,但是從功能上劃分,ARP和RARP屬於鏈路層,IP屬於網路層。雖然ICMP、IGMP、TCP、UDP的資料都需要IP協議來封裝成資料包,但是從功能上劃分,ICMP、IGMP與IP同屬於網路層,TCP和UDP屬於傳輸層。

IP資料包格式

IPv4資料包格式如下:

在 iOS 平臺實現Ping 和 traceroute

關於首部長度:

根據IP資料包,判斷當前包是否是IPv4


version佔4位,首部長度佔4位,version = 4(IPv4), ipheader=20.
由於首部長度是以4位元組為單位的-> version: 0100 ; 首部長度:0101

獲取version:  0100 0101 & 0xFO(11110000) = 01000000 = 0x40
獲取首部長度:  0100 0101 & 0x0F(00001111) = 0000 0101 = 5個4位元組 = 20 Byte

複製程式碼

ping實現(c++&oc)

技術預研與構思

根據ping的結果,我們需要解決以下問題:

macdeiMac:PhoneNetSDK ethan$ ping www.baidu.com

PING www.a.shifen.com (61.135.169.121): 56 data bytes
64 bytes from 61.135.169.121: icmp_seq=0 ttl=49 time=32.559 ms
64 bytes from 61.135.169.121: icmp_seq=1 ttl=49 time=32.413 ms
64 bytes from 61.135.169.121: icmp_seq=2 ttl=49 time=32.489 ms
^C
--- www.a.shifen.com ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 32.413/32.487/32.559/0.060 ms
macdeiMac:PhoneNetSDK ethan$ 
複製程式碼
  • DNS解析(域名->ip)
  • 本地終端接收到的每個icmp包來自哪個主機
  • icmp_seq
  • ttl
  • time

以上問題解決方案如下:

  • DNS解析: socket支援
  • 本地終端接收到的每個icmp包來自哪個主機: ip包中的source
  • icmp_seq: icmp包中的 sequence number
  • ttl: ip包中的Time to live
  • time: 傳送包和接收到包時的時間差

具體實現

IP包定義:

typedef struct PNetIPHeader {
    uint8_t versionAndHeaderLength;
    uint8_t differentiatedServices;
    uint16_t totalLength;
    uint16_t identification;
    uint16_t flagsAndFragmentOffset;
    uint8_t timeToLive;
    uint8_t protocol;
    uint16_t headerChecksum;
    uint8_t sourceAddress[4];
    uint8_t destinationAddress[4];
    // options...
    // data...
}PNetIPHeader;
複製程式碼

ICMP包定義:

/*
 use linux style . totals 64B
 */
typedef struct UICMPPacket
{
    uint8_t type;
    uint8_t code;
    uint16_t checksum;
    uint16_t identifier;
    uint16_t seq;
    char fills[56];  // data
}UICMPPacket;
複製程式碼

構造ICMP包:

+ (UICMPPacket *)constructPacketWithSeq:(uint16_t)seq andIdentifier:(uint16_t)identifier
{
    UICMPPacket *packet = (UICMPPacket *)malloc(sizeof(UICMPPacket));
    packet->type  = ENU_U_ICMPType_EchoRequest;
    packet->code = 0;
    packet->checksum = 0;
    packet->identifier = OSSwapHostToBigInt16(identifier);
    packet->seq = OSSwapHostToBigInt16(seq);
    memset(packet->fills, 65, 56);
    packet->checksum = [self in_cksumWithBuffer:packet andSize:sizeof(UICMPPacket)];
    return packet;
}
複製程式碼

傳送icmp包:

 UICMPPacket *packet = [PhoneNetDiagnosisHelper constructPacketWithSeq:index andIdentifier:identifier];
        _sendDate = [NSDate date];
        ssize_t sent = sendto(socket_client, packet, sizeof(UICMPPacket), 0, (struct sockaddr *)&remote_addr, (socklen_t)sizeof(struct sockaddr));
        if (sent < 0) {
            log4cplus_warn("PhoneNetPing", "ping %s , send icmp packet error..\n",[self.host UTF8String]);
        }
複製程式碼

接收icmp包:

 size_t bytesRead = recvfrom(socket_client, buffer, 65535, 0, (struct sockaddr *)&ret_addr, &addrLen);
  if ((int)bytesRead < 0) {
            [self reporterPingResWithSorceIp:self.host ttl:0 timeMillSecond:0 seq:0 icmpId:0 dataSize:0 pingStatus:PhoneNetPingStatusDidTimeout];
            res = YES;
        }else if(bytesRead == 0){
            log4cplus_warn("PhoneNetPing", "ping %s , receive icmp packet error , bytesRead=0",[self.host UTF8String]);
        }else{
            
            if ([PhoneNetDiagnosisHelper isValidPingResponseWithBuffer:(char *)buffer len:(int)bytesRead]) {
                
                UICMPPacket *icmpPtr = (UICMPPacket *)[PhoneNetDiagnosisHelper icmpInpacket:(char *)buffer andLen:(int)bytesRead];
                
                int seq = OSSwapBigToHostInt16(icmpPtr->seq);
                
                NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:_sendDate];
                
                int ttl = ((PNetIPHeader *)buffer)->timeToLive;
                int size = (int)(bytesRead-sizeof(PNetIPHeader));
                NSString *sorceIp = self.host;
                
                
//                NSLog(@"PhoneNetPing, ping %@ , receive icmp packet..\n",self.host );
                [self reporterPingResWithSorceIp:sorceIp  ttl:ttl timeMillSecond:duration*1000 seq:seq icmpId:OSSwapBigToHostInt16(icmpPtr->identifier) dataSize:size pingStatus:PhoneNetPingStatusDidReceivePacket];
                res = YES;
            }
複製程式碼

從接收到的buffer中分離icmp包:

/* 從 ipv4 資料包中解析出icmp */
+ (char *)icmpInpacket:(char *)packet andLen:(int)len
{
    if (len < (sizeof(PNetIPHeader) + sizeof(UICMPPacket))) {
        return NULL;
    }
    const struct PNetIPHeader *ipPtr = (const PNetIPHeader *)packet;
    if ((ipPtr->versionAndHeaderLength & 0xF0) != 0x40 // IPv4
        ||
        ipPtr->protocol != 1) { //ICMP
        return NULL;
    }
    size_t ipHeaderLength = (ipPtr->versionAndHeaderLength & 0x0F) * sizeof(uint32_t);
    
    if (len < ipHeaderLength + sizeof(UICMPPacket)) {
        return NULL;
    }
    
    return (char *)packet + ipHeaderLength;
}
複製程式碼

校驗接收到的icmp包:

+ (BOOL)isValidPingResponseWithBuffer:(char *)buffer len:(int)len
{
    UICMPPacket *icmpPtr = (UICMPPacket *)[self icmpInpacket:buffer andLen:len];
    if (icmpPtr == NULL) {
        return NO;
    }
    uint16_t receivedChecksum = icmpPtr->checksum;
    icmpPtr->checksum = 0;
    uint16_t calculatedChecksum = [self in_cksumWithBuffer:icmpPtr andSize:len-((char*)icmpPtr - buffer)];
    
    return receivedChecksum == calculatedChecksum &&
    icmpPtr->type == ENU_U_ICMPType_EchoReplay &&
    icmpPtr->code == 0 &&
    OSSwapBigToHostInt16(icmpPtr->identifier)>=KPingIcmpIdBeginNum;;
}
複製程式碼

TCP ping

當有些伺服器禁ping時,可以選擇TCP ping。

TCP ping原理

在 iOS 平臺實現Ping 和 traceroute

通過和目的主機及其埠建立TCP連線的方式計算其連線耗時。

traceroute

traceroute命令

/**** 設定每個路由傳送的包數 ****/
traceroute -q 5 baidu.com

/**** 設定最大路由跳數 ****/
traceroute -m 5 baidu.com

/**** 不做DNS解析 ****/
traceroute -n baidu.com

/**** 繞過路由表直接傳送到目的治具 ****/
traceroute -r baidu.com

/**** 使用ICMP包取代UDP包 ****/
traceroute -I baidu.com
複製程式碼

traceroute原理

tacceroute是利用增加存活時間(TTL)值來實現功能的。每當一個icmp包經過一個路由器時,其存活時間值就會減1,當其存活時間為0時,路由器便會取消包傳送,併傳送一個ICMP TTL封包給原封包發出者。

在 iOS 平臺實現Ping 和 traceroute

traceroute過程

主叫方首先發出TTL = 1 的資料包,第一個路由器將 TTL 減1得0後就不再繼續轉發此資料包,而是返回一個ICMP超時報文,主叫方從超時報文中即可提取出資料包所經過的第一個路由器的地址。然後又發出一個TTL=2的ICMP資料包,可獲得第二個路由器的地址,依次增加TTL便獲取了沿途所有路由器位地址。

需要注意的是,並不是所有路由器都會如實返回ICMP超時報文。出於安全性考慮,大多數防火牆以及啟動了防火牆功能的路由器預設配置為不返回各種ICMP報文,其路由器或交換機也可被管理員主動修改配置變為不返回ICMP報文。因此Traceroute程式不一定能拿全所有沿途路由器地址。所以當某個TTL值的資料包得不到響應是,並不能停止這一追蹤過程,程式仍然會把TTL遞增而發出下一個資料包。一直達到預設或用於引數制定的追蹤限制時才結束追蹤。

依據上述原理,利用了UDP資料包的Traceroute程式在資料包到達真正的目的主機時,就可能因為該主機沒有提供UDP服務而簡單將資料包丟棄,並不返回任何資訊。為了解決這個問題,Traceroute故意使用了一個大於30000的埠號,因UDP協議規定埠號必須小於30000,所以目標主機收到資料包後唯一能做的事就是返回一個"埠不可達"的ICMP報文,於是主叫方就將埠不可達報文當做跟蹤結束標誌。

利用wireshark檢視traceroute

我在命令列中traceroute www.baidu.com 以下是顯示結果:

image

如上圖所示,UDP請求,第一個請求的埠是33435 , 接下來的UDP請求,埠會遞增。

當到達目的地址時,目的地址會replay型別為3的包.

image

如上圖所示,是路由器返回的ICMP包,type是11。

UDP traceroute的實現

傳送udp包,接收ip+icmp包,過濾route ip計算時間。

github.com/mediaios/ne…

UDP traceroute存在的問題

使用 UDP 的 traceroute,失敗還是比較常見的。這常常是由於,在運營商的路由器上,UDP 與 ICMP 的待遇大不相同。為了利於 troubleshooting,ICMP 的request 和 replay 是不會封的,而 UDP 則不同。UDP 常被用來做網路攻擊,因為 UDP 無需連線,因而沒有任何狀態約束它,比較方便攻擊者偽造源 IP、偽造目的埠傳送任意多的 UDP 包,長度自定義。所以運營商為安全考慮,對於 UDP 埠常常採用白名單 ACL,就是隻有 ACL 允許的埠才可以通過,沒有明確允許的則統統丟棄。比如允許 DNS/DHCP/SNMP 等。

icmp traceroute

傳送icmp包,型別為8,每個路由返回的icmp包型別是11的超時包,當到達目的地址時,目的地址會replay型別為0的包

在 iOS 平臺實現Ping 和 traceroute

icmp traceroute的實現

github.com/mediaios/ne…

net-diagnosis(ios平臺下網路診斷SDK)

net-diagnosis是ios平臺下的網路診斷SDK,提供的功能有:

  • ping
  • tcp ping
  • traceroute
  • icmp traceroute
  • nslookup
  • port scan

專案地址: github

後續更多關於網路診斷的功能會不斷開發完善,歡迎提交issue

另,歡迎fork和star !

相關文章