[外鏈圖片轉存失敗(img-jGyWSBRz-1569142979995)(https://s2.ax1x.com/2019/08/17/muuRUg.png)]
調試過網絡程序的人大多使用過tcpdump
,但你知道tcpdump
是如何工作的嗎?
tcpdump
這類工具也被稱爲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