Java в Linux: прослушивание широковещательных сообщений по привязанному локальному адресу

У меня есть несколько странное требование иметь возможность прослушивать ряд сетевых интерфейсов из Java на машине с Linux и определять, получает ли один из них UDP-пакеты определенного типа. Выходные данные, которые мне нужны, - это IP-адрес рассматриваемого интерфейса. Есть ли способ сделать это на Java?

Прослушивание подстановочного адреса (новый DatagramSocket(port)) не помогает, потому что, хотя я и получаю широковещательные пакеты, я не могу определить локальный IP-адрес интерфейса, через который они прошли. Прослушивание широковещательных сообщений при привязке к определенному интерфейсу (новый DatagramSocket (порт, адрес)) вообще не получает пакеты. Этот случай заслуживает примера кода, который показывает, что я пытаюсь сделать:

Enumeration interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
  NetworkInterface ni = (NetworkInterface) interfaces.nextElement();
  Enumeration addresses = ni.getInetAddresses(); 
  while (addresses.hasMoreElements()) { 
    InetAddress address = (InetAddress)addresses.nextElement();
    if (address.isLoopbackAddress() || address instanceof Inet6Address) 
      continue; //Not interested in loopback or ipv6 this time, thanks
    DatagramSocket socket = new DatagramSocket(PORT, address);
     //Try to read the broadcast messages from socket here
  }
}

Я также попытался инициализировать сокет широковещательным адресом, построенным на основе начала реального IP-адреса интерфейса, а остальные - в соответствии с правильной сетевой маской:

byte [] mask = { (byte)255, 0, 0, 0 };
byte[] addrBytes = InetAddress.getByName("126.5.6.7").getAddress();
for (int i=0; i < 4; i++) {
  addrBytes[i] |= ((byte)0xFF) ^ mask[i];
}
InetAddress bcastAddr = InetAddress.getByAddress(addrBytes);

Это просто вызывает BindException при создании DatagramSocket.

EDIT: BindException (java.net.BindException: невозможно назначить запрошенный адрес) от вызова конструктора DatagramSocket с широковещательным адресом (например, 126.255.255.255) поставляется только с последней версией Ubuntu 9.04 (вероятно, не Ubuntu, но проблема, связанная с версией ядра). В Ubuntu 8.10 это работало, как и в версии Red Hat (RHEL 4.x), с которой я имею дело.

По-видимому, отсутствие получения пакетов при привязке к определенному локальному IP-адресу является правильным поведением, хотя в винде это работает. Мне нужно, чтобы он работал в Linux (RHEL и Ubuntu). С низкоуровневым C-кодом есть обходной путь setsockopt(SO_BINDTODEVICE), который я не могу найти в Java-API. Это не внушает мне особого оптимизма :-)


person auramo    schedule 07.05.2009    source источник
comment
Похоже, что эту ошибку не исправили за 10 лет! Сумасшедший! :D   -  person Peter D    schedule 07.05.2009


Ответы (5)


В конце концов, это была проблема ядра Linux IPV6. Обычно я отключал IPV6, потому что это вызывает всякую головную боль. Однако в Ubuntu 9.04 так сложно отключить IPV6, что я сдался, и это меня укусило.

Чтобы прослушивать широковещательные сообщения с определенного интерфейса, я сначала создам «широковещательную версию» IP-адреса интерфейса:

byte [] mask = { (byte)255, 0, 0, 0 };
byte[] addrBytes = InetAddress.getByName("126.5.6.7").getAddress();
for (int i=0; i < 4; i++) {
  addrBytes[i] |= ((byte)0xFF) ^ mask[i];
}
InetAddress bcastAddr = InetAddress.getByAddress(addrBytes);

Конечно, это на самом деле не привязывает меня к определенному интерфейсу, если многие интерфейсы имеют IP-адрес, который начинается с одной и той же сетевой части, но для меня этого решения достаточно.

Затем я создаю datagramsocket с этим адресом (и желаемым портом), и он работает. Но не без передачи следующих системных свойств в JVM:

-Djava.net.preferIPv6Addresses=false -Djava.net.preferIPv4Stack=true 

Понятия не имею, как IPV6 умудряется ломать прослушивание трансляций, но ломает, и указанные выше параметры это исправляют.

person auramo    schedule 11.05.2009

Чтобы еще раз сформулировать проблему, вам нужно определить, на какой интерфейс были получены широковещательные UDP-пакеты.

  • Если вы привязываетесь к подстановочному адресу, вы получаете широковещательные рассылки, но нет возможности определить, на какой сетевой адрес был получен пакет.
  • Если вы привязываетесь к определенному интерфейсу, вы знаете, какой адрес интерфейса вы получаете, но больше не получаете широковещательные сообщения (по крайней мере, в стеке Linux TCP/IP).

