接收UDP数据包时是否可以读取TTL IP头字段?

Zhe*_*ren 4 c unix sockets

我正在使用 UDP 套接字发送数据包,我想检查接收数据包的 IP 标头中的 TTL 字段。是否可以?

我注意到 IP_HDRINCL sockoption 但它似乎只适用于 RAW 套接字。

Mec*_*cki 6

iYou 可以使用该recvmsg()界面获取该信息。首先,您需要告诉系统您要访问此信息:

int yes = 1;
setsockopt(soc, IPPROTO_IP, IP_RECVTTL, &yes, sizeof(yes));
Run Code Online (Sandbox Code Playgroud)

然后准备接收缓冲区:

// Note that IP packets can be fragmented and 
// thus larger than the MTU. In theory they can 
// be up to UINT16_MAX bytes long!
const size_t largestPacketExpected = 1500;
uint8_t buffer[largestPacketExpected];
struct iovec iov[1] = { { buffer, sizeof(buffer) } };
Run Code Online (Sandbox Code Playgroud)

如果您还想知道数据包的来源(使用recvfrom()而不是recv()),您还需要存储该地址:

// sockaddr_storage is big enough for any socket address your system
// supports, like sockaddr_in or sockaddr_in6, etc.
struct sockaddr_storage srcAddress;
Run Code Online (Sandbox Code Playgroud)

最后,您需要存储控制数据。每个控制数据项都有一个固定大小的标头 ( struct cmsghdr),在大多数系统上都是 12 字节,后跟有效载荷数据,其大小和解释取决于控制项的类型。在您的情况下,有效负载数据只是一个字节,即 TTL 值。但是,必须考虑一些对齐要求,因此您不能只保留 13 个字节,实际上在大多数系统上您的缓冲区需要更大,这就是系统为此提供一个方便的宏的原因:

uint8_t ctrlDataBuffer[CMSG_SPACE(sizeof(uint8_t))];
Run Code Online (Sandbox Code Playgroud)

如果您想检索多个控制数据项,您可以这样定义缓冲区:

uint8_t ctrlDataBuffer[
    CMSG_SPACE(x) 
    + CMSG_SPACE(y) 
    + CMSG_SPACE(z) 
];
Run Code Online (Sandbox Code Playgroud)

随着xyz作为有效载荷数据的大小返回。没有任何额外有效载荷数据的普通标头的大小由 返回CMSG_SPACE(0),它应该等于sizeof(struct cmsghdr)。但在你的情况下,有效载荷数据只是一个字节。

现在你需要把所有这些放在一起struct msghdr

struct msghdr hdr = {
    .msg_name = &srcAddress,
    .msg_namelen = sizeof(srcAddress),
    .msg_iov = iov,
    .msg_iovlen = 1,
    .msg_control = ctrlDataBuffer,
    .msg_controllen = sizeof(ctrlDataBuffer)
};
Run Code Online (Sandbox Code Playgroud)

请注意,您可以将所有您不感兴趣的字段设置为NULL(pointers) 或0(lengths)。如果您愿意,您可以仅检索源地址或仅检索数据包有效载荷或仅检索控制数据以及这三者的任意组合。

最后你可以从套接字读取:

ssize_t bytesReceived = recvmsg(soc, &hdr, 0);
Run Code Online (Sandbox Code Playgroud)

返回值就像 for recv(),-1 表示错误,0 表示另一端已关闭流(但这仅在 TCP 的情况下才有可能,并且您无法检索 TCP 套接字的 TTL),否则您将获得写入的字节数buffer.

怎么办srcAddress

if (srcAddress.ss_family == AF_INET) {
    struct sockaddr_in * saV4 = (struct sockaddr_in *)&scrAddress;
    // ...

} else if (srcAddress.ss_family == AF_INET6) {
    struct sockaddr_in6 * saV6 = (struct sockaddr_in6 *)&scrAddress;
    // ...

} // and so on
Run Code Online (Sandbox Code Playgroud)

