Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to read the TTL IP header field when receiving UDP packets?

Tags:

c

unix

sockets

I am using UDP socket to send packets and I want to check the TTL field in IP header of received packet. Is it possible?

I notice a IP_HDRINCL sockoption but it seems to work only for RAW socket.

like image 909
ZhengZhiren Avatar asked Mar 25 '23 05:03

ZhengZhiren


1 Answers

iYou can get that information using the recvmsg() interface. First you need to tell the system that you want to access this information:

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

Then you prepare the receive buffer:

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

If you also want to know from where the packet came from (that you also get when using recvfrom() instead of recv()), you'll need storage for that address as well:

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

And finally you need storage for the control data. Every control data item has a fixed size header (struct cmsghdr) that is 12 bytes in size on most systems, followed by payload data whose size and interpretation depends on the kind of control item. In your case, the payload data is just one byte, the TTL value. However, there are some alignment requirements one has to take into account, so you cannot just reserve 13 bytes, in fact your buffer needs to be larger on most systems, that's why the system offers a handy macro for that:

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

In case you want to retrieve multiple control data items, you'd define your buffer like that:

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

With x, y, and z being the size of payload data returned. The size of a plain header without any additional payload data is returned by CMSG_SPACE(0) and it should equal sizeof(struct cmsghdr). But in your case, the payload data is just one byte.

Now you need to put all that together to a 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)
};

Note that you can set all fields you are not interested in to NULL (pointers) or 0 (lengths). You can retrieve only the source address if you like or just the packet payload or only the control data as well as any combination of those three.

And finally you can read from the socket:

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

The return value is just like for recv(), -1 means error, 0 means the other side has closed the stream (but that's only possible in case of TCP and you cannot retrieve TTL for TCP sockets) and otherwise you get the number of bytes written to buffer.

What to do with 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

Okay, but now what about the control data? You need to process it as shown below:

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

The CMSG_DATA() macro gives you a correctly aligned pointer to the actual control data payload. Again, there might be padding for memory aliment requirements so never try to access the data directly.

The advantages of this method over using a raw socket is:

  • This code doesn't require root rights.
  • sendmsg() is more portable than raw sockets.
  • The socket is a normal UDP socket and behaves like any other UDP socket.

For more information on which other information you can obtain that way, you need to check the API documentation of your operation system (e.g. the man page of ip). Here's a link to [the man page from OpenBSD][1] for example. Note that you can also obtain information on form other "levels" (e.g. SOL_SOCKET), documented on the man page of that level.

Oh, and in case you wonder, CMSG_LEN() is similar to CMSG_SPACE() but not identical. CMSG_LEN(x) returns the actual amount of bytes really in use by control data whose payload size is x, whereas CMSG_SPACE(x) returns the the actual amount of bytes really in use by control data whose payload size is x including any padding required after the payload data to correctly align the next control data header. Thus when reserving storage for multiple control data items, you always must use CMSG_SPACE()! You only use CMSG_LEN() for setting the cmsg_len field in struct cmsghdr in case you are creating such structures yourself (e.g. when using sendmsg() which exists as well).

And one last important thing to know: In case you accidentally made the ctrlDataBuffer too small, it's not that you won't get any control data at all or run into an error, the control data will then just be truncated. This truncation is indicated by a flag (the flags field of hdr is ignored on input but it may contain flags on output):

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

If you like, you can get identical behavior in case your data buffer has been chosen too small. Just check out this code:

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

Of course, knowing the correct size after destroying packet may not be really useful. But then you can just do this:

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

That way the data resits in the socket buffer, so you can read it again, now that you know the required buffer size for this. Something similar is not available for control data, though. You need to know the correct control data size in advance or you need to write some trial and error code, e.g. increasing the control data buffer in a loop until the MSG_CTRUNC is not set any longer. Usually once you found a good size, you can remember it as the amount of control data is usually constant for a given socket unless you make setsockopt() calls that would change it. By default a UDP socket returns no control data at all unless you have requested something.

like image 174
Mecki Avatar answered Mar 29 '23 23:03

Mecki