閱讀395 返回首頁    go 阿裏雲 go 技術社區[雲棲]


扯談網絡編程之自己實現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
Echo Request的ICMP包格式(from wiki):

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

  上一篇:go [算法係列之四]優先級隊列
  下一篇:go iOS應用發布Invalid Binary問題解決方案