rawsocket 使用小結

經過一學期網絡課的學(zi)習,對Linux平臺下使用raw socket編程有一定的瞭解。下面我將結合實驗寫過的wiresharkpingroutervpn程序使用socket的實例給大家分享我的經驗。


首先來看socket()系統調用的封裝函數:

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

第一個參數domain常用的參數有:AF_UNIX(通常用於IPC)、AF_INETIPv4)、AF_INET6IPv6)、AF_PACKET(底層的和硬件相關的包,如以太網幀)。本文主要使用AF_PACKET、和AF_INET

第二個參數type指定一種socket的類型,常用的有SOCK_STREAMTCP)、SOCK_DGRAMUDP)、SOCK_RAW(需要root權限或者CAP_NET_RAW的能力)。本文不會使用SOCK_STREAM參數。當然,以上所說的將不在本文使用的參數實際上是更加常用的,而raw socket反而不常用。網上的資料較多,可以自行檢索。

第三個參數protocol指定一種將要使用的協議。可以查看/etc/protocols文件。下面使用時在介紹。


使用什麼參數打開socket通常要結合實際需求。

在寫wireshark時,我們希望能夠聽到所有的包,並且能夠對所有字段進行解析,那麼,就應該接收所有的底層包:

int fd;
if ((fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1)
    errExit("open socket");

使用是AF_PACKET參數,socket類型爲SOCK_RAW,協議使用ETH_P_ALL,注意這個協議需要進行主機和網絡字節序的轉換,並且將不會過濾任何包。這樣就可以收到所有的包了,包括主機發出去的包。這個參數也可以用來發送底層的包,發送底層的包是最麻煩的。

與這個socket實例類似的一個是,vpn程序需要聽所有的IP包,我們使用如下的選項:

if ((recvfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP))) == -1) {
    errExit("open recvfd socket");
}

可見只更改了protocol參數,這個參數將只接收主機收到的IP包。當然其實鏈路層在vpn中是不需要的,guage曾將AF_PACKET改爲AF_INET,但是發現不能收到任何包,我沒有測試過。


如果想要發送RAW IP的包,則使用AF_INETIPPROTO_RAW參數,比如ping程序必須通過RAW IP包來發送ICMP

socket_fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (socket_fd == -1) {
    errExit("socket");
}

另一個需要使用RAW IP的是VPN程序,一個VPN server收到另一個VPN server發來的包時,去除封裝的包頭,取出原IP曾的數據,直接通過RAW IP發送:

if ((distributefd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) == -1) {
    errExit("open distributefd socket");
}

發送RAWIP還是比較麻煩的,後面將會講解發生的參數設置。


最後介紹的是一種常用的socket——數據報socket(即使用UDP協議的socket)。這個socket可用在VPN程序中,可以直接利用內核路由來發送包,比較簡單:

if ((forwardfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
    errExit("open forwardfd socket");
}


接下來介紹如何接收包:

由於socket是要麼取一幀要麼就沒有的,所以接收包需要一個足夠大的緩衝區,比如設置爲以太網幀大小,宏定義ETH_FRAME_LEN

#include <linux/if_ether.h>
#define ETH_FRAME_LEN   1514      /* Max. octets in frame sans FCS */

定義一個數組:

uint8_t recvbuf[ETH_FRAME_LEN];


下面爲收包時用的系統調用。

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
               struct sockaddr *src_addr, socklen_t *addrlen);

第一個參數指定要接受的socket的文件描述符,第二個參數是用於接收的緩衝區,第三個參數爲緩衝區大小。後面的參數通常不使用,設爲0NULL,詳情請見manpage

即通常寫爲這種形式:

recvSize = recvfrom(socket_fd, recv_packet, packet_size, 0, NULL, NULL);

後面的參數如果不使用,則可以使用通用IO模型read()系統調用:

num_recv = read(recvfd, recvbuf, ETH_FRAME_LEN);


最後介紹如何發送包,發送包與socket打開參數有較大的關係。

先從簡單的數據報socket說起。

數據報socket通常需要綁定到一個端口,使用memset函數初始化struct sockaddr_in結構體,這是因爲如果結構體內的數值設爲0則代表通配符,除了socket家族需要設置。

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(vpnport);
if (bind(forwardfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
    errExit("bind");
}

綁定到此端口後,發往此端口的包內核將分發給這個socket文件描述符,如果內核發現沒有此目的端口的socket打開,就會回覆一個ICMP type 3 code 3的報文通知目的端口不可達。通過此socket發送的包的源端口號爲綁定的端口號。

下面代碼演示了數據報socket的發包設置。先清零結構體,設置SOCKET家族、目的端口和目的地址。

struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_addr = rule->dest;
dest_addr.sin_port = htons(vpnport);
ssize_t ret = sendto(forwardfd, recvbuf, (size_t)num_recv, 0, (void*)&dest_addr, sizeof(dest_addr));


手動發送RAW IP麻煩之處在於要自己設置IP層的內容,如IP地址、checksum等等。我們以ping程序發送ICMP爲例,當然RAW IP和這個有些不太一樣。

// pack icmp
size_t pack(u_int16_t seq) {
    struct icmp *icmp;
    struct timeval *tv;
    icmp = (void *)send_packet;
    icmp->icmp_seq = htons((uint16_t)seq);
    tv = (void *)icmp->icmp_data;
    gettimeofday(tv, NULL);
    icmp->icmp_cksum = 0;
    icmp->icmp_cksum = (uint16_t)checksum((u_int16_t*)icmp, (int)(packet_size) + 8);
    return (size_t)packet_size + 8;
}

上面的函數設置了ICMPseqcksum


發送以太網幀更加複雜,需要設置以太網幀的源和目的MAC地址,設置上層協議,設置硬件接口的編號值(Linux可在/sys/class/net/*/ifindex文件中獲得,*通配接口名,也可以使用iplink,每個接口開頭的數字即是編號)。設置目的地的MAC地址。然後使用sendto()發送。

int sendpacket(int fd, const hostinfo &host, macaddr_t dest_mac, void *buf, size_t size, uint16_t pro) {
    struct ethhdr *eh = (struct ethhdr *)buf;
    memcpy(eh->h_dest, (void*)&dest_mac, ETH_ALEN);
    memcpy(eh->h_source, (void*)&host.mac, ETH_ALEN);
    eh->h_proto = htons(pro);
    struct sockaddr_ll socket_address;
    memset(&socket_address, 0, sizeof(socket_address));
    socket_address.sll_ifindex = host.ifindex;
    socket_address.sll_halen = ETH_ALEN;
    memcpy((void*)socket_address.sll_addr,(void*)&dest_mac,ETH_ALEN);
    if (sendto(fd, buf, size + ETH_HLEN, 0, (struct sockaddr*)&socket_address, sizeof(socket_address)) == -1) {
        return -1;
    }
    return 0;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章