Подтвердить что ты не робот

Установка IP-адреса источника для гнезда UDP

У меня есть UDP-сокет, привязанный к INADDR_ANY для прослушивания пакетов на всех IP-адресах моего сервера. Я отправляю ответы через один и тот же сокет.

В настоящий момент сервер автоматически выбирает, какой IP-адрес используется в качестве исходного IP-адреса при отправке пакетов, но я хотел бы иметь возможность самостоятельно устанавливать исходящий исходный IP-адрес.

Есть ли способ сделать это без необходимости создавать отдельный сокет для каждого IP?

4b9b3361

Ответ 1

Николай, используя отдельный сокет и bind (2) для каждого адреса или возиться с таблицами маршрутизации, часто не является допустимым вариантом, например. с динамическими адресами. Один IP_ADDRANY -пределенный UDP-сервер должен иметь возможность реагировать на один и тот же динамически назначенный IP-адрес, по которому принимается пакет.

К счастью, есть и другой способ. В зависимости от вашей системы вы можете использовать опции сокета IP_PKTINFO для установки или получения вспомогательных данных о сообщении. Вспомогательные данные (через cmsg(3)) покрываются во многих местах онлайн, хотя comp.os.linux.development.system имеет полный код, специфичный для IP_PKTINFO.

Код в ссылке использует IP_PKTINFO (или IP_RECVDSTADDR в зависимости от платформы), чтобы получить адрес назначения UDP-сообщения из вспомогательных данных cmsg(3). Перефразируем здесь:

struct msghdr msg;
struct cmsghdr *cmsg;
struct in_addr addr;
// after recvmsg(sd, &msg, flags);
for(cmsg = CMSG_FIRSTHDR(&msg);
    cmsg != NULL;
    cmsg = CMSG_NXTHDR(&msg, cmsg)) {
  if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) {
    addr = ((struct in_pktinfo*)CMSG_DATA(cmsg))->ipi_addr;
    printf("message received on address %s\n", inet_ntoa(addr));
  }
}

Джин, ваш вопрос спросил, как установить адрес источника исходящих пакетов. С помощью IP_PKTINFO можно установить поле ipi_spec_dst struct in_pktinfo в вспомогательных данных, переданных в sendmsg(2). См. Ссылку, указанную выше, cmsg(3) и sendmsg(2) для руководства по созданию и управлению вспомогательными данными в struct msghdr. Пример (без гарантии здесь) может быть:

struct msghdr msg;
struct cmsghdr *cmsg;
struct in_pktinfo *pktinfo;
// after initializing msghdr & control data to CMSG_SPACE(sizeof(struct in_pktinfo))
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = IPPROTO_IP;
cmsg->cmsg_type = IP_PKTINFO;
cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
pktinfo = (struct in_pktinfo*) CMSG_DATA(cmsg);
pktinfo->ipi_ifindex = src_interface_index;
pktinfo->ipi_spec_dst = src_addr;
// bytes_sent = sendmsg(sd, &msg, flags);

Обратите внимание, что в IPv6 это отличается: используйте struct in6_pktinfo::ipi6_addr как в случаях recvmsg, так и в sendmsg.

Обратите внимание также, что Windows не поддерживает эквивалент ipi_spec_dst в структуре in_pktinfo, поэтому вы не можете использовать этот метод для установки адреса источника в исходящем пакете winsock2.

(ссылки на man-страницы - ограничение 1 гиперссылки)

http:// linux.die.net/man/2/sendmsg
http:// linux.die.net/man/3/cmsg

Ответ 2

Я думал, что буду расширять Джереми, как это сделать для IPv6. Джереми оставляет много деталей, и некоторая документация (например, справочная страница Linux для ipv6) просто неверна. Сначала в некоторых дистрибутивах вы должны определить _GNU_SOURCE, иначе некоторые из свойств IPv6 не определены:

#define _GNU_SOURCE
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>

Затем настройте сокет довольно стандартным способом, который прослушивает все IP-пакеты (например, как IPv4, так и IPv6) на определенном порту UDP:

const int on=1, off=0;
int result;
struct sockaddr_in6 sin6;
int soc;

soc = socket(AF_INET6, SOCK_DGRAM, 0);
setsockopt(soc, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
setsockopt(soc, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on));
setsockopt(soc, IPPROTO_IPV6, IPV6_RECVPKTINFO, &on, sizeof(on));
setsockopt(soc, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off));
memset(&sin6, '\0', sizeof(sin6));
sin6.sin6_family = htons(AF_INET6);
sin6.sin6_port = htons(MY_UDP_PORT);
result = bind(soc, (struct sockaddr*)&sin6, sizeof(sin6));

Обратите внимание, что приведенный выше код устанавливает параметры IP и IPv6 для сокета IPv6. Оказывается, если пакет поступит на IPv4-адрес, вы получите IP_PKTINFO (т.е. IPv4) cmsg, даже если это сокет IPv6, и если вы не включите их, они не будут отправлены. Также обратите внимание, что установлена ​​опция IPV6_RECPKTINFO (которая не упоминается в man 7 ipv6), а не IPV6_PKTINFO (что неправильно описано в man 7 ipv6). Теперь получите пакет udp:

