Как я могу найти IP-адрес хоста, используя mdns?

Моя цель - найти IP-адрес компьютера "Linux" в локальной сети с компьютера с Windows. С другого компьютера Linux "клиент" я могу сделать:

ping -c1 server.local

и получите ответ. И "сервер", и "клиент" запускают Avahi, так что это легко. Тем не менее, я хотел бы узнать IP-адрес "сервера" из моего приложения Python, которое работает как на компьютерах с MS Windows, так и с Linux. Примечание: на компьютерах MS Windows, на которых не работает программное обеспечение mDNS, разрешение имени хоста отсутствует (и, очевидно, ping не работает в указанных системах Windows).

Я знаю о существовании pyzeroconf, и это модуль, который я пытался использовать; Тем не менее, документация скудна и не очень полезна для меня. Используя такие инструменты, как avahi-discoverЯ понял, что компьютеры публикуют записи типа сервиса _workstation._tcp.local. (с явно фиктивным портом 9, службой сброса) типа mDNS PTR это может быть эквивалент DNS-записи. Или я мог неправильно понять механизм mDNS.

Как я могу узнать IP-адрес компьютера (или получить список IP-адресов компьютеров) через mDNS из Python?

Уточнение (на основе комментария)

Очевидное socket.gethostbyname работает на компьютере, работающем и настроенном для использования программного обеспечения mDNS (например, Avahi):

