linux udp raw socket

发布时间 2023-08-30 15:50:26作者: 秋来叶黄

tcp/udp网络通信与socket实际上是两个概念,不过因为我们平常使用tcp/udp,不可避免的使用socket,所以认为两者是同一个事物。

我们现在所说的或者最常用到的都是BSD版本的socket。socket是对tcp/udp等网络协议的封装,提供上层接口,供我们使用,可以编写程序在网络间传递数据。

tcp/ip是一种协议标准,规定了数据如何传输,socket相当于对这个标准的实现。

https://www.rfc-editor.org/rfc/rfc147.html

https://man7.org/linux/man-pages/man2/socket.2.html

raw socket 原生套接字

使用linux开发udp socket程序时,我们只需要接收发送数据,并不需要关心每个数据包的头如何组装、MTU大小是多少、IP如何分片,这都是底层socket实现的。如果需要对收发数据包的头进行操作,linux提供了原生套接字,可以获取到完整的数据包——完整的数据包包括以太网头->IP头->TCP/UDP头->负载数据。

TCP/IP层原生套接字发送数据

linux提供的原生套接字功能,分两部分,一部分是获取到TCP/IP层,会屏蔽上层的以太网层的头部信息,也会屏蔽当前IP层分片等信息,也就是获取到的数据是处理过的,并不是真正原始的数据包,不过当前数据包包含IP层的头和TCP/UDP层的头;但是发送数据又不会帮我们自动分片。另一部分是获取原始数据包,也就是网络上数据包是什么样,就会获取到什么样的数据,包括以太网层的头,也就是真正原始的数据包。

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <linux/ip.h>
#include <linux/udp.h>

#define PCKT_LEN 8192

unsigned short csum(unsigned short *buf, int nwords)
{
    unsigned long sum;
    for (sum = 0; nwords > 0; nwords--)
    {
        sum += *buf++;
    }
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    return (unsigned short) (~sum);
}

int main(int argc, char const *argv[])
{
    u_int16_t src_port, dst_port;
    u_int32_t src_addr, dst_addr;
    src_addr = inet_addr("192.168.10.111");
    dst_addr = inet_addr("192.168.10.112");
    src_port = atoi("1234");
    dst_port = atoi("1234");

    struct sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(dst_port);
    sin.sin_addr.s_addr = dst_addr;

    int sd;
    // 接收数据的缓冲区
    char buffer[PCKT_LEN];
    memset(buffer, 0, PCKT_LEN);
    // 数据包含ip头和udp头,所以使用linux提供的ip头和udp头的结构体映射数据
    struct iphdr *ip = (struct iphdr *) buffer;
    struct udphdr *udp = (struct udphdr *) (buffer + sizeof(struct iphdr));

    // 创建socket
    // AF_INET表示ipv4协议
    // SOCK_RAW表示原生socket
    // IPPROTO_UDP表示接收udp协议
    sd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
    if (sd < 0)
    {
        perror("socket() error");
        exit(-1);
    }

    int optval = 1;
    // 设置socket,由我们管理ip头和udp头
    // sd表示要设置的socket id
    // IPPROTO_IP表示设置的是IP层
    // IP_HDRINCL表示由用户层管理tcp/ip层的头
    // &optval 设置IP_HDRINCL的数值,这里1表示打开,传递的是设置数值的指针
    // sizeof(optval) 设置参数数值的大小,因为上面传递的是指针,这里就要给定大小,不然不知道设置的内容有多长
    if (setsockopt(sd, IPPROTO_IP, IP_HDRINCL, &optval, sizeof(optval)) < 0)
    {
        perror("setsockopt error");
        exit(-1);
    }

    // 设置ip的头
    ip->ihl = 5;
    ip->version = 4;
    ip->tos = 16;
    ip->id = htons(54321);
    ip->ttl = 64;
    ip->protocol = 17;
    // 设置ip头中源ip的地址,这里就可以随意修改,很多伪造数据包攻击(比如反射攻击)就是用到这种方法
    ip->saddr = src_addr;
    ip->daddr = dst_addr;

    // 设置udp头
    udp->source = htons(src_port);
    udp->dest = htons(dst_port);
    // 设置ip的check位,有ip协议得知,这里只需要校验头,不包括payload
    ip->check = csum((unsigned short *) buffer, sizeof(struct iphdr) + sizeof(struct udphdr));
    int sendbufflen = 0;
    // 从udp头之后,填充用户数据,这里才是我们使用普通 socket发送数据填充的地方
    unsigned short *mbuffer = buffer + sizeof(struct iphdr) + sizeof(struct udphdr);
    int datalen = 1000;
    for (int i = 0; i < datalen; i++)
    {
        *(mbuffer + i) = (unsigned short) i;
    }
    sendbufflen = sizeof(struct iphdr) + sizeof(struct udphdr) + datalen;
    // 设置ip和udp中记录数据包长度的字段。注意需要使用htons,把本地字节序转为网络字节序
    ip->tot_len = htons(sendbufflen);
    udp->len = htons(sizeof(struct udphdr) + datalen);
    // 发送数据
    if (sendto(sd, buffer, sendbufflen, 0, (struct sockaddr *) &sin, sizeof(sin)) < 0)
    {
        perror("sendto error");
        exit(-1);
    }

    close(sd);
    return 0;
}

