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;
}


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