#!/usr/bin/env python3 """ Pythonic declaration of mDNS .local domains for YunoHost """ import subprocess import re import sys import yaml import socket from time import sleep from typing import List, Dict from zeroconf import Zeroconf, ServiceInfo # Helper command taken from Moulinette def check_output(args, stderr=subprocess.STDOUT, shell=True, **kwargs): """Run command with arguments and return its output as a byte string Overwrite some of the arguments to capture standard error in the result and use shell by default before calling subprocess.check_output. """ return ( subprocess.check_output(args, stderr=stderr, shell=shell, **kwargs) .decode("utf-8") .strip() ) # Helper command taken from Moulinette def _extract_inet(string, skip_netmask=False, skip_loopback=True): """ Extract IP addresses (v4 and/or v6) from a string limited to one address by protocol Keyword argument: string -- String to search in skip_netmask -- True to skip subnet mask extraction skip_loopback -- False to include addresses reserved for the loopback interface Returns: A dict of {protocol: address} with protocol one of 'ipv4' or 'ipv6' """ ip4_pattern = ( r"((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}" ) ip6_pattern = r"(((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::?((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)" ip4_pattern += r"/[0-9]{1,2})" if not skip_netmask else ")" ip6_pattern += r"/[0-9]{1,3})" if not skip_netmask else ")" result = {} for m in re.finditer(ip4_pattern, string): addr = m.group(1) if skip_loopback and addr.startswith("127."): continue # Limit to only one result result["ipv4"] = addr break for m in re.finditer(ip6_pattern, string): addr = m.group(1) if skip_loopback and addr == "::1": continue # Limit to only one result result["ipv6"] = addr break return result # Helper command taken from Moulinette def get_network_interfaces(): # Get network devices and their addresses (raw infos from 'ip addr') devices_raw = {} output = check_output("ip --brief a").split("\n") for line in output: line = line.split() iname = line[0] ips = ' '.join(line[2:]) devices_raw[iname] = ips # Parse relevant informations for each of them devices = { name: _extract_inet(addrs) for name, addrs in devices_raw.items() if name != "lo" } return devices if __name__ == '__main__': ### # CONFIG ### with open('/etc/yunohost/mdns.yml', 'r') as f: config = yaml.safe_load(f) or {} updated = False required_fields = ["interfaces", "domains"] missing_fields = [field for field in required_fields if field not in config] if missing_fields: print("The fields %s are required" % ', '.join(missing_fields)) if config['interfaces'] is None: print('No interface listed for broadcast.') sys.exit(0) if 'yunohost.local' not in config['domains']: config['domains'].append('yunohost.local') zcs = {} interfaces = get_network_interfaces() for interface in config['interfaces']: infos = [] # List of ServiceInfo objects, to feed Zeroconf ips = [] # Human-readable IPs b_ips = [] # Binary-convered IPs ipv4 = interfaces[interface]['ipv4'].split('/')[0] if ipv4: ips.append(ipv4) b_ips.append(socket.inet_pton(socket.AF_INET, ipv4)) ipv6 = interfaces[interface]['ipv6'].split('/')[0] if ipv6: ips.append(ipv6) b_ips.append(socket.inet_pton(socket.AF_INET6, ipv6)) # If at least one IP is listed if ips: # Create a Zeroconf object, and store the ServiceInfos zc = Zeroconf(interfaces=ips) zcs[zc]=[] for d in config['domains']: d_domain=d.replace('.local','') if '.' in d_domain: print(d_domain+'.local: subdomains are not supported.') else: # Create a ServiceInfo object for each .local domain zcs[zc].append(ServiceInfo( type_='_device-info._tcp.local.', name=interface+': '+d_domain+'._device-info._tcp.local.', addresses=b_ips, port=80, server=d+'.', )) print('Adding '+d+' with addresses '+str(ips)+' on interface '+interface) # Run registration print("Registering...") for zc, infos in zcs.items(): for info in infos: zc.register_service(info) try: print("Registered. Press Ctrl+C or stop service to stop.") while True: sleep(1) except KeyboardInterrupt: pass finally: print("Unregistering...") for zc, infos in zcs.items(): for info in infos: zc.unregister_service(info) zc.close()