这样我们就可以伪造一份网络数据包,并且发送到指定的服务器和端口。通过wireshark等抓包可以看到源ip地址并不是我们机器的地址,而是指定的地址。
https://man7.org/linux/man-pages/man3/setsockopt.3p.html

AF_INET与PF_INET

https://man7.org/linux/man-pages/man2/socket.2.html

有时候我们发现代码中有用AF_INET,也有用PF_INET,这两者有什么区别呢?实际上官方文档已经给了说明:

HISTORY         top

       POSIX.1-2001, 4.4BSD.

       socket() appeared in 4.2BSD.  It is generally portable to/from
       non-BSD systems supporting clones of the BSD socket layer
       (including System V variants).

       The manifest constants used under 4.x BSD for protocol families
       are PF_UNIX, PF_INET, and so on, while AF_UNIX, AF_INET, and so
       on are used for address families.  However, already the BSD man
       page promises: "The protocol family generally is the same as the
       address family", and subsequent standards use AF_* everywhere.

现在用的socket基本上都是有BSD版本迁移而来,当时设定根据协议簇和地址簇进行区分,但是最后没有实现,PF_*AF_*现在是相等的,所以用AF_INETPF_INET是一样的,不过建议使用AF_*

raw - Linux IPv4 raw sockets

IP_HDRINCL

设置了IP_HDRINCL后,有些ip头的数据系统是可以帮我们填充的:

┌───────────────────────────────────────────────────┐
│IP Header fields modified on sending by IP_HDRINCL │
├──────────────────────┬────────────────────────────┤
│IP Checksum           │ Always filled in           │
├──────────────────────┼────────────────────────────┤
│Source Address        │ Filled in when zero        │
├──────────────────────┼────────────────────────────┤
│Packet ID             │ Filled in when zero        │
├──────────────────────┼────────────────────────────┤
│Total Length          │ Always filled in           │
└──────────────────────┴────────────────────────────┘

其他设置ip头的方式

按照官方文档所说,从linux 2.2起,ip头所有的字段都可以通过ip socket设置进行修改,也就是原生socket只需用在新的协议或者用户层无法控制的协议,比如ICMP。

Starting with Linux 2.2, all IP header fields and options can be
set using IP socket options. This means raw sockets are usually
needed only for new protocols or protocols with no user interface
(like ICMP).

https://man7.org/linux/man-pages/man7/raw.7.html

sendto error: Message too long

如果使用了IP_HDRINCL,自己进行填充ip头,就会遇到这个问题,因为系统发送数据会被MTU限制(大部分是1500个字节),超过了需要分片,但是设置了IP_HDRINCL,系统就不会自动帮你分片了,所以需要手动分片。

有人说获取udp数据最大是65535,这句话本身是正确的,udp协议规则一个udp包最大是65535个字节,但是网络数据包受到MTU的限制,导致ip数据包并不会这么大,udp被ip数据包封装,所以也同样受到ip数据包大小的限制,这才有了IP分片(IP Fragement),这也是为什么有IP分片,而没有tcp或者udp分片。使用普通的socket通信,底层系统会自动分片,也会自动组装分片,所以接收和发送的最大长度可以是udp协议设定的65535个字节。

