【问题标题】:Is it possible to read the TTL IP header field when receiving UDP packets?接收UDP数据包时是否可以读取TTL IP头字段?
【发布时间】:2013-04-16 10:37:52
【问题描述】:

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

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

【问题讨论】:

  • 在 Linux 上,我认为您可以通过设置 IP_RECVTTL 套接字选项并使用 recvmsg() 来获得它。
  • This answer 可能会对您有所帮助。

标签: c sockets unix


【解决方案1】:

您可以使用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 srcAddress;

最后,您需要存储控制数据。每个控制数据项都有一个固定大小的标头 (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 的手册页][1] 的链接。请注意,您还可以获得有关其他“级别”(例如 SOL_SOCKET)的信息,记录在该级别的手册页上。

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

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

// 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 套接字根本不返回任何控制数据,除非您有请求。

【讨论】:

  • 在 Linux 内核 4.12.14-122.63-default (SLES12) 中,我发现 TTL 值返回为cmsg->cmsg_type == IP_TTL,而不是cmsg->cmsg_type == IP_RECVTTL。错误在哪里?
  • @U.Windl 没有错误。套接字选项不是标准化的和特定于操作系统的。我的回复是指 BSD 实现,它是所有其他实现的基础实现,它至今仍在 FreeBSD 和 macOS/iOS(以及许多其他系统)中使用。不幸的是,Linux 喜欢与众不同(这就是它也不是 100% 符合 POSIX 的原因)。比较 unix.com/man-page/FreeBSD/4/ipman7.org/linux/man-pages/man7/ip.7.html
【解决方案2】:

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

【讨论】:

  • 从技术上讲,UDP 没有标头。 OP 想要的标头用于 IP(v4 ?)协议。
  • TTL 在 IP 标头中,但我通常是在说。我不明白您所说的“UDP 没有标头”是什么意思。如果您的意思是我们在使用 UDP 套接字时不构建 UDP 标头,那么是的,但它仍然存在,如果您使用原始套接字并使用 UDP 作为传输层协议,则必须构建它。跨度>
猜你喜欢
  • 2013-05-15
  • 2014-05-18
  • 1970-01-01
  • 2010-12-09
  • 1970-01-01
  • 2011-11-01
  • 1970-01-01
  • 2015-01-02
  • 2020-11-29
相关资源
最近更新 更多