Java на Linux: прослушивание широковещательных сообщений по связанному локальному адресу - PullRequest
6 голосов
/ 07 мая 2009

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

Прослушивание по шаблону (новый DatagramSocket (порт)) не помогает, потому что, пока я получаю широковещательные пакеты, я не могу определить локальный 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-адресу является правильным поведением , хотя в Windows это работает. Мне нужно, чтобы он работал на Linux (RHEL и Ubuntu). В низкоуровневом C-коде есть обходной путь setsockopt (SO_BINDTODEVICE), который я не могу найти в Java-API. Это точно не вызывает у меня оптимизма: -)

Ответы [ 5 ]

4 голосов
/ 11 мая 2009

В конце концов это была проблема с ядром 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, который начинается с одной и той же сетевой части, но для меня этого решения достаточно.

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

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

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

1 голос
/ 09 мая 2009

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

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

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

0 голосов
/ 13 мая 2009

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

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

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

, а не просто

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

или адреса интерфейсов получаются в виде строки?

0 голосов
/ 07 мая 2009

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

IP_RECVDSTADDR

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

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

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

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

Редактировать:

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

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

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

Усовершенствованный TCP / IP - ПРИМЕРЫ ПРОГРАММЫ RAW SOCKET

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

Edit2

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

0 голосов
/ 07 мая 2009

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

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

Может быть, вы можете связать каждого из них независимо?

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

...