int bytes_received;
struct sockaddr_in6 from;
struct iovec iovec[1];
struct msghdr msg;
char msg_control[1024];
char udp_packet[1500];

iovec[0].iov_base = udp_packet;
iovec[0].iov_len = sizeof(udp_packet);
msg.msg_name = &from;
msg.msg_namelen = sizeof(from);
msg.msg_iov = iovec;
msg.msg_iovlen = sizeof(iovec) / sizeof(*iovec);
msg.msg_control = msg_control;
msg.msg_controllen = sizeof(msg_control);
msg.msg_flags = 0;
bytes_received = recvmsg(soc, &msg, 0);

Следующим шагом будет извлечение интерфейса и адрес UDP-пакета, который был получен из cmsg:

struct in_pktinfo in_pktinfo;
struct in6_pktinfo in6_pktinfo;
int have_in_pktinfo = 0;
int have_in6_pktinfo = 0;
struct cmsghdr* cmsg;

for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != 0; cmsg = CMSG_NXTHDR(&msg, cmsg))
{
  if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO)
  {
    in_pktinfo = *(struct in_pktinfo*)CMSG_DATA(cmsg);
    have_in_pktinfo = 1;
  }
  if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_PKTINFO)
  {
    in6_pktinfo = *(struct in6_pktinfo*)CMSG_DATA(cmsg);
    have_in6_pktinfo = 1;
  }
}

Наконец, мы получим, чтобы отправить ответ обратно, используя тот же пункт назначения.

int cmsg_space;

iovec[0].iov_base = udp_response;
iovec[0].iov_len = udp_response_length;
msg.msg_name = &from;
msg.msg_namelen = sizeof(from);
msg.msg_iov = iovec;
msg.msg_iovlen = sizeof(iovec) / sizeof(*iovec);
msg.msg_control = msg_control;
msg.msg_controllen = sizeof(msg_control);
msg.msg_flags = 0;
cmsg_space = 0;
cmsg = CMSG_FIRSTHDR(&msg);
if (have_in6_pktinfo)
{
  cmsg->cmsg_level = IPPROTO_IPV6;
  cmsg->cmsg_type = IPV6_PKTINFO;
  cmsg->cmsg_len = CMSG_LEN(sizeof(in6_pktinfo));
  *(struct in6_pktinfo*)CMSG_DATA(cmsg) = in6_pktinfo;
  cmsg_space += CMSG_SPACE(sizeof(in6_pktinfo));
}
if (have_in_pktinfo)
{
  cmsg->cmsg_level = IPPROTO_IP;
  cmsg->cmsg_type = IP_PKTINFO;
  cmsg->cmsg_len = CMSG_LEN(sizeof(in_pktinfo));
  *(struct in_pktinfo*)CMSG_DATA(cmsg) = in_pktinfo;
  cmsg_space += CMSG_SPACE(sizeof(in_pktinfo));
}
msg.msg_controllen = cmsg_space;
ret = sendmsg(soc, &msg, 0);

Снова обратите внимание, как, если пакет пришел через IPv4, мы должны поставить опцию IPv4 в cmsg, даже если это сокет AF_INET6. По крайней мере, это то, что вам нужно сделать для Linux.

Это удивительный объем работы, но AFAICT - это минимум, который вам нужно сделать, чтобы создать надежный UDP-сервер, который работает во всех мыслимых средах Linux. Большинство из них не требуется для TCP, поскольку он прозрачно обрабатывает многократное преобразование.

Ответ 3

Вы либо bind(2) на каждый адрес интерфейса и управляете несколькими сокетами, либо даете ядру неявное назначение IP-адреса источника с помощью INADDR_ANY. Другого пути нет.

Мой вопрос: зачем вам это нужно? Обычная IP-маршрутизация не работает для вас?

Ответ 4

В последнее время я столкнулся с той же проблемой.

Что я делаю для решения этой проблемы,

  • получить имя интерфейса из полученного пакета
  • привязать сокет к определенному интерфейсу
  • unbind socket

Пример:

  struct ifreq ifr;
  ...
  recvmsg(fd, &msg...)
  ...      
  if (msg.msg_controllen >= sizeof(struct cmsghdr))
    for (cmptr = CMSG_FIRSTHDR(&msg); cmptr; cmptr = CMSG_NXTHDR(&msg, cmptr))
      if (cmptr->cmsg_level == SOL_IP && cmptr->cmsg_type == IP_PKTINFO)
      {
        iface_index = ((struct in_pktinfo *)CMSG_DATA(cmptr))->ipi_ifindex;
      }
  if_indextoname(iface_index , ifr.ifr_name);
  mret=setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr));

  sendmsg(...);

  memset(&ifr, 0, sizeof(ifr));
  snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "");
  mret=setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr));