How can I find the IP address of a host using mdns?
Asked Answered
F

4

15

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?

CLARIFICATION (based on a comment)

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
Fundamentalism answered 20/4, 2012 at 10:4 Comment(5)
Since you can ping the server using a hostname, why not just use normal hostname resolution? Like socket.gethostbynameSerpent
Because normal hostname resolution for a mDNS-published record on a Windows computer not running bonjour or mdnsresponder or whatever does not work.Fundamentalism
You want to be able to do this without a dependency on Bonjour for Windows?Examination
... You want to write a mDNS daemon in Python? You're better off just throwing in Avahi.Centenarian
@Ignacio Great, so I'll throw in Avahi for Windows. No, wait. And in any case, the Windows computers already have Python, but I can't install other software. I think I'd be better off if I used pyzeroconf in my specific case (it IS an mDNS daemon, after all); I just need helpful documentation for it.Fundamentalism
A
8

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.

Aglitter answered 7/3, 2016 at 20:25 Comment(11)
According to the mdns specification (RFC 6762 §6.7) if the query received by an MDNSresponder comes from a source port other than 5353, this is an indication that the querier is a simple resolver, and the responder MUST send a UDP response directly back to the querier, via unicast, to the query packet's source IP address and port. It is due to this that the code above (and nslookup itself) works. And it works only when the responder abides by this stipulation, avahi does and the code works. But simpler implementations (like the esp8266 one) do not, so the code does not work either.Aglitter
In the last paragraph of my answer I say that '(when the registration example is running) zeroconf will respond to address queries but nslookup ignores (or does not receive) the answer'. Actually zeroconf will respond to address queries only if they are coming from the mdns port 5353. nslookup does not send its queries from said port, so zeroconf receives but does not respond to them. The 2nd program above uses 5353 as source port, and so successfully resolves targets advertising their services with zeroconf (eg by running the registration.py example correctly configured).Aglitter
Thanks! I've created a small script based on your answer github.com/AndreMiras/mdns-lookupEscrow
Hey @Aglitter I've been testing your code and I'm trying to discover the arduino services (including esp devices). In the zeroconf package I do that with _arduino._tcp.local. the problem is I need to run the code for a sublime text plugin so I need a code as small as possible and run without much dependencies. In the case of zeroconf netifaces is written in C and I couldn't make it work in ST. Can you tell me if it's possible to do that with you code? I've tried with: for host in ['arduino','_arduino'][::-1]: but I didn't get any resultAnalcite
@GEPD, sorry i just saw your comment, if the matter is still unresolved please tell me and i will try to help. It seems to me though that you are trying to resolve services not hosts, in which case this program will not help. I have found that mdns does not work reliably with esp8266, and given that windows10 does not support it either, I am converting to static ip.Aglitter
@AndreMiras, you are welcome. What kind of hosts are you trying to lookup? In my experience mdns isn't very reliable, even when using avahi.Aglitter
@NameOfTheRose, yes indeed mdns isn't so reliable. I cold resolve raspberrypi.local, some Ubuntu hostname running avahi, as well as printers. So not too bad so far.Escrow
@AndreMiras, if you are running linux, I have found an arp-scan based solution that is more reliable and does not depend on mdns - you have to know the mac address of the device you are looking for. I will gladly post if you are interested.Aglitter
Yes I am interested indeed, but if I know the device ARP, I usually just ping the broadcast and then just grep in my ARP cache, like here: github.com/AndreMiras/km/blob/master/…Escrow
@AndreMiras, arp scan, basically using arp-scan instead of nmap, and wrapping it all in python. Can be imported to other python scripts.Aglitter
@Aglitter Nice thank you! But it requires root access then.Escrow
A
4

Sticking to the letter of the original question, the answer is a qualified yes. Targets running avahi can be discovered with python zeroconf provided that they advertise some service. By default the avahi-deamon advertises the _workstation._tcp.local service. In order to discover such servers modify the browser.py example coming with zeroconf so that it looks for this service (or any other service advertised by the targets of interest) instead of (or in addition to) _http._tcp.local. browser.py will also discover targets using zeroconf's registration.py example to advertise their services, but not esp8266 targets (esp8266 responds with a malformed message to TXT (16) query).

#!/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]

But to be honest I would not use zeroconf for this.

Aglitter answered 28/3, 2016 at 6:47 Comment(1)
There are 50 computers on my network, including several with Linux running avahi, yet this script returns nothing...Impudicity
G
0

If you just wanted a one liner for bash in Linux:

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

Make sure to replace HOSTNAME with the hostname you are looking for.

Godspeed answered 25/2, 2022 at 16:38 Comment(0)
C
-4

If you can ping the system why not use subprocess, redirect the output to a file, read the file and be voila! Here is a rough sketch:

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]

NOTE: In my case ip will be 127.0.0.1, but that is because I used localhost. Also you could make the ping count 1, but I made it 5 just incase there were any network issues. You will have to be smarter about how you parse the file. For that you could use the re module.

Anyway, the method described here is crude, but it should work if you know the name of the system that you want to ping.

Chem answered 8/5, 2012 at 22:35 Comment(1)
I see. I need to make it absolutely clear in the question (right now it's implied) that ping hostname does not work on a Windows computer not running mDNS software, otherwise I'll have more similar answers. Thanks for taking the time to reply, anyway.Fundamentalism

© 2022 - 2024 — McMap. All rights reserved.