虽然官方文档说设置IP_MTU_DISCOVER可以控制对大于MTU的包分片,但是测试下来,针对UDP,并没有效果。

https://man7.org/linux/man-pages/man7/ip.7.html
https://man7.org/linux/man-pages/man7/raw.7.html

BUGS         top

       Transparent proxy extensions are not described.

       When the IP_HDRINCL option is set, datagrams will not be
       fragmented and are limited to the interface MTU.

       Setting the IP protocol for sending in sin_port got lost in Linux
       2.2.  The protocol that the socket was bound to or that was
       specified in the initial socket(2) call is always used.

TCP/IP层原生套接字接收数据

接收一个原生socket数据,并且修改部分信息,再发送给另一个ip

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <linux/ip.h>
#include <linux/udp.h>

#define PCKT_LEN 8192

unsigned short csum(unsigned short *buf, int nwords)
{
    unsigned long sum;
    for (sum = 0; nwords > 0; nwords--)
    {
        sum += *buf++;
    }
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    return (unsigned short) (~sum);
}

int main(int argc, char const *argv[])
{
    u_int16_t src_port, dst_port;
    u_int32_t src_addr, dst_addr;
    src_addr = inet_addr("192.168.10.111");
    dst_addr = inet_addr("192.168.10.112");
    src_port = atoi("1234");
    dst_port = atoi("1234");

    // 创建原生接收socket
    int recvsd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
    if (recvsd < 0)
    {
        perror("recvsd");
        return -1;
    }

    struct sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(dst_port);
    sin.sin_addr.s_addr = dst_addr;

    int sd;
    // 接收数据的缓冲区
    char buffer[PCKT_LEN];
    memset(buffer, 0, PCKT_LEN);

    // 创建socket
    // AF_INET表示ipv4协议
    // SOCK_RAW表示原生socket
    // IPPROTO_UDP表示接收udp协议
    sd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
    if (sd < 0)
    {
        perror("socket() error");
        exit(-1);
    }

    int optval = 1;
    // 设置socket,由我们管理ip头和udp头
    // sd表示要设置的socket id
    // IPPROTO_IP表示设置的是IP层
    // IP_HDRINCL表示由用户层管理tcp/ip层的头
    // &optval 设置IP_HDRINCL的数值,这里1表示打开,传递的是设置数值的指针
    // sizeof(optval) 设置参数数值的大小,因为上面传递的是指针,这里就要给定大小,不然不知道设置的内容有多长
    if (setsockopt(sd, IPPROTO_IP, IP_HDRINCL, &optval, sizeof(optval)) < 0)
    {
        perror("setsockopt error");
        exit(-1);
    }

    int recvlen = 0;
    // 接收数据
    // 这里把接收保存的地址设置为NULL,所以地址长度也指定为0,是因为这里使用不到,我们在下面sendto的时候指定了一个其他地址
    recvlen = recvfrom(recvsd, buffer, PCKT_LEN, 0, NULL, 0);
    if (recvlen <= 0)
    {
        perror("recv error");
        exit(-1);
    }

    // 数据包含ip头和udp头,所以使用linux提供的ip头和udp头的结构体映射数据
    struct iphdr *ip = (struct iphdr *) buffer;
    struct udphdr *udp = (struct udphdr *) (buffer + sizeof(struct iphdr));
    // 这里只做了部分修改,如果想修改其他的,可以任意修改对应的变量
    ip->saddr = src_addr;
    ip->daddr = dst_addr;

    // 设置ip的check位,有ip协议得知,这里只需要校验头,不包括payload
    ip->check = csum((unsigned short *) buffer, sizeof(struct iphdr) + sizeof(struct udphdr));

    // 发送数据
    if (sendto(sd, buffer, recvlen, 0, (struct sockaddr *) &sin, sizeof(sin)) < 0)
    {
        perror("sendto error");
        exit(-1);
    }

    close(sd);
    close(recvsd);
    return 0;
}

上面的代码我们会发现一个不一样的地方,就是没有bind,而可以直接接收数据,因为当前设定的原生socket是ip层,指定端口是tcp/udp层,所以原生socket不支持bind端口,即使绑定了,也不会生效。

但是我们可以通过bind绑定地址,也可以通过SO_BINDTODEVICE设定网卡名称。

https://man7.org/linux/man-pages/man7/raw.7.html