ICMP在IP系统间传递差错和管理报文,是任何IP系统必须实现的组成部分。Linux 2.6.34中ICMP模块的实现在linux/icmp.h,net/icmp.h和ipv4/icmp.c中,导出了icmp_err_convert数组和icmp_send函数,供其它网络子系统使用。在其它网络子系统中,当检测到错误时,调用icmp_send产生并发送相应的ICMP差错消息到源主机;当源主机收到ICMP不可达差错消息,传递到原始套接字和传输层,而它们使用icmp_err_convert把对应的消息代码转换成套接字层比较容易理解的错误代码。在内核空间中可发送的ICMP消息包括查询应答和差错报文,下面总结了产生这两类消息的网络子系统(及函数)与错误转换。
应答消息
应答消息由ICMP模块的内部函数icmp_reply而非icmp_send发送。根据RFC1122 3.2.2.9规范, 除非一个主机作为地址掩码代理,否则不能发送回复,这对应ICMP的icmp_address实现为空,因此上表没有列出地址掩码应答项(内核符号为ICMP_ADDRESSREPLY)。
差错消息
差错消息由中间路由器或目的主机产生,当数据报不能成功提交给目的主机时。从上表可见,在IP层的接收、本地处理、转发和输出各过程中,都可能产生差错消息;在传输层如果对应的端口没有打开,那么UDP会产生ICMP端口不可达差错,而TCP则会使用自己的差错处理机制发送一个RST复位包,这也是上表没有列出TCP子系统的原因。对于重定向差错,由ICMP模块的icmp_redirect调用ip_rt_redirect更新路由;其它差错则由icmp_unreach处理。
错误转换
第2列为icmp_err_convert数组索引,第4列也就是调用socket API出错时返回的errno,最后1列为icmp_err_convert中的fatal成员取值,0表示非致命错误,1表示致命错误,需要报告给用户进程。错误转换会被RAW的raw_err、TCP的tcp_v4_err和UDP的udp_err用到,对于ICMP_DEST_UNREACH类型的差错,使用上表转换;ICMP_SOURCE_QUENCH类型的忽略不处理;ICMP_PARAMETERPROB类型的转换成EPROTO(协议错误);ICMP_TIME_EXCEEDED类型的转换成EHOSTUNREACH。
在这要注意,从ICMP_PORT_UNREACH到ECONNREFUSED的转换,不适用于TCP,原因已在上节说明;而对于UDP的未连接套接字,如果主机在线而端口没打开,调用sendto得不到ECONNREFUSED错误,但recvfrom会阻塞,这是因为虽然内核收到了ICMP差错,但没上报给应用进程。尽管如此,如果想得到ECONNREFUSED错误,那么可以写个ICMP守护进程,应用进程先把它的套接字描述符通过unix域套接口传递到ICMP守护进程,而守护进程使用raw socket来接收ICMP差错,再发给应用进程。
tcp
telnet 10.10.X.y 9999
RST 报文
udp
root@ubuntu:~# tcpdump -i enahisic2i0 host 10.0.xx.x tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on enahisic2i0, link-type EN10MB (Ethernet), capture size 262144 bytes 14:35:46.226936 IP ubuntu.51519 > 10.0.xx.xx.9999: UDP, length 1 14:35:46.227055 IP 10.10.xx.xx > ubuntu: ICMP 10.10.xx.xx udp port 9999 unreachable, length 37
Linux内核对UDP处理:
(1):作为服务器接受到一个UDP请求:
首先,做为服务器,当一个报文经过查路由,目的ip是上送本机的时候,经过netfilter 判决后,
调用ip_local_deliver_finish,它根据ip头中的协议类型(TCP/UDP/ICMP/......),调用不同的4层接口函数进行处理。所以之前说了,即使开启了TCP服务,服务器建立的socket的hash和udp超找socket的hash不一致,也会回端口不可达。
对于udp而言,handler 是udp_rcv,它直接调用了__udp4_lib_rcv,查找相应的sock,
如果sk不存在if(sk != NULL),就回复icmp destination unreachable(这就是服务器没有对应端口接受UDP的处理流程),函数非常简单
所以作为服务器,收到一个目的端口并未监听的报文,直接回复端口不可达。
那么作为客户端,如何处理服务器回复的 端口不可达 报文呢?
起始当初想法很简单,我认为,不同的协议之间是不会干涉的,即TCP和UDP直接是不会干涉的。
何况这种不伦不类的icmp?后来想错了。
(2)作为客户端收到ICMP端口不可达的回复:
作为客户端,端口不可达报文进入ip_local_deliver_finish,它调用icmp_rcv函数,进行处理。(其实这也是当初我认为客户端udp不会对端口不可达数据进行相应的原因,因为udp处理流程是udp_rcv)。
实际上icmp_rcv函数最重要的是 它调用了:icmp_pointers[icmph->type].handler(skb);
handler = icmp_unreach
icmp_unreach函数最终的一步,就是它最后一步:
是不是很像ip_local_deliver_finish?
是很像,只是ip_local_deliver_finish中,调用了ipprot->handler,而这里调用了ipprot->err_handler
对于udp,err_handler = udp_err = __udp4_lib_err
在该函数中,只有进入如下的流程,应用程序才会反应:
__udp4_lib_err先根据skb->data中dip和sip,查找socket,skb->data是icmp的负载
故先调用 __udp4_lib_lookup 查找socket,传参时,sip和dip需要反一下。
__udp4_lib_err:
先决条件是inet->recverr为非0,或者inet->recverr为0但是udp处于TCP_ESTABLISHED状态。
否则应用程序休想收到该端口不可达的数据,应用程序就等着read超时吧。所以说,为了获取udp端口不可达的情况
有2种方法:
(1):
int val = 1;
setsockopt(fd, IPPROTO_IP, IP_RECVERR , &val,sizeof(int));
(2):
对udp进行connect操作,并且将sendto改成send
4:
udp获知端口不可达的源程序(方法1:设置Socket选项;方法2:对UDP进行Connect)
注意,阻塞情况下,recvfrom会阻塞,即使收到端口不可达消息,也会阻塞。但是经过 方法1 和 方法2后,recvfrom会返回,返回值是-1,然后 判断errno是否是ECONNREFUSED来判断是否收到端口不可达消息。
#include <stdio.h> #include <netinet/in.h> #include <sys/socket.h> #include <string.h> #include <errno.h> unsigned char revc_buf[1024]; int main() { int fd,ret,recv_len,size=1024; struct sockaddr_in server_addr,addr; int val = 1; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("10.1.1.8"); server_addr.sin_port = htons(77); fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if(fd < 0) { perror("socket fail "); return -1; } printf("socket sucess "); //方法1 #if 1 setsockopt(fd, IPPROTO_IP, IP_RECVERR , &val,sizeof(int)); if(sendto(fd, "nihao", strlen("nihao"), 0, (const struct sockaddr *)&(server_addr), sizeof(struct sockaddr_in))<0) { perror("sendto fail "); return -1; } printf("sendto sucess "); ret = recvfrom(fd, revc_buf, sizeof(revc_buf), 0, (struct sockaddr *)&addr, (int *)&size); if (ret == -1) { if (errno == ECONNREFUSED) { printf("Recv port unreachable "); } } //方法2 #elif 0 ret = connect(fd, (const struct sockaddr *) &(server_addr), sizeof (struct sockaddr_in)); if(ret < 0) { printf("connect fail "); return -1; } ret = send(fd, "ni hao", strlen("nihao"),0); if(ret < 0) { printf("write fail "); return -1; } ret = recvfrom(fd, revc_buf, sizeof(revc_buf), 0, (struct sockaddr *)&addr, (int *)&size); if (ret == -1) { if (errno == ECONNREFUSED) { printf("Recv port unreachable "); } } #endif getchar(); close(fd); return 0; }
方法一
setsockopt(fd, IPPROTO_IP, IP_RECVERR , &val,sizeof(int)); if(sendto(fd, "nihao", strlen("nihao"), 0, (const struct sockaddr *)&(server_addr), sizeof(struct sockaddr_in))<0) { perror("sendto fail "); return -1; } printf("sendto sucess "); ret = recvfrom(fd, revc_buf, sizeof(revc_buf), 0, (struct sockaddr *)&addr, (int *)&size); if (ret == -1) { if (errno == ECONNREFUSED) { printf("Recv port unreachable "); } }
root@ubuntu:~/c++# ./udp
socket sucess
Recv port unreachable
//方法2