好的,但是现在控制数据呢?您需要按如下所示进行处理:

int ttl = -1;
struct cmsghdr * cmsg = CMSG_FIRSTHDR(&hdr); 
for (; cmsg; cmsg = CMSG_NXTHDR(&hdr, cmsg)) {
    if (cmsg->cmsg_level == IPPROTO_IP
        && cmsg->cmsg_type == IP_RECVTTL
    ) {
        uint8_t * ttlPtr = (uint8_t *)CMSG_DATA(cmsg);
        ttl = *ttlPtr;
        break;
    }
}
// ttl is now either the real ttl or -1 if something went wrong
Run Code Online (Sandbox Code Playgroud)

CMSG_DATA()宏为您提供了一个正确对齐的指向实际控制数据有效负载的指针。同样,可能会有内存需求的填充,所以永远不要尝试直接访问数据。

与使用原始套接字相比,此方法的优点是:

  • 此代码不需要 root 权限。
  • sendmsg() 比原始套接字更便携。
  • 该套接字是一个普通的 UDP 套接字,其行为与任何其他 UDP 套接字一样。

有关您可以通过这种方式获得哪些其他信息的更多信息,您需要查看操作系统的 API 文档(例如 的手册页ip)。例如,这里有一个指向 [OpenBSD 手册页][1] 的链接。请注意,您还可以获得有关其他“级别”(例如 SOL_SOCKET)的信息,该信息记录在该级别的手册页中。

哦,如果你想知道,CMSG_LEN()是相似CMSG_SPACE()但不相同。CMSG_LEN(x)返回有效负载大小为 的控制数据实际使用的实际字节数x,而CMSG_SPACE(x)返回控制数据实际使用的实际字节数,其有效负载大小x 包括在有效负载数据之后正确对齐下一个控制数据所需的任何填充标题。因此,在为多个控制数据项保留存储空间时,您必须始终使用CMSG_SPACE()! 您仅CMSG_LEN()用于在您自己创建此类结构的情况下设置cmsg_len字段struct cmsghdr(例如,当使用sendmsg()which 也存在时)。

还有最后一件重要的事情要知道:如果你不小心把它ctrlDataBuffer弄得太小了,并不是说你根本不会得到任何控制数据或遇到错误,控制数据就会被截断。这种截断由一个标志指示(hdr输入时忽略标志字段,但它可能包含输出标志):

// After recvmsg()...
if (hdr.msg_flags & MSG_CTRUNC) {
    // Control data buffer was too small to make all data fit!
}
Run Code Online (Sandbox Code Playgroud)

如果您愿意,如果您选择的数据缓冲区太小,您可以获得相同的行为。只需查看此代码:

ssize_t bytesReceived = recvmsg(soc, &hdr, MSG_TRUNC);
if (hdr.msg_flags & MSG_TRUNC) {
    // The data buffer was too small, data has been read but it
    // was truncated. bytesReceived does *NOT* contain the amount of
    // bytes read but the amount of bytes that would have been read if
    // the data buffer had been of sufficient size!
}
Run Code Online (Sandbox Code Playgroud)

当然,在销毁数据包后知道正确的大小可能不是很有用。但是你可以这样做:

ssize_t bytesReceived = recvmsg(soc, &hdr, MSG_TRUNC | MSG_PEEK);
Run Code Online (Sandbox Code Playgroud)

这样,数据会重新存储在套接字缓冲区中,因此您可以再次读取它,因为您知道了所需的缓冲区大小。但是,类似的东西不适用于控制数据。您需要提前知道正确的控制数据大小,或者您需要编写一些试错代码,例如在循环中增加控制数据缓冲区,直到MSG_CTRUNC不再设置。通常一旦找到合适的大小,您就会记住它,因为对于给定的套接字,控制数据的数量通常是恒定的,除非您进行setsockopt()调用会改变它。默认情况下,UDP 套接字根本不返回控制数据,除非您已请求某些内容。