乐趣区

在-iOS-平台实现Ping-和-traceroute

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 字节)

  • 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 回显请求和回显应答报文如下图所示。

ping 的原理:

ping 的原理是用类型码为 8 的 ICMP 发请求,收到请求的主机则用类型码为 0 的 ICMP 回应。通过计算 ICMP 应答报文数量和与接受与发送报文之间的时间差,判断当前的网络状态。这个往返时间的计算方法是:ping 命令在发送 ICMP 报文时将当前的时间值存储在 ICMP 报文中发出,当应答报文返回时,使用当前时间值减去存放在 ICMP 报文数据中存放发送请求的时间值来计算往返时间。ping 返回接收到的数据报文字节大小、TTL 值以及往返时间。

利用 wireshark 查看 ping

我在命令行中 ping www.baidu.com 以下是显示结果:

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

计算机网络基础知识

TCP/IP 协议栈与数据包封装

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

两台计算机通过 TCP/IP 的通信过程如下:

传输层及其以下的机制由内核提供,应用层由用户进程提供, 应用程序对通讯数据的含义进行解释,而传输层及其以下处理通讯的细节,将数据从一台计算机通过一定的路径发送到另一台计算机。应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation)。

TCP/IP 数据包的封装:

目的主机收到数据包后,经过各层协议栈最后到达应用程序。

以太网驱动程序首先根据以太网首部中的“上层协议”字段确定该数据帧的有效载荷是 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 数据包格式如下:

关于首部长度:

根据 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 原理

通过和目的主机及其端口建立 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 封包给原封包发出者。

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 以下是显示结果:

如上图所示,UDP 请求,第一个请求的端口是 33435,接下来的 UDP 请求,端口会递增。

当到达目的地址时,目的地址会 replay 类型为 3 的包.

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

UDP traceroute 的实现

发送 udp 包,接收 ip+icmp 包, 过滤 route ip 计算时间。

https://github.com/mediaios/n…

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 的包

icmp traceroute 的实现

https://github.com/mediaios/n…

net-diagnosis(ios 平台下网络诊断 SDK)

net-diagnosis是 ios 平台下的网络诊断 SDK,提供的功能有:

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

项目地址:github

后续更多关于网络诊断的功能会不断开发完善,欢迎提交 issue

另,欢迎 fork 和 star !

退出移动版