经过一学期网络课的学(zi)习,对Linux平台下使用raw socket编程有一定的了解。下面我将结合实验写过的wireshark、ping、router和程序使用socket的实例给大家分享我的经验。
首先来看socket()系统调用的封装函数:
#include#include 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实例类似的一个是,程序需要听所有的IP包,我们使用如下的选项:
if ((recvfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP))) == -1) { errExit("open recvfd socket"); }
可见只更改了protocol参数,这个参数将只接收主机收到的IP包。当然其实链路层在中是不需要的,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#define ETH_FRAME_LEN 1514 /* Max. octets in frame sans FCS */
定义一个数组:
uint8_t recvbuf[ETH_FRAME_LEN];
下面为收包时用的系统调用。
#include#include 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(port); 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(port); 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; }