TCP/IP详解卷1:第六章(ICMP:internet控制报文协议)

1. ICMP基础知识

ICMP经常被认为是IP层的一个组成部分。它传递差错报文以及其他需要注意的信息。ICMP报文通常被IP层或更高层协议(TCP或UDP)使用。一些ICMP报文把差错报文返回给用户进程。


1) ICMP报文的类型

而具体的类型和代码如下:


在对ICMP差错报文进行响应时,永远不会生生成另一份ICMP差错报文(否则会无休止的产生差错报文)。下面这种情况都不会导致产生ICMP差错报文:
1) ICMP差错报文(ICMP查询报文可能会产生ICMP差错报文)
2) 目的地址是广播地址或者多播地址的IP数据报
3) 作为链路层广播的数据报
4) 不是IP分片的第一片
5) 源地址不是单个主机的数据报。这就是说,源地址不能为零地址,环回地址,广播地址或多播地址。
这些规则是为了防止过去允许ICMP差错报文对广播分组响应所带来的广播风暴

2. ICMP地址掩码的请求与应答/ICMP时间戳请求与应答

这部分直接看代码,会比较直观一些:

icmpaddrmask.c:
/*
 * Issue an ICMP address mask request and print the reply.
 *
 * This program originated from the public domain ping program written
 * by Mike Muuss.
 *
 * You must be superuser to run this program (or it must be suid to root)
 * since it requires a raw socket.
 */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/file.h>
#include <sys/time.h>
#include <sys/signal.h>

#include <netinet/in_systm.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
/* #include <netinet/ip_var.h> */
#include <netdb.h>
#include <unistd.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <string.h>

//DEFDATALEN为子网掩码的请求报文长度(类型(1字节) + 代码(1字节) + 校验和(2字节) + 标识符(2字节) + 序列号(2字节) + 32位子网掩码(4字节))
#define	DEFDATALEN	(12)	/* default data length */
#define	MAXIPLEN	60			//IP数据报的最大长度(因为胃4bit,最大为15,所以共有15 * 32bit(IP首部长度)共60字节)
#define	MAXICMPLEN	76			//为什么ICMP的最大长度为76
#define	MAXPACKET	(65536 - 60 - 8)/* max packet size */

struct sockaddr	whereto;	/* who to send request to */
int		datalen = DEFDATALEN;
int		s;
u_char		outpack[MAXPACKET];
char		*hostname;		//存储输入的IP地址(点分十进制)
u_long		inet_addr();
char		*inet_ntoa();
void		sig_alrm(int);
int		response = 0;

main(argc, argv)
int	argc;
char	**argv;
{
	int			i, ch, fdmask, hold, packlen, preload;
	extern int		errno;
	struct hostent		*hp;
	struct sockaddr_in	*to;
	struct protoent		*proto;
	u_char			*packet;
	char			*target, hnamebuf[MAXHOSTNAMELEN], *malloc();

	if (argc != 2)
		exit(1);

	target = argv[1];

	bzero((char *)&whereto, sizeof(struct sockaddr));
	to = (struct sockaddr_in *)&whereto;
	to->sin_family = AF_INET;

		/* try to convert as dotted decimal address,
		   else if that fails assume it's a hostname */
	to->sin_addr.s_addr = inet_addr(target);
	//说明target是一个IP地址
	if (to->sin_addr.s_addr != (u_int)-1)
		hostname = target;
	else {
		//说明target是一个域名或主机名
		hp = gethostbyname(target);
		if (!hp) {
			fprintf(stderr, "unknown host %s\n", target);
			exit(1);
		}
		to->sin_family = hp->h_addrtype;
		bcopy(hp->h_addr, (caddr_t)&to->sin_addr, hp->h_length);
		strncpy(hnamebuf, hp->h_name, sizeof(hnamebuf) - 1);
		hostname = hnamebuf;
	}

	packlen = datalen + MAXIPLEN + MAXICMPLEN;		//=12 + 60 + 76
	if ( (packet = (u_char *)malloc((u_int)packlen)) == NULL) {
		fprintf(stderr, "malloc error\n");
		exit(1);
	}

	if ( (proto = getprotobyname("icmp")) == NULL) {
		fprintf(stderr, "unknown protocol icmp\n");
		exit(1);
	}

	if ( (s = socket(AF_INET, SOCK_RAW, proto->p_proto)) < 0) {
		perror("socket");	/* probably not running as superuser */
		exit(1);
	}

	/*
	 * We send one request, then wait 5 seconds, printing any
	 * replies that come back.  This lets us send a request to
	 * a broadcast address and print multiple replies.
	 */

	signal(SIGALRM, sig_alrm);
	alarm(5);	/* 5 second time limit */

	sender();	/* send the request */

	for (;;) {
		struct sockaddr_in	from;
		int			cc, fromlen;

		fromlen = sizeof(from);
		if ( (cc = recvfrom(s, (char *)packet, packlen, 0,
		    		    (struct sockaddr *)&from, &fromlen)) < 0) {
			if (errno == EINTR)
				continue;
			perror("recvfrom error");
			continue;
		}
		procpack((char *)packet, cc, &from);
	}
}

