395
技術社區[雲棲]
扯談網絡編程之自己實現ping
ping是基於ICMP(Internet Control Message Protocol)協議實現的,而ICMP協議是在IP層實現的。
ping實際上是發起者發送一個Echo Request(type = 8)的,遠程主機回應一個Echo Reply(type = 0)的過程。
為什麼用ping不能測試某一個端口
剛開始接觸網絡的時候,可能很多人都有疑問,怎麼用ping來測試遠程主機的某個特定端口?
其實如果看下ICMP協議,就可以發現ICMP裏根本沒有端口這個概念,也就根本無法實現測試某一個端口了。
ICMP協議的包格式(來自wiki):
| Bits 0–7 | Bits 8–15 | Bits 16–23 | Bits 24–31 | |
|---|---|---|---|---|
|
IP Header (20 bytes) |
Version/IHL | Type of service | Length | |
| Identification | flags and offset | |||
| Time To Live (TTL) | Protocol | Checksum | ||
| Source IP address | ||||
| Destination IP address | ||||
|
ICMP Header (8 bytes) |
Type of message | Code | Checksum | |
| Header Data | ||||
|
ICMP Payload (optional) |
Payload Data | |||
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Type = 8 | Code = 0 | Header Checksum | |||||||||||||||||||||||||||||
| Identifier | Sequence Number | ||||||||||||||||||||||||||||||
| Data | |||||||||||||||||||||||||||||||
Ping如何計算請問耗時
在ping命令的輸出上,可以看到有顯示請求的耗時,那麼這個耗時是怎麼得到的呢?
64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=6.28 ms
從Echo Request的格式裏,看到不時間相關的東東,但是因為是Echo,即遠程主機會原樣返回Data數據,所以Ping的發起方把時間放到了Data數據裏,當得到Echo Reply裏,取到發送時間,再和當前時間比較,就可以得到耗時了。當然,還有其它的思路,比如記錄每一個包的發送時間,當得到返回時,再計算得到時間差,但顯然這樣的實現太複雜了。
Ping如何區分不同的進程?
我們都知道本機IP,遠程IP,本機端口,遠程端口,四個元素才可以確定唯的一個信道。而ICMP裏沒有端口,那麼一個ping程序如何知道哪些包才是發給自己的?或者說操作係統如何區別哪個Echo Reply是要發給哪個進程的?
實際上操作係統不能區別,所有的本機IP,遠程IP相同的ICMP程序都可以接收到同一份數據。
程序自己要根據Identifier來區分到底一個ICMP包是不是發給自己的。在Linux下,Ping發出去的Echo Request包裏Identifier就是進程pid,遠程主機會返回一個Identifier相同的Echo Reply包。
可以接下麵的方法簡單驗證:
啟動係統自帶的ping程序,查看其pid。
設定自己實現的ping程序的identifier為上麵得到的pid,然後發Echo Request包。
可以發現係統ping程序會接收到遠程主機的回應。
自己實現ping
自己實現ping要用到rawsocket,在linux下需要root權限。網上有很多實現的程序,但是有很多地方不太對的。自己總結實現了一個(最好用g++編繹):
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <errno.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
unsigned short csum(unsigned short *ptr, int nbytes) {
register long sum;
unsigned short oddbyte;
register short answer;
sum = 0;
while (nbytes > 1) {
sum += *ptr++;
nbytes -= 2;
}
if (nbytes == 1) {
oddbyte = 0;
*((u_char*) &oddbyte) = *(u_char*) ptr;
sum += oddbyte;
}
sum = (sum >> 16) + (sum & 0xffff);
sum = sum + (sum >> 16);
answer = (short) ~sum;
return (answer);
}
inline double countMs(timeval before, timeval after){
return (after.tv_sec - before.tv_sec)*1000 + (after.tv_usec - before.tv_usec)/1000.0;
}
#pragma pack(1)
struct EchoPacket {
u_int8_t type;
u_int8_t code;
u_int16_t checksum;
u_int16_t identifier;
u_int16_t sequence;
timeval timestamp;
char data[40]; //sizeof(EchoPacket) == 64
};
#pragma pack()
void ping(in_addr_t source, in_addr_t destination) {
static int sequence = 1;
static int pid = getpid();
static int ipId = 0;
char sendBuf[sizeof(iphdr) + sizeof(EchoPacket)] = { 0 };
struct iphdr* ipHeader = (iphdr*)sendBuf;
ipHeader->version = 4;
ipHeader->ihl = 5;
ipHeader->tos = 0;
ipHeader->tot_len = htons(sizeof(sendBuf));
ipHeader->id = htons(ipId++);
ipHeader->frag_off = htons(0x4000); //set Flags: don't fragment
ipHeader->ttl = 64;
ipHeader->protocol = IPPROTO_ICMP;
ipHeader->check = 0;
ipHeader->saddr = source;
ipHeader->daddr = destination;
ipHeader->check = csum((unsigned short*)ipHeader, ipHeader->ihl * 2);
EchoPacket* echoRequest = (EchoPacket*)(sendBuf + sizeof(iphdr));
echoRequest->type = 8;
echoRequest->code = 0;
echoRequest->checksum = 0;
echoRequest->identifier = htons(pid);
echoRequest->sequence = htons(sequence++);
gettimeofday(&(echoRequest->timestamp), NULL);
u_int16_t ccsum = csum((unsigned short*)echoRequest, sizeof(sendBuf) - sizeof(iphdr));
echoRequest->checksum = ccsum;
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(0);
sin.sin_addr.s_addr = destination;
int s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (s == -1) {
perror("socket");
return;
}
//IP_HDRINCL to tell the kernel that headers are included in the packet
if (setsockopt(s, IPPROTO_IP, IP_HDRINCL, "1",sizeof("1")) < 0) {
perror("Error setting IP_HDRINCL");
exit(0);
}
sendto(s, sendBuf, sizeof(sendBuf), 0, (struct sockaddr *) &sin, sizeof(sin));
char responseBuf[sizeof(iphdr) + sizeof(EchoPacket)] = {0};
struct sockaddr_in receiveAddress;
socklen_t len = sizeof(receiveAddress);
int reveiveSize = recvfrom(s, (void*)responseBuf, sizeof(responseBuf), 0, (struct sockaddr *) &receiveAddress, &len);
if(reveiveSize == sizeof(responseBuf)){
EchoPacket* echoResponse = (EchoPacket*) (responseBuf + sizeof(iphdr));
//TODO check identifier == pid ?
if(echoResponse->type == 0){
struct timeval tv;
gettimeofday(&tv, NULL);
in_addr tempAddr;
tempAddr.s_addr = destination;
printf("%d bytes from %s : icmp_seq=%d ttl=%d time=%.2f ms\n",
sizeof(EchoPacket),
inet_ntoa(tempAddr),
ntohs(echoResponse->sequence),
((iphdr*)responseBuf)->ttl,
countMs(echoResponse->timestamp, tv));
}else{
printf("response error, type:%d\n", echoResponse->type);
}
}else{
printf("error, response size != request size.\n");
}
close(s);
}
int main(void) {
in_addr_t source = inet_addr("192.168.1.100");
in_addr_t destination = inet_addr("192.168.1.1");
for(;;){
ping(source, destination);
sleep(1);
}
return 0;
}
安全相關的一些東東:
死亡之Ping https://zh.wikipedia.org/wiki/%E6%AD%BB%E4%BA%A1%E4%B9%8BPing
盡管是很老的漏洞,但是也可以看出協議棧的實現也不是那麼的靠譜。
Ping flood https://en.wikipedia.org/wiki/Ping_flood
服務器關閉ping服務,默認是0,是開啟:
echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
總結:
在自己實現的過程中,發現有一些蛋疼的地方,如
協議文檔不夠清晰,得反複對照;
有時候一個小地方處理不對,很難查bug,即使程序能正常工作,但也並不代表它是正確的;
用wireshark可以很方便驗證自己寫的程序有沒有問題。
參考:
https://en.wikipedia.org/wiki/Ping_(networking_utility)
https://en.wikipedia.org/wiki/ICMP_Destination_Unreachable
https://tools.ietf.org/pdf/rfc792.pdf
最後更新:2017-04-03 12:56:30