經過一學期網絡課的學(zi)習,對Linux平臺下使用raw socket編程有一定的瞭解。下面我將結合實驗寫過的wireshark、ping、router和vpn程序使用socket的實例給大家分享我的經驗。
首先來看socket()系統調用的封裝函數:
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
第一個參數domain常用的參數有:AF_UNIX(通常用於IPC)、AF_INET(IPv4)、AF_INET6(IPv6)、AF_PACKET(底層的和硬件相關的包,如以太網幀)。本文主要使用AF_PACKET、和AF_INET。
第二個參數type指定一種socket的類型,常用的有SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、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_INET和IPPROTO_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的文件描述符,第二個參數是用於接收的緩衝區,第三個參數爲緩衝區大小。後面的參數通常不使用,設爲0或NULL,詳情請見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; }
上面的函數設置了ICMP的seq和cksum。
發送以太網幀更加複雜,需要設置以太網幀的源和目的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; }