调试过网络程序的人大多使用过tcpdump
,那你知道tcpdump
是如何工作的吗? tcpdump
这类工具也被称为Sniffer
,它可以在不影响应用程序正常报文的情况下,将流经网卡的报文复制一份给Sniffer
,然后经过加工过滤,最后呈现给用户。
本文不分析tcpdump
的具体实现,而只是借tcpdump
来揭示一些网络编程中一个大多数人都容易忽略的一个主题:Socket
参数对用户接收报文的影响...
相信所有接触过Socket
编程的人都应该认识下面这个API
#include <sys/socket.h>
sockfd = socket(int socket_family, int socket_type, int protocol);
没错,它基本是socket
编程的第一步,创建一个套接字。他有三个参数,不过又有多少人真的去了解这些参数的意义呢? 对于TCP
或者UDP
应用的开发者来说,他们可以很容易地从互联网上找(抄)到这样的例子:
/* 创建TCP socket*/
sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* 创建UDP socket*/
sockfd = socket(AF_INET, SOCK_DGRAM, 0)
为什么第一个参数要使用AF_INET
,为什么第二个参数要使用SOCK_STREAM
或者SOCK_DGRAM
,为什么第三个参数要填0
?
socket_family
第一个参数表示创建的socket
所属的地址簇
或者协议簇
,取值以AF
或者PF
开头定义在(include\linux\socket.h
),实际使用中并没有区别(有两个不同的名字只是因为是历史上的设计原因)。最常用的取值有AF_INET
,AF_PACKET
,AF_UNIX
等。AF_UNIX
用于主机内部进程间通信,本文暂且不谈。AF_INET
与AF_PACKET
的区别在于使用前者只能看到IP
层以上的东西,而后者可以看到链路层的信息。
什么意思呢? 为了说明这个问题,我们需要知道网络报文的分类。如下图所示:Ethernet II
帧是应用最为广泛的帧类型(当然也有像PPP
这样的其他链路帧类型)。Ethernet II
帧内部,又可大致分为IP
报文和其他报文。我们熟悉的TCP
或者UDP
报文都属于IP
报文。
AF_INET
是与IP
报文对应的,而AF_PACKET
则是与Ethernet II
报文对应的。AF_INET
创建的套接字称为inet socket
,而AF_PACKET
创建的套接字称为packet socket
socket_type & protocol
第一个参数family
会影响第二个参数socket_type
和第三个参数protocol
取值范围
第二个参数socket_type
表示套接字类型。它的取值不多,常见的就以下三种
enum sock_type {
SOCK_STREAM = 1, /* stream (connection) socket */
SOCK_DGRAM = 2, /* datagram (conn.less) socket */
SOCK_RAW = 3, /* raw socket */
};
第三个参数protocol
表示套接字上报文的协议。
对于AF_INET
地址簇,protocol
的取值范围是如 IPPROTO_TCP IPPROTO_UDP IPPROTO_ICMP 这样的IP
报文协议类型,或者IPPROTO_IP = 0 这个特殊值
对于AF_PACKET
地址簇,protocol
的取值范围是 ETH_P_IP ETH_P_ARP这样的以太帧协议类型。
inet socket的协议开关表
每一个inet socket
只能收发一种IP
协议类型的报文,这是在套接字创建的时候就决定的(protocol
参数),比如TCP
套接字是不能收发UDP
报文的,反之也是一样。并且,protocol
的值还受到socket_type
的限制,不匹配的取值会导致套接字创建操作会返回失败。
/* 错误取值,返回失败 */
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_TCP);
内核通过协议开关表
记录了哪些哪些取值是有效的,inet
在初始化时会将支持的协议注册在协议开关表
中的以socket_type
为KEY
的链表上:
而在创建套接字时,inet_create
会在协议开关表中根据socket_type
和protocol
进行匹配
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
err = 0;
/* Check the non-wild match. */
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT;
}
IPPROTO_IP
的值为0
, 在用户使用0
作为创建套接字的第三个参数时,会匹配到该链表上的第一个协议,这正是创建TCP
或者UDP
套接字时,第三个参数可以为0
的原因, 0
表示由内核自动选择。··
/* 创建TCP socket*/
sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* 创建UDP socket*/
sockfd = socket(AF_INET, SOCK_DGRAM, 0)
raw inet socket
对于inet socket
来说,一个TCP
报文可以这样分解:
packet = IP Header + TCP Header + Payload
如果我们是使用SOCK_STREAM
创建的TCP
套接字,应用程序在通过send
发送数据时,只需要提供Payload
就行了,而IP Header
和TCP Header
则由内核组装完成。接收方向,应用程序通过recv
也只能收到payload
而RAW
套接字则为应用提供了更底层的控制能力
int s = socket (AF_INET, SOCK_RAW, IPPROTO_TCP);
使用上面的接口可以创建一个更原始的TCP
套接字,当我们使用这个套接字发送数据时,需要提供Payload
和TCP Header
,而IP Header
依然由内核协议栈自动组装。
如果希望手动组装IP Header
,有两个方法:
第一种是protocol
使用IPPROTO_RAW
int s = socket (AF_INET, SOCK_RAW, IPPROTO_RAW);
第二种是置位IP_HDRINCL
的套接字选项。
int s = socket (AF_INET, SOCK_RAW, IPPROTO_TCP);
int one = 1;
const int *val = &one;
if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, val, sizeof (one)) < 0)
{
printf ("Error setting IP_HDRINCL. Error number : %d . Error message : %s \n" , errno , strerror(errno));
exit(0);
}
以上两种方法都是告诉内核,IP Header
也由应用程序自己提供。
packet socket
inet socket
的控制范围是IP
报文,而packet socket
的控制范围扩大到了以太层报文。
对于inet socket
, 第二个参数socket_type
只能选择SOCK_DGRAM
、SOCK_RAW
或者SOCK_PACKET
, protocol
则表示支持的网络层的协议类型。
Protocol Handler
对以太帧来说,不同的网络层协议类型(比如IP
ARP
PPPoE
)有不同的接收处理函数。在内核中,这就是Protocol Handler
。
内核中的Protocl Handler
是这样组织的注
:
注
该patch将Protocl Handler
在dev
下增加了ptype_all
链表和ptype_base
链表
无论网卡是否采用NAPI
,内核最终都会调用到__netfi_receive_skb
接收报文,这个函数会遍历ptype_all
链表上已注册的handler
,然后再遍历ptype_base
特定协议链上的所有已注册的handler
handler
的注册是通过dev_add_pack
完成的,如果没有指定协议(ETH_P_ALL
),该handler
就会注册在ptype_all
上(tcpdump
默认就会注册在这里),否则根据协议注册在ptype_base
的某条链表上。
在报文接收过程中,同一个skb
会被deliver_skb
到多个handler
(至少将ptype_all
链表上的handler
走一遍)。
内核启动时,inet
会注册一个handler
,它支持IP
协议,所有AF_INET
套接字实际上是共用这样一个handler
,对应的接收函数是ip_rcv
,区分是哪一个套接字的报文是之后的工作。
/* net/ipv4/af_inet.c */
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
};
static int __init inet_init(void)
{
// code omitted
dev_add_pack(&ip_packet_type);
// code omitted
}
而对于AF_PACKET
,handler
是在packet_create
中单独注册的,也就是说,每个AF_PACKET
套接字拥有独立的handler
static int packet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
// code omitted
po->prot_hook.func = packet_rcv;
// code omitted
register_prot_hook(sk); // 这里面去 dev_add_pack
}
单独的handler
,使得在接收函数packet_rcv
的时候,就已经可以知道这是属于哪一个套接字的数据了。
raw packet socket
对于AF_PACKET
来说,一个报文可以这样分解:
packet = Ethernet Header + Payload
而SOCK_DGRAM
和SOCK_RAW
的区别就在于,在接收方向,使用SOCK_DGRAM
套接字的应用程序收到的报文已经去除了Ethernet Header
,而SOCK_RAW
套接字则会保留。
packet socket 与 tcpdump
回到本文最初的问题,tcpdump
是如何完成嗅探工作的呢? 没错!它正是使用的packet socket
:
-
tcpdump
作为sniffer
,它不能影响正常的报文收发,因此它需要单独的protocol handler
,这样内核接收的报文会复制一份后,交给tcpdump
-
tcpdump
不止能抓取IP
报文, 它还可以抓起链路层信息或者其他一些非IP
报文。
REF
difference-between-pf-inet-sockets-and-pf-packet
data-link-access-and-zero-copy
raw-socket-in-linux
raw-sockets-c-code-linux
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。