My target is to discover the IP address of a Linux computer "server" in the local network from a Windows computer. From another Linux computer "client" I can do:
ping -c1 server.local
and get a reply. Both "server" and "client" run Avahi, so this is easy. However, I would like to discover the IP address of "server" from a Python application of mine, which runs on both MS Windows and Linux computers. Note: on MS Windows computers that do not run mDNS software, there is no hostname resolution (and obviously ping
does not work on said Windows systems).
I know of the existence of pyzeroconf, and this is the module I tried to use; however, the documentation is scarce and not very helpful to me. Using tools like avahi-discover
, I figured that computers publish records of the service type _workstation._tcp.local.
(with the obviously dummy port 9, the discard service) of mDNS type PTR
that might be the equivalent of a DNS A record. Or I might have misunderstood completely the mDNS mechanism.
How can I discover the IP address of a computer (or get a list of IP addresses of computers) through mDNS from Python?
The obvious socket.gethostbyname
works on a computer running and configured to use mDNS software (like 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'
However, on Windows computers not running mDNS software (the default), I get:
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
In case somebody is still interested in this, the task can be accomplished, on Windows and Linux, using dnspython as follows:
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.'
This code works when the target computer runs avahi, but fails when the target runs python zeroconf or the esp8266 mdns implementation. Interestingly Linux systems running avahi successfully resolve such targets (avahi apparently implementing nssswitch.conf mdns plugin and being a fuller implementation of the mdns protocol)
In case of a naive mdns responder which, contrary to the rfc, sends its response via the mdns port, the following code (run on linux and windows and resolving linux avahi, hp printer and esp8266 targets) works for me: (and is also non-compliant as it uses the MDNS port to send the query while it is obviously NOT a full implementation)
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
The empty dns object was created in the above code by passing the constructor a string collected from the network using
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'
This query was produced by nslookup so its id was non-zero (in this case \xf6\xe8) trying to resolve esp01.local. An dns object containing an empty query was then created by:
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'
The same result could also be created by:
dns=dpkt.dns.DNS(qd=[dpkt.dns.DNS.Q(name='')])
The dns object could also be created with non-empty query:
dns=dpkt.dns.DNS(qd=[dpkt.dns.DNS.Q(name='esp01.local')])
or even with multiple queries:
dns=dpkt.dns.DNS(qd=[dpkt.dns.DNS.Q(name='esp01.local'),dpkt.dns.DNS.Q(name='esp02.local')])
but minimal responders may fail to handle dns messages containing multiple queries
I am also unhappy with the python zeroconf documentation. From a casual reading of the code and packet monitoring using tcpdump, it seems that (when the registration example is running) zeroconf will respond to address queries but nslookup ignores (or does not receive) the answer.