这个问题其实是我几个月前碰到,只是那时好像还在回忆着什么,心系上海,还没有完全适应这个新环境,加上这个问题也不是什么太深奥的问题,觉得太简单了,就搁置了。今天周末闲来无事就顺便写来来了。加上深圳经常下雨,越来越喜欢了。
本文没什么深度,仅为记录,以及阐述一个“看文档学习原理->猜测并自行实现->对比标准实现确认”的方法。
问题是这样的:
在Linux上如果使用tcpdump去抓取lo口的数据包,你只能抓到一遍,而不是两遍,按常理来讲,数据包在outgoing路径上和incoming路径上都会被抓到,对于lo口而言,两个路径都要经过它本身,为什么只能抓到一个呢?难道lo口做了什么特殊的处理吗?
依然是传统的三招破解:看文档学习原理,猜测实现,确认实现
1.看文档学习原理
最直接的就是先去man tcpdump,前面的参数详解部分可以不看,只看最后面的NOTES或者BUGS部分就好,其BUGS部分有一个细节正中这个话题:
BUGS
...
On Linux systems with 2.0[.x] kernels:
packets on the loopback device will be seen twice;
packet filtering cannot be done in the kernel, so that all packets must be copied from the kernel in order to be filtered in user mode;
all of a packet, not just the part that‘s within the snapshot length, will be copied from the kernel (the 2.0[.x] packet capture mechanism, if asked to copy only part of a packet to userland, will not report the true
length of the packet; this would cause most IP packets to get an error from tcpdump);
capturing on some PPP devices won‘t work correctly.
看样子已经说的很清楚了,“packets on the loopback device will be seen twice”描述了问题,“packet filtering cannot be done in the kernel, so that all packets must be copied from the kernel in order to be filtered in user mode”假装说明了原因。
2.猜测实现
如果在Linux内核内部对lo进行特殊的抓包过滤,那就太不靠谱了,如果有这么一个需求就这么过滤一次的话,内核会遍布if-else...switch-case...搞不好还要引入正儿八经的多态。内核只要提供足够的信息,用户态自己过滤即可。在看代码之前,做出以上猜测是合理的。
3.确认实现
最直接的先看内核代码的packet_rcv函数,这个函数是tcpdump抓包的入口函数(当然如果经过了PF_RING的优化,就不是这样了,但简单来讲,大多数就是这样):
static int packet_rcv(struct sk_buff *skb, struct device *dev, struct packet_type *pt) { struct sock *sk; struct sockaddr_ll *sll = (struct sockaddr_ll*)skb->cb; /* * When we registered the protocol we saved the socket in the data * field for just this event. */ sk = (struct sock *) pt->data; // 注意,PACKET_LOOPBACK只是和multicast相关的,与我们讨论的无关 if (skb->pkt_type == PACKET_LOOPBACK) { kfree_skb(skb); return 0; } skb->dev = dev; sll->sll_family = AF_PACKET; sll->sll_hatype = dev->type; sll->sll_protocol = skb->protocol; sll->sll_pkttype = skb->pkt_type; sll->sll_ifindex = dev->ifindex; sll->sll_halen = 0; ... }
然后看一下发送路径的处理:
void dev_queue_xmit_nit(struct sk_buff *skb, struct device *dev) { ... skb2->pkt_type = PACKET_OUTGOING; ptype->func(skb2, skb->dev, ptype); ... }
看来,PACKET_OUTGOING是一个核心的标志,它可以告诉tcpdump数据包是从哪里路径发出的。最后,我们看一下pcap的数据包读取函数pcap_read_linux_mmap:
if (sll->sll_pkttype == PACKET_OUTGOING) { /* * Outgoing packet. * If this is from the loopback device, reject it; * we‘ll see the packet as an incoming packet as well, * and we don‘t want to see it twice. */ if (sll->sll_ifindex == handle->md.lo_ifindex) goto skip; /* * If the user only wants incoming packets, reject it. */ if (handle->direction == PCAP_D_IN) goto skip; }
直接看注释就好了。
只可惜,Linux 2.0的内核中,并没有提供这个PACKET_OUTGOING信息,抓上去的就是一个裸包,没有足够的信息,当然就无法区分两个方向了,这也是tcpdmp的manual中那个BUG的根源。这个BUG的解决也帮助我们理解了为什么lo口抓包只抓到一个方向的,而不是两遍。还让我们知道了,lo口抓包抓取的是incoming方向的数据包。