調試過網絡程序的人大多使用過tcpdump
,那你知道tcpdump
是如何工做的嗎? tcpdump
這類工具也被稱爲Sniffer
,它能夠在不影響應用程序正常報文的狀況下,將流經網卡的報文複製一份給Sniffer
,而後通過加工過濾,最後呈現給用戶。html
本文不分析tcpdump
的具體實現,而只是借tcpdump
來揭示一些網絡編程中一個大多數人都容易忽略的一個主題:Socket
參數對用戶接收報文的影響...linux
相信全部接觸過Socket
編程的人都應該認識下面這個API
git
#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
所屬的地址簇
或者協議簇
,取值以AF
或者PF
開頭定義在(include\linux\socket.h
),實際使用中並無區別(有兩個不一樣的名字只是由於是歷史上的設計緣由)。最經常使用的取值有AF_INET
,AF_PACKET
,AF_UNIX
等。AF_UNIX
用於主機內部進程間通訊,本文暫且不談。AF_INET
與AF_PACKET
的區別在於使用前者只能看到IP
層以上的東西,然後者能夠看到鏈路層的信息。 less
什麼意思呢? 爲了說明這個問題,咱們須要知道網絡報文的分類。以下圖所示:Ethernet II
幀是應用最爲普遍的幀類型(固然也有像PPP
這樣的其餘鏈路幀類型)。Ethernet II
幀內部,又可大體分爲IP
報文和其餘報文。咱們熟悉的TCP
或者UDP
報文都屬於IP
報文。socket
AF_INET
是與IP
報文對應的,而AF_PACKET
則是與Ethernet II
報文對應的。AF_INET
建立的套接字稱爲inet socket
,而AF_PACKET
建立的套接字稱爲packet socket
tcp
第一個參數family
會影響第二個參數socket_type
和第三個參數protocol
取值範圍ide
第二個參數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
只能收發一種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)
對於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
也由應用程序本身提供。
inet socket
的控制範圍是IP
報文,而packet socket
的控制範圍擴大到了以太層報文。
對於inet socket
, 第二個參數socket_type
只能選擇SOCK_DGRAM
、SOCK_RAW
或者SOCK_PACKET
, protocol
則表示支持的網絡層的協議類型。
對以太幀來講,不一樣的網絡層協議類型(好比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
的時候,就已經能夠知道這是屬於哪個套接字的數據了。
對於AF_PACKET
來講,一個報文能夠這樣分解:
packet = Ethernet Header + Payload
而SOCK_DGRAM
和SOCK_RAW
的區別就在於,在接收方向,使用SOCK_DGRAM
套接字的應用程序收到的報文已經去除了Ethernet Header
,而SOCK_RAW
套接字則會保留。
回到本文最初的問題,tcpdump
是如何完成嗅探工做的呢? 沒錯!它正是使用的packet socket
:
tcpdump
做爲sniffer
,它不能影響正常的報文收發,所以它須要單獨的protocol handler
,這樣內核接收的報文會複製一份後,交給tcpdump
tcpdump
不止能抓取IP
報文, 它還能夠抓起鏈路層信息或者其餘一些非IP
報文。difference-between-pf-inet-sockets-and-pf-packet
data-link-access-and-zero-copy
raw-socket-in-linux
raw-sockets-c-code-linux