Python 2.6.5 (r265:79063, Apr 16 2010, 13:09:56)
[GCC 4.4.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> socket.gethostbyname('server.local')
'192.168.42.42'

Однако на компьютерах с Windows, на которых не запущено программное обеспечение mDNS (по умолчанию), я получаю:

Python 2.7.1 (r271:86832, Nov 27 2010, 18:30:46) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> socket.gethostbyname('server.local')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
socket.gaierror: [Errno 11001] getaddrinfo failed

4 ответа

В случае, если кто-то все еще заинтересован в этом, задачу можно выполнить в Windows и Linux, используя dnspython следующим образом:

import dns.resolver
myRes=dns.resolver.Resolver()
myRes.nameservers=['224.0.0.251'] #mdns multicast address
myRes.port=5353 #mdns port
a=myRes.query('microknoppix.local','A')
print a[0].to_text()
#'10.0.0.7'
a=myRes.query('7.0.0.10.in-addr.arpa','PTR')
print a[0].to_text()
#'Microknoppix.local.'

Этот код работает, когда на целевом компьютере запускается avahi, но происходит сбой, когда на целевом компьютере выполняется python zeroconf или реализация esp8266 mdns. Интересно, что системы Linux, работающие с avahi, успешно решают такие задачи (по-видимому, avahi реализует плагин nssswitch.conf mdns и является более полной реализацией протокола mdns)
В случае наивного респондента mdns, который, в отличие от rfc, отправляет свой ответ через порт mdns, у меня работает следующий код (запуск на linux и windows и определение целей linux avahi, hp printer и esp8266): (а также несовместим, так как использует порт MDNS для отправки запроса, хотя это явно НЕ полная реализация)

import socket
import struct
import dpkt, dpkt.dns
UDP_IP="0.0.0.0"
UDP_PORT=5353
MCAST_GRP = '224.0.0.251'
sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM )
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind( (UDP_IP,UDP_PORT) )
#join the multicast group
mreq = struct.pack("4sl", socket.inet_aton(MCAST_GRP), socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
for host in ['esp01','microknoppix','pvknoppix','hprinter'][::-1]:
#    the string in the following statement is an empty query packet
     dns = dpkt.dns.DNS('\x00\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01')
     dns.qd[0].name=host+'.local'
     sock.sendto(dns.pack(),(MCAST_GRP,UDP_PORT))
sock.settimeout(5)
while True:
  try:
     m=sock.recvfrom( 1024 );#print '%r'%m[0],m[1]
     dns = dpkt.dns.DNS(m[0])
     if len(dns.qd)>0:print dns.__repr__(),dns.qd[0].name
     if len(dns.an)>0 and dns.an[0].type == dpkt.dns.DNS_A:print dns.__repr__(),dns.an[0].name,socket.inet_ntoa(dns.an[0].rdata)
  except socket.timeout:
     break
#DNS(qd=[Q(name='hprinter.local')]) hprinter.local
#DNS(qd=[Q(name='pvknoppix.local')]) pvknoppix.local
#DNS(qd=[Q(name='microknoppix.local')]) microknoppix.local
#DNS(qd=[Q(name='esp01.local')]) esp01.local
#DNS(an=[RR(name='esp01.local', rdata='\n\x00\x00\x04', ttl=120, cls=32769)], op=33792) esp01.local 10.0.0.4
#DNS(an=[RR(name='PVknoppix.local', rdata='\n\x00\x00\xc2', ttl=120, cls=32769)], op=33792) PVknoppix.local 10.0.0.194


Пустой объект DNS был создан в вышеприведенном коде путем передачи конструктору строки, собранной из сети с использованием

m0=sock.recvfrom( 1024 );print '%r'%m0[0]
#'\xf6\xe8\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x05esp01\x05local\x00\x00\x01\x00\x01'

Этот запрос был создан nslookup, поэтому его идентификатор был ненулевым (в данном случае \xf6\xe8), пытаясь разрешить esp01.local. Затем был создан объект DNS, содержащий пустой запрос:

dns = dpkt.dns.DNS(m0[0])
dns.id=0
dns.qd[0].name=''
print '%r'%dns.pack()
#'\x00\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01'

Тот же результат также может быть создан:

dns=dpkt.dns.DNS(qd=[dpkt.dns.DNS.Q(name='')])

Объект DNS также может быть создан с непустым запросом:

dns=dpkt.dns.DNS(qd=[dpkt.dns.DNS.Q(name='esp01.local')])

или даже с несколькими запросами:

dns=dpkt.dns.DNS(qd=[dpkt.dns.DNS.Q(name='esp01.local'),dpkt.dns.DNS.Q(name='esp02.local')])

но минимальные респонденты могут не обрабатывать DNS-сообщения, содержащие несколько запросов


Я также недоволен документацией по python zeroconf. Из случайного чтения кода и мониторинга пакетов с использованием tcpdump кажется, что (когда работает пример регистрации) zeroconf будет отвечать на запросы адреса, но nslookup игнорирует (или не получает) ответ.

Придерживаясь буквы исходного вопроса, ответ квалифицированный да. Цели, на которых работает avahi, могут быть обнаружены с помощью python zeroconf при условии, что они рекламируют какой-либо сервис. По умолчанию avahi-deamon объявляет рабочую станцию.Сервис tcp.local. Чтобы обнаружить такие серверы, измените пример browser.py, поставляемый с zeroconf, чтобы он искал эту службу (или любую другую службу, объявленную интересующими объектами) вместо (или в дополнение к) _http.tcp.local. browser.py также обнаружит цели, используя пример zeroconf registration.py для рекламы своих сервисов, но не цели esp8266 (esp8266 отвечает неправильно сформированным сообщением на запрос TXT (16)).

#!/usr/bin/env python
from __future__ import absolute_import, division, print_function, unicode_literals
""" Example of resolving local hosts"""
# a stripped down verssion of browser.py example
# zeroconf may have issues with ipv6 addresses and mixed case hostnames
from time import sleep

from zeroconf import ServiceBrowser, ServiceStateChange, Zeroconf,DNSAddress

def on_service_state_change(zeroconf, service_type, name, state_change):
    if state_change is ServiceStateChange.Added:
        zeroconf.get_service_info(service_type, name)

zeroconf = Zeroconf()
ServiceBrowser(zeroconf, "_workstation._tcp.local.", handlers=[on_service_state_change])
ServiceBrowser(zeroconf, "_telnet._tcp.local.", handlers=[on_service_state_change])
ServiceBrowser(zeroconf, "_http._tcp.local.", handlers=[on_service_state_change])
ServiceBrowser(zeroconf, "_printer._tcp.local.", handlers=[on_service_state_change])
sleep(2)
#lookup specific hosts
print(zeroconf.cache.entries_with_name('esp01.local.'))
print(zeroconf.cache.entries_with_name('microknoppix.local.'))
print(zeroconf.cache.entries_with_name('pvknoppix.local.'))
print(zeroconf.cache.entries_with_name('debian.local.'))
cache=zeroconf.cache.cache
zeroconf.close()
# list all known hosts in .local
for key in cache.keys():
    if isinstance(cache[key][0],DNSAddress):
       print(key,cache[key])
sleep(1)
#output follows
#[10.0.0.4]
#[10.0.0.7]
#[]
#[3ffe:501:ffff:100:a00:27ff:fe6f:1bfb, 10.0.0.6]
#debian.local. [3ffe:501:ffff:100:a00:27ff:fe6f:1bfb, 10.0.0.6]
#esp01.local. [10.0.0.4]
#microknoppix.local. [10.0.0.7]

Но если честно, я бы не использовал для этого zeroconf.

Если вам просто нужен один лайнер для bash в Linux:

      getent hosts HOSTNAME.local | awk '{ print $1 }'

Обязательно замените HOSTNAME на имя хоста, которое вы ищете.

Если вы можете пропинговать систему, почему бы не использовать подпроцесс, перенаправить вывод в файл, прочитать файл и быть вуаля! Вот примерный набросок:

import subprocess
server = 'localhost'
cmd = 'ping -c 5 %s &> hostname_ping.txt' % server
proc = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
ret = proc.wait()
if ret != 0:
    # Things went horribly wrong!
    #NOTE: You could also do some type of retry.
    sys.exit(ret)
f = open('hostname_ping.txt')
ip = f.next().split(' ')[3][:-1]

ПРИМЕЧАНИЕ: в моем случае ip будет 127.0.0.1, но это потому, что я использовал localhost. Также вы можете сделать ping count 1, но я сделал 5, только если возникли какие-либо проблемы с сетью. Вы должны быть умнее в том, как вы анализируете файл. Для этого вы можете использовать модуль re.

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

Другие вопросы по тегам