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

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

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

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

 int yes = 1; setsockopt(soc, IPPROTO_IP, IP_RECVTTL, &yes, sizeof(yes)); 

然后准备接收缓冲区:

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

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

 // sockaddr_storage is big enough for any socket address your system // supports, like sockaddr_in or sockaddr_in6, etc. struct sockaddr_storage sourceAddress; 

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

 uint8_t ctrlDataBuffer[CMSG_SPACE(sizeof(uint8_t))]; 

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

 uint8_t ctrlDataBuffer[ CMSG_SPACE(x) + CMSG_SPACE(y) + CMSG_SPACE(z) ]; 

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

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

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

 ssize_t bytesReceived = recvmsg(soc, &hdr, 0); 

返回值与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 

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

 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 

CMSG_DATA()宏为您提供指向实际控制数据有效负载的正确对齐指针。 同样,可能存在填充内存消耗要求,因此永远不要尝试直接访问数据。

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

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

有关您可以通过哪种方式获取的其他信息的更多信息,您需要查看操作系统的API文档(例如ip的手册页)。 以下是OpenBSD手册页的链接。 请注意,您还可以获取有关该级别手册页上记录的其他“级别”(例如SOL_SOCKET)的信息。

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

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

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

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

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

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

 ssize_t bytesReceived = recvmsg(soc, &hdr, MSG_TRUNC | MSG_PEEK); 

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

当您使用UDP套接字时,所有标头都将被删除(解封装),因此您将无法获取TTL字段值或IP标头的任何其他字段,但如果您有兴趣获取或设置它,使用原始套接字并构建标头,使用原始套接字将标头传递给您的应用程序,包括您构建的标头(IP +传输)层标头。