/*
 * Send the ICMP address mask request.
 */

sender()
{
	int		i, cc;
	struct icmp	*icp;

	//所发送的ICMP地址掩码请求数据(后期可指定标识符icmp_id和序列号icmp_seq来把应答和请求相匹配)
	icp = (struct icmp *)outpack;
	icp->icmp_type = ICMP_MASKREQ;	//地址掩码请求
	icp->icmp_code = 0;				//代码为0
	icp->icmp_cksum = 0;	/* compute checksum below */
	icp->icmp_seq = 12345;	/* seq and id must be reflected */	//序列号
	icp->icmp_id = getpid();	//标识符---之所以把进程ID设置为标识符,因为同时运行的程序组不可能存在进程ID相同的两个进程

	icp->icmp_mask = 0;

	cc = ICMP_MASKLEN;	/* 12 = 8 bytes of header, 4 bytes of mask */

		/* compute ICMP checksum here */
	icp->icmp_cksum = in_cksum((u_short *)icp, cc);

	//往指定的目的地址whereto发送ICMP地址掩码请求
	i = sendto(s, (char *)outpack, cc, 0, &whereto,
	    				sizeof(struct sockaddr));
	if (i < 0 || i != cc)  {
		if (i < 0)
			perror("sendto error");
		printf("wrote %s %d chars, ret=%d\n", hostname, cc, i);
	}
}

/*
 * Process a received ICMP message.
 */

procpack(buf, cc, from)
char			*buf;
int			cc;
struct sockaddr_in	*from;
{
	int		i, hlen;
	struct icmp	*icp;
	struct ip	*ip;
	struct timeval	tvdelta;

		/* Check the IP header */
	ip = (struct ip *)buf;
	hlen = ip->ip_hl << 2;		//得到IP数据报的长度(IP数据报的前两个字节为:版本 + IP首部长度)
	if (cc < hlen + ICMP_MINLEN) {
		fprintf(stderr, "packet too short (%d bytes) from %s\n", cc,
			  inet_ntoa(*(struct in_addr *)&from->sin_addr.s_addr));
		return;
	}

		/* Now the ICMP part */
	cc -= hlen;
	icp = (struct icmp *)(buf + hlen);

		/* With a raw ICMP socket we get all ICMP packets that
		   come into the kernel. */

