inet socket 與 packet socket

details

調試過網絡程序的人大多使用過tcpdump,那你知道tcpdump是如何工做的嗎?
tcpdump這類工具也被稱爲Sniffer,它能夠在不影響應用程序正常報文的狀況下,將流經網卡的報文複製一份給Sniffer,而後通過加工過濾,最後呈現給用戶。html

本文不分析tcpdump的具體實現,而只是借tcpdump來揭示一些網絡編程中一個大多數人都容易忽略的一個主題:Socket參數對用戶接收報文的影響...linux

相信全部接觸過Socket編程的人都應該認識下面這個APIgit

#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_INETAF_PACKET的區別在於使用前者只能看到IP層以上的東西,然後者能夠看到鏈路層的信息less

什麼意思呢? 爲了說明這個問題,咱們須要知道網絡報文的分類。以下圖所示:Ethernet II幀是應用最爲普遍的幀類型(固然也有像PPP這樣的其餘鏈路幀類型)。Ethernet II幀內部,又可大體分爲IP報文和其餘報文。咱們熟悉的TCP或者UDP報文都屬於IP報文。socket

packet_class

AF_INET是與IP報文對應的,而AF_PACKET則是與Ethernet II報文對應的。AF_INET建立的套接字稱爲inet socket,而AF_PACKET建立的套接字稱爲packet sockettcp

packet_create

socket_type & protocol

第一個參數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的協議開關表

每個inet socket只能收發一種IP協議類型的報文,這是在套接字建立的時候就決定的(protocol參數),好比TCP套接字是不能收發UDP報文的,反之也是同樣。而且,protocol的值還受到socket_type的限制,不匹配的取值會致使套接字建立操做會返回失敗。

/* 錯誤取值,返回失敗 */
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_TCP);

內核經過協議開關表記錄了哪些哪些取值是有效的,inet在初始化時會將支持的協議註冊在協議開關表中的以socket_typeKEY的鏈表上:

inetsw

而在建立套接字時,inet_create會在協議開關表中根據socket_typeprotocol進行匹配

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 HeaderTCP Header則由內核組裝完成。接收方向,應用程序經過recv也只能收到payload

RAW套接字則爲應用提供了更底層的控制能力

int s = socket (AF_INET, SOCK_RAW, IPPROTO_TCP);

使用上面的接口能夠建立一個更原始的TCP套接字,當咱們使用這個套接字發送數據時,須要提供PayloadTCP 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 = &amp;one;
if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, val, sizeof (one)) &lt; 0)
{
    printf (&quot;Error setting IP_HDRINCL. Error number : %d . Error message : %s \n&quot; , errno , strerror(errno));
    exit(0);
}

以上兩種方法都是告訴內核,IP Header也由應用程序本身提供。

packet socket

inet socket的控制範圍是IP報文,而packet socket的控制範圍擴大到了以太層報文

對於inet socket, 第二個參數socket_type只能選擇SOCK_DGRAMSOCK_RAW或者SOCK_PACKET, protocol則表示支持的網絡層的協議類型。

Protocol Handler

對以太幀來講,不一樣的網絡層協議類型(好比IP ARP PPPoE)有不一樣的接收處理函數。在內核中,這就是Protocol Handler

內核中的Protocl Handler是這樣組織的

protocol handler

patchProtocl Handlerdev下增長了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_PACKEThandler是在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_DGRAMSOCK_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

相關文章
相關標籤/搜索