Как уже упоминалось, существуют сторонние библиотеки необработанных сокетов для Java, такие как RockSaw или Jpcap, что может помочь вам определить адрес фактического интерфейса .

person Kieran Tully    schedule 09.05.2009
comment
Спасибо, я проверю эти библиотеки и посмотрю, насколько они переносимы. Что я хочу, так это просто скопировать двоичные файлы между win‹-›unix без забот. - person auramo; 10.05.2009

Не уверен, что это поможет, но я знаю, что для получения списка всех сетевых интерфейсов:

Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();

Может быть, вы можете привязываться к каждому отдельно?

Только что нашел несколько отличных примеров использования getNetworkInterfaces().

person Peter D    schedule 07.05.2009
comment
Перебор интерфейсов/адресов - это именно то, что я делаю, но проблема в том, что я не могу слушать широковещательные сообщения. Я ничего не получаю ни с одним из адресов, только когда я создаю DatagramSocket с конструктором с параметром простого порта (используя групповой адрес). - person auramo; 07.05.2009

Насколько мне известно, единственный способ сделать это будет с помощью

IP_RECVDSTADDR

вариант сокета. Предполагается, что эта опция гарантирует, что dst-адрес интерфейса, на который пришел пакет, доступен при привязке к подстановочному адресу. Так что это должно работать и с трансляцией, я полагаю.

Вот пример C, который я нашел в Интернете:

Как получить адрес назначения UDP при входящем пакеты

Я бы прочитал recvmsg, а затем попытался выяснить, доступен ли этот интерфейс в Джава.

Изменить:

Я только что понял, что у вас может быть еще один вариант, если он поддерживается в Java. Вам все еще может понадобиться параметр сокета IP_RECVDSTADDR (не уверен), но вместо использования recvmsg вы можете использовать необработанный сокет и получить адрес назначения из заголовка IP.

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

Вот пример использования UDP с необработанным сокетом в C в Linux:

Расширенный TCP/IP — ПРИМЕРЫ ПРОГРАММЫ RAW SOCKET

Я был бы удивлен, если бы этот метод не работал и в Java.

Изменить2

Еще одна идея. Есть ли причина, по которой вы не можете использовать многоадресную рассылку, или конкретная причина, по которой вы выбрали широковещательную рассылку вместо многоадресной? Насколько я понимаю, с многоадресной рассылкой вы всегда будете знать, на каком интерфейсе получены пакеты, поскольку вы всегда привязываетесь к определенному интерфейсу при присоединении к группе многоадресной рассылки (особенно с IP4, где вы привязываетесь к интерфейсу через один из его IP-адресов).

person Robert S. Barnes    schedule 07.05.2009
comment
Похоже, когда-нибудь это может оказаться в Java 7. - person auramo; 07.05.2009
comment
Я только что понял, что у вас может быть еще один вариант, если он поддерживается в Java. Взгляните на мою правку. - person Robert S. Barnes; 08.05.2009
comment
Использование необработанных сокетов из Java потребует собственного кода и вызовов JNI или JNA. Я не хочу туда идти, потому что это довольно хлопотно. Я бы предпочел вернуться к моему плану Б, который состоит в том, чтобы статически настроить используемый интерфейс. У него есть проблемы с удобством использования, но он превосходит вариант с собственным кодом. - person auramo; 08.05.2009
comment
Еще одна идея. Есть ли причина, по которой вы не можете использовать многоадресную рассылку, или конкретная причина, по которой вы выбрали широковещательную рассылку вместо многоадресной? Насколько я понимаю, с многоадресной рассылкой вы всегда будете знать, на каком интерфейсе получены пакеты, поскольку вы всегда привязываетесь к определенному интерфейсу при присоединении к группе многоадресной рассылки (особенно с IP4, где вы привязываетесь к интерфейсу через один из его IP-адресов). - person Robert S. Barnes; 09.05.2009
comment
Я не выбирал широковещательную передачу, это древний протокол, определенный около 20 лет назад, и он активно используется нашими продуктами, развертываемыми в полевых условиях. - person auramo; 10.05.2009
comment
Ааа, ну удачи с этим... извините, больше ничем помочь не могу. - person Robert S. Barnes; 10.05.2009

Не могу комментировать, поэтому вместо этого добавьте это как ответ.

Это интересно. Хотя мне любопытно, почему вы это делаете

byte[] addrBytes = InetAddress.getByName("126.5.6.7").getAddress();

а не просто

byte[] addrBytes = {126, 5, 6, 7);

или адреса интерфейсов попадают к вам как String ?

person Kieran Tully    schedule 12.05.2009
comment
Никакой причины: вы правы, что простое перечисление байтов в массиве более элегантно. - person auramo; 13.05.2009