	//如果得到的是子网掩码的应答
	//不知道为什么ICMP_MASKREPLY的值不等于17
	if (ICMP_MASKREPLY != 17)
		printf("error!\n");
	if (icp->icmp_type == 17) {
		if (cc != ICMP_MASKLEN)
			printf("cc = %d, expected cc = 12\n", cc);
		//判断序列号是否一致
		if (icp->icmp_seq != 12345)
			printf("received sequence # %d\n", icp->icmp_seq);
		//判断标识符是否一致
		if (icp->icmp_id != getpid())
			printf("received id %d\n", icp->icmp_id);

		//如果标识符一致并且序列号一致,则打印其子网掩码和地址
		printf("received mask = %08x, from %s\n",
			ntohl(icp->icmp_mask),
			inet_ntoa(*(struct in_addr *) &from->sin_addr.s_addr));
		response++;
	}
	/* We ignore all other types of ICMP messages */
}

/*
 * in_cksum --
 *	Checksum routine for Internet Protocol family headers (C Version)
 */
in_cksum(addr, len)
	u_short *addr;
	int len;
{
	register int nleft = len;
	register u_short *w = addr;
	register int sum = 0;
	u_short answer = 0;

	/*
	 * Our algorithm is simple, using a 32 bit accumulator (sum), we add
	 * sequential 16 bit words to it, and at the end, fold back all the
	 * carry bits from the top 16 bits into the lower 16 bits.
	 */
	while (nleft > 1)  {
		sum += *w++;
		nleft -= 2;
	}

	/* mop up an odd byte, if necessary */
	if (nleft == 1) {
		*(u_char *)(&answer) = *(u_char *)w ;
		sum += answer;
	}

	/* add back carry outs from top 16 bits to low 16 bits */
	sum = (sum >> 16) + (sum & 0xffff);	/* add hi 16 to low 16 */
	sum += (sum >> 16);			/* add carry */
	answer = ~sum;				/* truncate to 16 bits */
	return(answer);
}

void
sig_alrm(int signo)
{
	if (response == 0) {
		printf("timeout\n");
		exit(1);
	}
	exit(0);	/* we got one or more responses */
}
可以通过单步调试的方式,进行学习。程序运行如下:
root@ThinkPad-T430i:/home/leichaojian# ./icmpaddrmask 192.168.0.5
error!
received mask = 00000000, from 192.168.0.5


3. ICMP端口不可达

UDP的规则之一是:如果收到一份UDP数据报而目的端口与某个正在使用的进程不相符,那么UDP返回一个ICMP不可达报文,可以用TFTP来强制生成一个端口不可达报文。
leichaojian@ThinkPad-T430i:~$ tftp
tftp> connect svr4 8888
tftp> get temp.foo
Transfer timed out.

我们执行如下命令:
root@ThinkPad-T430i:/home/leichaojian# tcpdump -e > temp.foo
我们把temp.foo里面关于8888端口的日志打印出来如下:
20:51:54.096199 00:21:cc:cc:05:d4 (oui Unknown) > c8:3a:35:64:96:d8 (oui Unknown), ethertype IPv4 (0x0800), length 62: 192.168.0.5.45810 > 218.30.64.194.8888: UDP, length 20
20:51:59.096378 00:21:cc:cc:05:d4 (oui Unknown) > c8:3a:35:64:96:d8 (oui Unknown), ethertype IPv4 (0x0800), length 62: 192.168.0.5.45810 > 218.30.64.194.8888: UDP, length 20
20:52:04.096526 00:21:cc:cc:05:d4 (oui Unknown) > c8:3a:35:64:96:d8 (oui Unknown), ethertype IPv4 (0x0800), length 62: 192.168.0.5.45810 > 218.30.64.194.8888: UDP, length 20
20:52:09.096700 00:21:cc:cc:05:d4 (oui Unknown) > c8:3a:35:64:96:d8 (oui Unknown), ethertype IPv4 (0x0800), length 62: 192.168.0.5.45810 > 218.30.64.194.8888: UDP, length 20
从中我们可以看出UDP的数据报长度为20.下图为ICMP完整长度:
而ICMP不可达报文的一般格式如下:


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值