blob: 07058b16418cd14a7b7b9fbbc57def3c1b6e2242 [file] [log] [blame]
# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module that enumerates mDNS services."""
from __future__ import print_function
import collections
import dpkt
import select
import socket
import time
MDNS_MULTICAST_ADDRESS = '224.0.0.251'
MDNS_PORT = 5353
PACKET_BUFFER_SIZE = 2048
Service = collections.namedtuple(
'Service',
('hostname', 'ip', 'port', 'ptrname', 'text'))
def _ParseResponse(data):
"""Parse mDNS network response into Service tuple.
Args:
data: Byte-blob of mDNS response.
Returns:
None if the mDNS response is invalid or incomplete, else a fully populated
|Service| tuple.
"""
hostname = None
ip = None
port = None
ptrname = None
text = None
try:
dns_response = dpkt.dns.DNS(data)
except (dpkt.Error, dpkt.NeedData, dpkt.UnpackError):
# Ignore bad mDNS response.
return None
for rr in dns_response.an:
if rr.type == dpkt.dns.DNS_A:
ip = socket.inet_ntoa(rr.ip)
elif rr.type == dpkt.dns.DNS_PTR:
ptrname = rr.ptrname
elif rr.type == dpkt.dns.DNS_SRV:
hostname = rr.srvname
port = rr.port
elif rr.type == dpkt.dns.DNS_TXT:
text = dict(entry.split('=', 1) for entry in rr.text)
service = Service(hostname, ip, port, ptrname, text)
# Ignore incomplete responses.
if any(x is None for x in service):
return None
return service
def FindServices(source_ip, service_name, should_add_func=None,
should_continue_func=None, timeout_seconds=1):
"""Find all instances of |service_name| on the network.
For each service found, |should_add_func| is called with the service
information to determine wheter the service should be added to the results
list. This method exits after |timeout_seconds| or earlier if instructed by
the return value from |should_continue_func|.
Args:
source_ip: IP address of the network interface to use for service discovery.
service_name: Name of mDNS service to discover (eg. '_ssh._tcp.local').
should_add_func: Function called for each service found to determine if the
service should be added to the results list. If None is specified, all
services found are added to the results list.
should_continue_func: Function called for each service found to determine
whether to continue with service discovery. If None is specified,
service discovery continues until the timeout expires.
timeout_seconds: Number of seconds to wait for services to respond to
discovery request.
Returns:
List of |Service| found.
"""
# Default callback functions that add all services to the results list and
# continue service discovery until the timeout expires.
if not should_add_func:
should_add_func = lambda service: True
if not should_continue_func:
should_continue_func = lambda service: True
net_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
net_socket.bind((source_ip, 0))
query = [dpkt.dns.DNS.Q(name=service_name, type=dpkt.dns.DNS_PTR)]
dns_packet = dpkt.dns.DNS(op=dpkt.dns.DNS_QUERY, qd=query)
net_socket.sendto(str(dns_packet),
(MDNS_MULTICAST_ADDRESS, MDNS_PORT))
results = []
remaining_time = timeout_seconds
end_time = time.time() + remaining_time
while remaining_time > 0:
read_ready, _, _ = select.select([net_socket], [], [], remaining_time)
if not read_ready:
break
data, _ = net_socket.recvfrom(PACKET_BUFFER_SIZE)
service = _ParseResponse(data)
if service:
if should_add_func(service):
results.append(service)
if not should_continue_func(service):
break
remaining_time = end_time - time.time()
return results