Можно ли прочитать поле заголовка IP-адреса TTL при получении пакетов UDP?
Я использую сокет UDP для отправки пакетов и хочу проверить поле TTL в заголовке IP полученного пакета. Является ли это возможным?
Я заметил sockoption IP_HDRINCL, но он, кажется, работает только для сокета RAW.
2 ответа
Вы можете получить эту информацию, используя 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)
];
С x
, y
, а также z
размер возвращаемых данных полезной нагрузки. Размер простого заголовка без каких-либо дополнительных данных полезной нагрузки возвращается 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, и вы не можете получить TTL для сокетов TCP), в противном случае вы получите количество байтов, записанных в 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()
макрос дает правильно выровненный указатель на фактическую полезную нагрузку управляющих данных. Опять же, может быть заполнение для требований памяти, поэтому никогда не пытайтесь получить доступ к данным напрямую.
Преимущества этого метода по сравнению с использованием необработанного сокета:
- Этот код не требует рут прав.
sendmsg()
является более портативным, чем необработанные сокеты.- Сокет является обычным сокетом UDP и ведет себя как любой другой сокет UDP.
Для получения дополнительной информации о том, какую другую информацию вы можете получить таким образом, вам необходимо проверить документацию API вашей операционной системы (например, страницу руководства ip
). Вот ссылка на справочную страницу из OpenBSD, например. Обратите внимание, что вы также можете получить информацию о других "уровнях" формы (например, SOL_SOCKET), документированных на странице руководства этого уровня.
Ох, и если вам интересно, CMSG_LEN()
похож на CMSG_SPACE()
но не идентичны. CMSG_LEN(x)
возвращает фактическое количество байтов, действительно используемых контрольными данными, размер полезной нагрузки которых x
, в то время как CMSG_SPACE(x)
возвращает фактическое количество байтов, действительно используемых контрольными данными, размер полезной нагрузки которых x
включая любые отступы, необходимые после данных полезной нагрузки, чтобы правильно выровнять следующий заголовок управляющих данных. Таким образом, резервируя хранилище для нескольких элементов данных управления, вы всегда должны использовать CMSG_SPACE()
! Вы используете только CMSG_LEN()
для настройки cmsg_len
поле в struct cmsghdr
если вы создаете такие структуры самостоятельно (например, при использовании 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 не возвращает никаких управляющих данных, если только вы не запросили что-либо.
Когда вы используете сокет UDP, все заголовки удаляются (декапсулируются), поэтому вы не сможете получить значение поля TTL или любое другое поле заголовка IP, но если вы заинтересованы в его получении или настройке, используйте необработанные сокеты и создавайте свои заголовки, используя необработанные сокеты, заголовки будут передаваться вашему приложению, включая заголовки слоя, которые вы создали ( IP+ Transport).