我的目标是从Windows计算机发现本地网络中Linux计算机“服务器”的IP地址。从另一台Linux计算机“客户端”我可以做到:
ping -c1 server.local
并得到回复。 “服务器”和“客户端”都运行Avahi,因此这很容易。但是,我想从我的Python应用程序中发现“服务器”的IP地址,该应用程序在MS Windows和Linux计算机上运行。注意:在不运行mDNS软件的MS Windows计算机上,没有主机名解析(显然ping
在所述Windows系统上不起作用)。
我知道pyzeroconf的存在,这是我试图使用的模块;但是,文档很少,对我来说不是很有帮助。使用avahi-discover
之类的工具,我发现计算机发布的mDNS类型_workstation._tcp.local.
的服务类型PTR
(显然是虚拟端口9,丢弃服务)的记录可能相当于DNS A记录。或者我可能完全误解了mDNS机制。
如何通过Python中的mDNS发现计算机的IP地址(或获取计算机的IP地址列表)?
显而易见的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'
但是,在没有运行mDNS软件的Windows计算机上(默认),我得到:
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
答案 0 :(得分:6)
如果有人对此感兴趣,可以在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实现时, 。有趣的是,运行avahi的Linux系统成功地解决了这些目标(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
通过将构造函数传递给使用
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生成,因此其id为非零(在本例中为\ 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忽略(或不接收)答案。
答案 1 :(得分:3)
坚持原始问题的信,答案是肯定的。运行avahi的目标可以使用python zeroconf发现,只要它们宣传某些服务即可。默认情况下,avahi-deamon会公布_workstation。 tcp.local 服务。为了发现这样的服务器,修改随zeroconf一起提供的browser.py示例,以便它查找此服务(或由感兴趣的目标通告的任何其他服务),而不是(或除了)_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。
答案 2 :(得分:-4)
如果您可以ping系统,为什么不使用子进程,将输出重定向到文件,读取文件,瞧!这是一个粗略的草图:
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计数设为1,但是只要有任何网络问题我就做了5。您必须更聪明地解析文件的解析方式。为此你可以使用re模块。
无论如何,这里描述的方法很粗糙,但如果您知道要ping的系统的名称,它应该可以工作。