From 7c92b44ddb19b3c712f78b898e11a5fadc9b7092 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Mon, 5 Jul 2021 21:24:00 +0000 Subject: [PATCH 01/33] Add mdns.py for mDNS broadcast --- data/other/mdns.py | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 data/other/mdns.py diff --git a/data/other/mdns.py b/data/other/mdns.py new file mode 100644 index 000000000..144742758 --- /dev/null +++ b/data/other/mdns.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +""" +WIP +Pythonic declaration of mDNS .local domains. +Heavily based off https://github.com/jstasiak/python-zeroconf/blob/master/tests/test_asyncio.py +""" + +import os +import sys +import argparse + +import asyncio +import logging +import socket +import time +from typing import List + +sys.path.insert(0, "/usr/lib/moulinette/") +from yunohost.domain import domain_list + +from zeroconf import IPVersion +from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf + +# TODO: Remove traceback beautification +from rich.traceback import install +install(show_locals=True) + +async def register_services(infos: List[AsyncServiceInfo]) -> None: + tasks = [aiozc.async_register_service(info) for info in infos] + background_tasks = await asyncio.gather(*tasks) + await asyncio.gather(*background_tasks) + + +async def unregister_services(infos: List[AsyncServiceInfo]) -> None: + tasks = [aiozc.async_unregister_service(info) for info in infos] + background_tasks = await asyncio.gather(*tasks) + await asyncio.gather(*background_tasks) + + +async def close_aiozc(aiozc: AsyncZeroconf) -> None: + await aiozc.async_close() + + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG) + + local_domains = [ d for d in domain_list()['domains'] if d.endswith('.local') ] + + parser = argparse.ArgumentParser() + parser.add_argument('--debug', action='store_true') + version_group = parser.add_mutually_exclusive_group() + version_group.add_argument('--v6', action='store_true') + version_group.add_argument('--v6-only', action='store_true') + args = parser.parse_args() + + if args.debug: + logging.getLogger('zeroconf').setLevel(logging.DEBUG) + if args.v6: + ip_version = IPVersion.All + elif args.v6_only: + ip_version = IPVersion.V6Only + else: + ip_version = IPVersion.V4Only + + infos = [] + for d in local_domains: + d_domain=d.replace('.local','') + infos.append( + AsyncServiceInfo( + type_="_device-info._tcp.local.", + name=d_domain+f"._device-info._tcp.local.", + addresses=[socket.inet_aton("127.0.0.1")], + port=80, + server=d, + ) + ) + + print("Registration of .local domains, press Ctrl-C to exit...") + aiozc = AsyncZeroconf(ip_version=ip_version) + loop = asyncio.get_event_loop() + loop.run_until_complete(register_services(infos)) + print("Registration complete.") + try: + while True: + time.sleep(0.1) + except KeyboardInterrupt: + pass + finally: + print("Unregistering...") + loop.run_until_complete(unregister_services(infos)) + print("Unregistration complete.") + loop.run_until_complete(close_aiozc(aiozc)) + From 99390f23133f41b21a2665dbccbb417bbdc46413 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Mon, 5 Jul 2021 23:01:19 +0000 Subject: [PATCH 02/33] PoC for mDNS broadcast --- data/other/mdns.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/data/other/mdns.py b/data/other/mdns.py index 144742758..2146caec0 100644 --- a/data/other/mdns.py +++ b/data/other/mdns.py @@ -2,8 +2,8 @@ """ WIP -Pythonic declaration of mDNS .local domains. -Heavily based off https://github.com/jstasiak/python-zeroconf/blob/master/tests/test_asyncio.py +Pythonic declaration of mDNS .local domains for YunoHost +Based off https://github.com/jstasiak/python-zeroconf/blob/master/tests/test_asyncio.py """ import os @@ -18,6 +18,7 @@ from typing import List sys.path.insert(0, "/usr/lib/moulinette/") from yunohost.domain import domain_list +from yunohost.utils.network import get_network_interfaces from zeroconf import IPVersion from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf @@ -47,21 +48,26 @@ if __name__ == '__main__': local_domains = [ d for d in domain_list()['domains'] if d.endswith('.local') ] + # TODO: Create setting to list interfaces + wanted_interfaces = [ 'zt3jnskpna' ] + interfaces = get_network_interfaces() + ips = [] + for i in wanted_interfaces: + try: + ips.append(socket.inet_pton(socket.AF_INET, interfaces[i]['ipv4'].split('/')[0])) + except: + pass + try: + ips.append(socket.inet_pton(socket.AF_INET6, interfaces[i]['ipv6'].split('/')[0])) + except: + pass + parser = argparse.ArgumentParser() parser.add_argument('--debug', action='store_true') - version_group = parser.add_mutually_exclusive_group() - version_group.add_argument('--v6', action='store_true') - version_group.add_argument('--v6-only', action='store_true') args = parser.parse_args() if args.debug: logging.getLogger('zeroconf').setLevel(logging.DEBUG) - if args.v6: - ip_version = IPVersion.All - elif args.v6_only: - ip_version = IPVersion.V6Only - else: - ip_version = IPVersion.V4Only infos = [] for d in local_domains: @@ -70,14 +76,14 @@ if __name__ == '__main__': AsyncServiceInfo( type_="_device-info._tcp.local.", name=d_domain+f"._device-info._tcp.local.", - addresses=[socket.inet_aton("127.0.0.1")], + addresses=ips, port=80, - server=d, + server=d+'.', ) ) print("Registration of .local domains, press Ctrl-C to exit...") - aiozc = AsyncZeroconf(ip_version=ip_version) + aiozc = AsyncZeroconf() loop = asyncio.get_event_loop() loop.run_until_complete(register_services(infos)) print("Registration complete.") From 654690b42749ea0ee022136f601ccc8c171e13e6 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 6 Jul 2021 10:48:39 +0000 Subject: [PATCH 03/33] Add interfaces selection for mDNS broadcast - System setting added - Loop through list of interfaces --- data/other/mdns.py | 106 +++++++++++++++++++++++---------------- locales/en.json | 1 + src/yunohost/settings.py | 1 + 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/data/other/mdns.py b/data/other/mdns.py index 2146caec0..0e249e7b8 100644 --- a/data/other/mdns.py +++ b/data/other/mdns.py @@ -19,26 +19,23 @@ from typing import List sys.path.insert(0, "/usr/lib/moulinette/") from yunohost.domain import domain_list from yunohost.utils.network import get_network_interfaces +from yunohost.settings import settings_get +from moulinette import m18n +from moulinette.interfaces.cli import get_locale -from zeroconf import IPVersion from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf -# TODO: Remove traceback beautification -from rich.traceback import install -install(show_locals=True) -async def register_services(infos: List[AsyncServiceInfo]) -> None: +async def register_services(aiozc: AsyncZeroconf, infos: List[AsyncServiceInfo]) -> None: tasks = [aiozc.async_register_service(info) for info in infos] background_tasks = await asyncio.gather(*tasks) await asyncio.gather(*background_tasks) - -async def unregister_services(infos: List[AsyncServiceInfo]) -> None: +async def unregister_services(aiozc: AsyncZeroconf, infos: List[AsyncServiceInfo]) -> None: tasks = [aiozc.async_unregister_service(info) for info in infos] background_tasks = await asyncio.gather(*tasks) await asyncio.gather(*background_tasks) - async def close_aiozc(aiozc: AsyncZeroconf) -> None: await aiozc.async_close() @@ -46,22 +43,6 @@ async def close_aiozc(aiozc: AsyncZeroconf) -> None: if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) - local_domains = [ d for d in domain_list()['domains'] if d.endswith('.local') ] - - # TODO: Create setting to list interfaces - wanted_interfaces = [ 'zt3jnskpna' ] - interfaces = get_network_interfaces() - ips = [] - for i in wanted_interfaces: - try: - ips.append(socket.inet_pton(socket.AF_INET, interfaces[i]['ipv4'].split('/')[0])) - except: - pass - try: - ips.append(socket.inet_pton(socket.AF_INET6, interfaces[i]['ipv6'].split('/')[0])) - except: - pass - parser = argparse.ArgumentParser() parser.add_argument('--debug', action='store_true') args = parser.parse_args() @@ -69,24 +50,62 @@ if __name__ == '__main__': if args.debug: logging.getLogger('zeroconf').setLevel(logging.DEBUG) - infos = [] - for d in local_domains: - d_domain=d.replace('.local','') - infos.append( - AsyncServiceInfo( - type_="_device-info._tcp.local.", - name=d_domain+f"._device-info._tcp.local.", - addresses=ips, - port=80, - server=d+'.', - ) - ) - print("Registration of .local domains, press Ctrl-C to exit...") - aiozc = AsyncZeroconf() - loop = asyncio.get_event_loop() - loop.run_until_complete(register_services(infos)) - print("Registration complete.") + local_domains = [ d for d in domain_list()['domains'] if d.endswith('.local') ] + + m18n.load_namespace("yunohost") + m18n.set_locale(get_locale()) + + if settings_get('mdns.interfaces'): + wanted_interfaces = settings_get('mdns.interfaces').split() + else: + wanted_interfaces = [] + print('No interface listed for broadcast.') + + aiozcs = [] + interfaces = get_network_interfaces() + for interface in wanted_interfaces: + infos = [] + ips = [] # Human-readable IPs + b_ips = [] # Binary-convered IPs + + # Parse the IPs and prepare their binary version + try: + ip = interfaces[interface]['ipv4'].split('/')[0] + ips.append(ip) + b_ips.append(socket.inet_pton(socket.AF_INET, ip)) + except: + pass + try: + ip = interfaces[interface]['ipv6'].split('/')[0] + ips.append(ip) + b_ips.append(socket.inet_pton(socket.AF_INET6, ip)) + except: + pass + + # Create a ServiceInfo object for each .local domain + for d in local_domains: + d_domain=d.replace('.local','') + infos.append( + AsyncServiceInfo( + type_="_device-info._tcp.local.", + name=d_domain+f"._device-info._tcp.local.", + addresses=b_ips, + port=80, + server=d+'.', + ) + ) + print('Adding '+d+' with addresses '+str(ips)+' on interface '+interface) + + # Create an AsyncZeroconf object, store it, and start Service registration + aiozc = AsyncZeroconf(interfaces=ips) + aiozcs.append(aiozc) + print("Registration on interface "+interface+"...") + loop = asyncio.get_event_loop() + loop.run_until_complete(register_services(aiozc, infos)) + + # We are done looping among the interfaces + print("Registration complete. Press Ctrl-c to exit...") try: while True: time.sleep(0.1) @@ -94,7 +113,8 @@ if __name__ == '__main__': pass finally: print("Unregistering...") - loop.run_until_complete(unregister_services(infos)) + for aiozc in aiozcs: + loop.run_until_complete(unregister_services(aiozc, infos)) + loop.run_until_complete(close_aiozc(aiozc)) print("Unregistration complete.") - loop.run_until_complete(close_aiozc(aiozc)) diff --git a/locales/en.json b/locales/en.json index 84a01dfaa..70a0e9309 100644 --- a/locales/en.json +++ b/locales/en.json @@ -321,6 +321,7 @@ "global_settings_cant_write_settings": "Could not save settings file, reason: {reason:s}", "global_settings_key_doesnt_exists": "The key '{settings_key:s}' does not exist in the global settings, you can see all the available keys by running 'yunohost settings list'", "global_settings_reset_success": "Previous settings now backed up to {path:s}", + "global_settings_setting_mdns_interfaces": "Space-separated list of interfaces for mDNS broadcast. Leave empty to disable mDNS.", "global_settings_setting_pop3_enabled": "Enable the POP3 protocol for the mail server", "global_settings_setting_security_nginx_compatibility": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", "global_settings_setting_security_password_admin_strength": "Admin password strength", diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 0466d8126..36904ee70 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -100,6 +100,7 @@ DEFAULTS = OrderedDict( ("smtp.relay.password", {"type": "string", "default": ""}), ("backup.compress_tar_archives", {"type": "bool", "default": False}), ("ssowat.panel_overlay.enabled", {"type": "bool", "default": True}), + ("mdns.interfaces", {"type": "string", "default": ""}), ] ) From 9e93efa895ae6c3a7013c47adeb2891bdf3c8a3f Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 6 Jul 2021 20:33:23 +0000 Subject: [PATCH 04/33] Move mDNS script to yunomdns --- data/other/mdns.py => bin/yunomdns | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename data/other/mdns.py => bin/yunomdns (100%) mode change 100644 => 100755 diff --git a/data/other/mdns.py b/bin/yunomdns old mode 100644 new mode 100755 similarity index 100% rename from data/other/mdns.py rename to bin/yunomdns From 99aacd8b51ade29f5f153cde3808f55706c15bf6 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 11 Jul 2021 14:23:41 +0000 Subject: [PATCH 05/33] Do not rely on Moulinette, integration with Yunohost of mDNS broadcast --- bin/yunomdns | 269 +++++++++++++++++++++------ data/hooks/conf_regen/01-yunohost | 5 +- data/other/yunomdns.service | 13 ++ data/templates/yunohost/mdns.yml | 4 + data/templates/yunohost/services.yml | 2 + debian/install | 1 + debian/postinst | 4 + 7 files changed, 244 insertions(+), 54 deletions(-) create mode 100644 data/other/yunomdns.service create mode 100644 data/templates/yunohost/mdns.yml diff --git a/bin/yunomdns b/bin/yunomdns index 0e249e7b8..795481dfa 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -6,115 +6,278 @@ Pythonic declaration of mDNS .local domains for YunoHost Based off https://github.com/jstasiak/python-zeroconf/blob/master/tests/test_asyncio.py """ +import subprocess import os +import re import sys import argparse +import yaml import asyncio import logging import socket import time -from typing import List - -sys.path.insert(0, "/usr/lib/moulinette/") -from yunohost.domain import domain_list -from yunohost.utils.network import get_network_interfaces -from yunohost.settings import settings_get -from moulinette import m18n -from moulinette.interfaces.cli import get_locale +from typing import List, Dict +from zeroconf import DNSEntry, DNSRecord from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf +# 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() + ) -async def register_services(aiozc: AsyncZeroconf, infos: List[AsyncServiceInfo]) -> None: - tasks = [aiozc.async_register_service(info) for info in infos] +# 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 addr show") + for d in re.split(r"^(?:[0-9]+: )", output, flags=re.MULTILINE): + # Extract device name (1) and its addresses (2) + m = re.match(r"([^\s@]+)(?:@[\S]+)?: (.*)", d, flags=re.DOTALL) + if m: + devices_raw[m.group(1)] = m.group(2) + + # Parse relevant informations for each of them + devices = { + name: _extract_inet(addrs) + for name, addrs in devices_raw.items() + if name != "lo" + } + + return devices + +# async commands +async def register_services(aiozcs: Dict[AsyncZeroconf, List[AsyncServiceInfo]]) -> None: + tasks = [] + for aiozc, infos in aiozcs.items(): + for info in infos: + tasks.append(aiozc.async_register_service(info)) background_tasks = await asyncio.gather(*tasks) await asyncio.gather(*background_tasks) -async def unregister_services(aiozc: AsyncZeroconf, infos: List[AsyncServiceInfo]) -> None: - tasks = [aiozc.async_unregister_service(info) for info in infos] +async def unregister_services(aiozcs: Dict[AsyncZeroconf, List[AsyncServiceInfo]]) -> None: + for aiozc, infos in aiozcs.items(): + for info in infos: + tasks.append(aiozc.async_unregister_service(info)) background_tasks = await asyncio.gather(*tasks) await asyncio.gather(*background_tasks) -async def close_aiozc(aiozc: AsyncZeroconf) -> None: - await aiozc.async_close() +async def close_aiozcs(aiozcs: Dict[AsyncZeroconf, List[AsyncServiceInfo]]) -> None: + tasks = [] + for aiozc in aiozcs: + tasks.append(aiozc.async_close()) + background_tasks = await asyncio.gather(*tasks) + await asyncio.gather(*background_tasks) if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) - parser = argparse.ArgumentParser() + + ### + # ARGUMENTS + ### + + parser = argparse.ArgumentParser(description=''' + mDNS broadcast for .local domains. + Configuration file: /etc/yunohost/mdns.yml + Subdomains are not supported. + ''') parser.add_argument('--debug', action='store_true') + parser.add_argument('--regen', nargs='?', const='as_stored', choices=['domains', 'interfaces', 'all', 'as_stored'], + help=''' + Regenerates selection into the configuration file then starts mDNS broadcasting. + ''') + parser.add_argument('--set-regen', choices=['domains', 'interfaces', 'all', 'none'], + help=''' + Set which part of the configuration to be regenerated. + Implies --regen as_stored, with newly stored parameter. + ''') + able = parser.add_mutually_exclusive_group() + able.add_argument('--enable', action='store_true', help='Enables mDNS broadcast, and regenerates the configuration') + able.add_argument('--disable', action='store_true') args = parser.parse_args() if args.debug: logging.getLogger('zeroconf').setLevel(logging.DEBUG) - - - local_domains = [ d for d in domain_list()['domains'] if d.endswith('.local') ] - - m18n.load_namespace("yunohost") - m18n.set_locale(get_locale()) - - if settings_get('mdns.interfaces'): - wanted_interfaces = settings_get('mdns.interfaces').split() + logging.getLogger('asyncio').setLevel(logging.DEBUG) else: - wanted_interfaces = [] - print('No interface listed for broadcast.') + logging.getLogger('zeroconf').setLevel(logging.WARNING) + logging.getLogger('asyncio').setLevel(logging.WARNING) - aiozcs = [] + ### + # CONFIG + ### + + with open('/etc/yunohost/mdns.yml', 'r') as f: + config = yaml.load(f) or {} + updated = False + + if args.enable: + config['enabled'] = True + args.regen = 'as_stored' + updated = True + + if args.disable: + config['enabled'] = False + updated = True + + if args.set_regen: + config['regen'] = args.set_regen + args.regen = 'as_stored' + updated = True + + if args.regen: + if args.regen == 'as_stored': + r = config['regen'] + else: + r = args.regen + if r == 'none': + print('Regeneration disabled.') + if r == 'interfaces' or r == 'all': + config['interfaces'] = [ i for i in get_network_interfaces() ] + print('Regenerated interfaces list: ' + str(config['interfaces'])) + if r == 'domains' or r == 'all': + import glob + config['domains'] = [ d.rsplit('/',1)[1][:-2] for d in glob.glob('/etc/nginx/conf.d/*.local.d') ] + print('Regenerated domains list: ' + str(config['domains'])) + updated = True + + if updated: + with open('/etc/yunohost/mdns.yml', 'w') as f: + yaml.safe_dump(config, f, default_flow_style=False) + print('Configuration file updated.') + + ### + # MAIN SCRIPT + ### + + if config['enabled'] is not True: + print('YunomDNS is disabled.') + sys.exit(0) + + if config['interfaces'] is None: + print('No interface listed for broadcast.') + sys.exit(0) + + aiozcs = {} + tasks = [] + loop = asyncio.get_event_loop() interfaces = get_network_interfaces() - for interface in wanted_interfaces: - infos = [] + for interface in config['interfaces']: + infos = [] # List of ServiceInfo objects, to feed Zeroconf ips = [] # Human-readable IPs b_ips = [] # Binary-convered IPs # Parse the IPs and prepare their binary version + addressed = False try: ip = interfaces[interface]['ipv4'].split('/')[0] + if len(ip)>0: addressed = True ips.append(ip) b_ips.append(socket.inet_pton(socket.AF_INET, ip)) except: pass try: ip = interfaces[interface]['ipv6'].split('/')[0] + if len(ip)>0: addressed = True ips.append(ip) b_ips.append(socket.inet_pton(socket.AF_INET6, ip)) except: pass - # Create a ServiceInfo object for each .local domain - for d in local_domains: - d_domain=d.replace('.local','') - infos.append( - AsyncServiceInfo( - type_="_device-info._tcp.local.", - name=d_domain+f"._device-info._tcp.local.", - addresses=b_ips, - port=80, - server=d+'.', + # If at least one IP is listed + if addressed: + # Create a ServiceInfo object for each .local domain + for d in config['domains']: + d_domain=d.replace('.local','') + infos.append( + AsyncServiceInfo( + type_='_device-info._tcp.local.', + name=d_domain+'._device-info._tcp.local.', + addresses=b_ips, + port=80, + server=d+'.', + ) ) - ) - print('Adding '+d+' with addresses '+str(ips)+' on interface '+interface) + infos.append( + AsyncServiceInfo( + type_='_http._tcp.local.', + name=d_domain+'._http._tcp.local.', + addresses=b_ips, + port=80, + server=d+'.', + ) + ) + print('Adding '+d+' with addresses '+str(ips)+' on interface '+interface) + # Create an AsyncZeroconf object, and store registration task + aiozc = AsyncZeroconf(interfaces=ips) + aiozcs[aiozc]=infos - # Create an AsyncZeroconf object, store it, and start Service registration - aiozc = AsyncZeroconf(interfaces=ips) - aiozcs.append(aiozc) - print("Registration on interface "+interface+"...") - loop = asyncio.get_event_loop() - loop.run_until_complete(register_services(aiozc, infos)) + # Run registration + loop.run_until_complete(register_services(aiozcs)) + print("Registration complete. Press Ctrl-c or stop service to exit...") - # We are done looping among the interfaces - print("Registration complete. Press Ctrl-c to exit...") try: while True: - time.sleep(0.1) + time.sleep(1) except KeyboardInterrupt: pass finally: print("Unregistering...") - for aiozc in aiozcs: - loop.run_until_complete(unregister_services(aiozc, infos)) - loop.run_until_complete(close_aiozc(aiozc)) + loop.run_until_complete(unregister_services(aiozcs)) print("Unregistration complete.") + loop.run_until_complete(close_aiozcs(aiozcs)) diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index 3d65d34cd..d160b9e66 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -3,6 +3,7 @@ set -e services_path="/etc/yunohost/services.yml" +mdns_path="/etc/yunohost/mdns.yml" do_init_regen() { if [[ $EUID -ne 0 ]]; then @@ -18,9 +19,11 @@ do_init_regen() { [[ -f /etc/yunohost/current_host ]] \ || echo "yunohost.org" > /etc/yunohost/current_host - # copy default services and firewall + # copy default services, mdns, and firewall [[ -f $services_path ]] \ || cp services.yml "$services_path" + [[ -f $mdns_path ]] \ + || cp mdns.yml "$mdns_path" [[ -f /etc/yunohost/firewall.yml ]] \ || cp firewall.yml /etc/yunohost/firewall.yml diff --git a/data/other/yunomdns.service b/data/other/yunomdns.service new file mode 100644 index 000000000..36d938035 --- /dev/null +++ b/data/other/yunomdns.service @@ -0,0 +1,13 @@ +[Unit] +Description=YunoHost mDNS service +After=network.target + +[Service] +User=avahi +Group=avahi +Type=simple +ExecStart=/usr/bin/yunomdns +StandardOutput=syslog + +[Install] +WantedBy=default.target diff --git a/data/templates/yunohost/mdns.yml b/data/templates/yunohost/mdns.yml new file mode 100644 index 000000000..3ed9e792b --- /dev/null +++ b/data/templates/yunohost/mdns.yml @@ -0,0 +1,4 @@ +enabled: True +regen: all +interfaces: +domains: diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index 7df563c67..b961d274e 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -52,6 +52,8 @@ yunohost-firewall: need_lock: true test_status: iptables -S | grep "^-A INPUT" | grep " --dport" | grep -q ACCEPT category: security +yunomdns: + needs_exposed_ports: [5353] glances: null nsswitch: null ssl: null diff --git a/debian/install b/debian/install index 1691a4849..e30a69a8b 100644 --- a/debian/install +++ b/debian/install @@ -5,6 +5,7 @@ doc/yunohost.8.gz /usr/share/man/man8/ data/actionsmap/* /usr/share/moulinette/actionsmap/ data/hooks/* /usr/share/yunohost/hooks/ data/other/yunoprompt.service /etc/systemd/system/ +data/other/yunomdns.service /etc/systemd/system/ data/other/password/* /usr/share/yunohost/other/password/ data/other/dpkg-origins/yunohost /etc/dpkg/origins data/other/dnsbl_list.yml /usr/share/yunohost/other/ diff --git a/debian/postinst b/debian/postinst index ecae9b258..7590197bd 100644 --- a/debian/postinst +++ b/debian/postinst @@ -38,6 +38,10 @@ do_configure() { # Yunoprompt systemctl enable yunoprompt.service + + # Yunomdns + chown avahi:avahi /etc/yunohost/mdns.yml + systemctl enable yunomdns.service } # summary of how this script can be called: From 842783f64c54bc0744d1692f5b1e4ea37e7c35ce Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 11 Jul 2021 15:20:14 +0000 Subject: [PATCH 06/33] Accomodate mDNS feature in diagnosis --- data/hooks/diagnosis/12-dnsrecords.py | 23 +++++++++++++++++++++-- data/hooks/diagnosis/21-web.py | 6 ++++++ data/templates/yunohost/services.yml | 1 + locales/en.json | 4 +++- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index 719ce4d6a..89816847d 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -29,8 +29,9 @@ class DNSRecordsDiagnoser(Diagnoser): for domain in all_domains: self.logger_debug("Diagnosing DNS conf for %s" % domain) is_subdomain = domain.split(".", 1)[1] in all_domains + is_localdomain = domain.endswith(".local") for report in self.check_domain( - domain, domain == main_domain, is_subdomain=is_subdomain + domain, domain == main_domain, is_subdomain=is_subdomain, is_localdomain=is_localdomain ): yield report @@ -48,7 +49,7 @@ class DNSRecordsDiagnoser(Diagnoser): for report in self.check_expiration_date(domains_from_registrar): yield report - def check_domain(self, domain, is_main_domain, is_subdomain): + def check_domain(self, domain, is_main_domain, is_subdomain, is_localdomain): expected_configuration = _build_dns_conf( domain, include_empty_AAAA_if_no_ipv6=True @@ -59,6 +60,24 @@ class DNSRecordsDiagnoser(Diagnoser): if is_subdomain: categories = ["basic"] + if is_localdomain: + categories = [] + if is_subdomain: + yield dict( + meta={"domain": domain, "category": "basic"}, + results={}, + status="WARNING", + summary="diagnosis_domain_subdomain_localdomain", + ) + else: + yield dict( + meta={"domain": domain, "category": "basic"}, + results={}, + status="INFO", + summary="diagnosis_domain_localdomain", + ) + + for category in categories: records = expected_configuration[category] diff --git a/data/hooks/diagnosis/21-web.py b/data/hooks/diagnosis/21-web.py index 81c4d6e48..04c36661e 100644 --- a/data/hooks/diagnosis/21-web.py +++ b/data/hooks/diagnosis/21-web.py @@ -34,6 +34,12 @@ class WebDiagnoser(Diagnoser): summary="diagnosis_http_nginx_conf_not_up_to_date", details=["diagnosis_http_nginx_conf_not_up_to_date_details"], ) + elif domain.endswith('.local'): + yield dict( + meta={"domain": domain}, + status="INFO", + summary="diagnosis_http_localdomain", + ) else: domains_to_check.append(domain) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index b961d274e..447829684 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -54,6 +54,7 @@ yunohost-firewall: category: security yunomdns: needs_exposed_ports: [5353] + category: mdns glances: null nsswitch: null ssl: null diff --git a/locales/en.json b/locales/en.json index 70a0e9309..3734b7cf3 100644 --- a/locales/en.json +++ b/locales/en.json @@ -190,6 +190,8 @@ "diagnosis_domain_expiration_warning": "Some domains will expire soon!", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", "diagnosis_domain_expires_in": "{domain} expires in {days} days.", + "diagnosis_domain_localdomain": "Domain {domain}, with a .local TLD, is not expected to have DNS records as it can be discovered through mDNS.", + "diagnosis_domain_subdomain_localdomain": "Domain {domain} is a subdomain of a .local domain. Zeroconf/mDNS discovery only works with first-level domains.", "diagnosis_services_running": "Service {service} is running!", "diagnosis_services_conf_broken": "Configuration is broken for service {service}!", "diagnosis_services_bad_status": "Service {service} is {status} :(", @@ -259,6 +261,7 @@ "diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at https://yunohost.org/dns_local_network", "diagnosis_http_could_not_diagnose": "Could not diagnose if domains are reachable from outside in IPv{ipversion}.", "diagnosis_http_could_not_diagnose_details": "Error: {error}", + "diagnosis_http_localdomain": "Domain {domain}, with a .local TLD, is not expected to be reached from outside the local network.", "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.", "diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", "diagnosis_http_connection_error": "Connection error: could not connect to the requested domain, it's very likely unreachable.", @@ -321,7 +324,6 @@ "global_settings_cant_write_settings": "Could not save settings file, reason: {reason:s}", "global_settings_key_doesnt_exists": "The key '{settings_key:s}' does not exist in the global settings, you can see all the available keys by running 'yunohost settings list'", "global_settings_reset_success": "Previous settings now backed up to {path:s}", - "global_settings_setting_mdns_interfaces": "Space-separated list of interfaces for mDNS broadcast. Leave empty to disable mDNS.", "global_settings_setting_pop3_enabled": "Enable the POP3 protocol for the mail server", "global_settings_setting_security_nginx_compatibility": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", "global_settings_setting_security_password_admin_strength": "Admin password strength", From f14dcec711f1856373456033134ba4ffe019c13e Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 11 Jul 2021 15:20:35 +0000 Subject: [PATCH 07/33] Remove mDNS setting now we use our own setting file, not a system setting --- src/yunohost/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 36904ee70..0466d8126 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -100,7 +100,6 @@ DEFAULTS = OrderedDict( ("smtp.relay.password", {"type": "string", "default": ""}), ("backup.compress_tar_archives", {"type": "bool", "default": False}), ("ssowat.panel_overlay.enabled", {"type": "bool", "default": True}), - ("mdns.interfaces", {"type": "string", "default": ""}), ] ) From 92a1a12226d6caa5becb502bc2c0c690da4c199c Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 11 Jul 2021 17:32:33 +0000 Subject: [PATCH 08/33] [mdns] Fix NonUniqueNameException Service names have to be unique even accross different interfaces --- bin/yunomdns | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index 795481dfa..ad1c1a012 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -246,7 +246,7 @@ if __name__ == '__main__': infos.append( AsyncServiceInfo( type_='_device-info._tcp.local.', - name=d_domain+'._device-info._tcp.local.', + name=interface+' '+d_domain+'._device-info._tcp.local.', addresses=b_ips, port=80, server=d+'.', @@ -255,7 +255,7 @@ if __name__ == '__main__': infos.append( AsyncServiceInfo( type_='_http._tcp.local.', - name=d_domain+'._http._tcp.local.', + name=interface+' '+d_domain+'._http._tcp.local.', addresses=b_ips, port=80, server=d+'.', From 7c58aa198c1096f4ce27993f7f6a05f628445dae Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 27 Jul 2021 20:38:02 +0000 Subject: [PATCH 09/33] Remove asyncio and do not support subdomains --- bin/yunomdns | 88 +++++++++++++++++----------------------------------- 1 file changed, 28 insertions(+), 60 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index ad1c1a012..123935871 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -16,11 +16,10 @@ import yaml import asyncio import logging import socket -import time +from time import sleep from typing import List, Dict -from zeroconf import DNSEntry, DNSRecord -from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf +from zeroconf import Zeroconf, ServiceInfo # Helper command taken from Moulinette def check_output(args, stderr=subprocess.STDOUT, shell=True, **kwargs): @@ -98,30 +97,6 @@ def get_network_interfaces(): return devices -# async commands -async def register_services(aiozcs: Dict[AsyncZeroconf, List[AsyncServiceInfo]]) -> None: - tasks = [] - for aiozc, infos in aiozcs.items(): - for info in infos: - tasks.append(aiozc.async_register_service(info)) - background_tasks = await asyncio.gather(*tasks) - await asyncio.gather(*background_tasks) - -async def unregister_services(aiozcs: Dict[AsyncZeroconf, List[AsyncServiceInfo]]) -> None: - for aiozc, infos in aiozcs.items(): - for info in infos: - tasks.append(aiozc.async_unregister_service(info)) - background_tasks = await asyncio.gather(*tasks) - await asyncio.gather(*background_tasks) - -async def close_aiozcs(aiozcs: Dict[AsyncZeroconf, List[AsyncServiceInfo]]) -> None: - tasks = [] - for aiozc in aiozcs: - tasks.append(aiozc.async_close()) - background_tasks = await asyncio.gather(*tasks) - await asyncio.gather(*background_tasks) - - if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) @@ -212,9 +187,7 @@ if __name__ == '__main__': print('No interface listed for broadcast.') sys.exit(0) - aiozcs = {} - tasks = [] - loop = asyncio.get_event_loop() + zcs = {} interfaces = get_network_interfaces() for interface in config['interfaces']: infos = [] # List of ServiceInfo objects, to feed Zeroconf @@ -240,44 +213,39 @@ if __name__ == '__main__': # If at least one IP is listed if addressed: - # Create a ServiceInfo object for each .local domain + # Create a Zeroconf object, and store the ServiceInfos + zc = Zeroconf(interfaces=ips) + zcs[zc]=[] for d in config['domains']: d_domain=d.replace('.local','') - infos.append( - AsyncServiceInfo( - type_='_device-info._tcp.local.', - name=interface+' '+d_domain+'._device-info._tcp.local.', - addresses=b_ips, - port=80, - server=d+'.', - ) - ) - infos.append( - AsyncServiceInfo( - type_='_http._tcp.local.', - name=interface+' '+d_domain+'._http._tcp.local.', - addresses=b_ips, - port=80, - server=d+'.', - ) - ) - print('Adding '+d+' with addresses '+str(ips)+' on interface '+interface) - # Create an AsyncZeroconf object, and store registration task - aiozc = AsyncZeroconf(interfaces=ips) - aiozcs[aiozc]=infos + 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 - loop.run_until_complete(register_services(aiozcs)) - print("Registration complete. Press Ctrl-c or stop service to exit...") + 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: - time.sleep(1) + sleep(1) except KeyboardInterrupt: pass finally: print("Unregistering...") - loop.run_until_complete(unregister_services(aiozcs)) - print("Unregistration complete.") - loop.run_until_complete(close_aiozcs(aiozcs)) - + for zc, infos in zcs.items(): + for info in infos: + zc.unregister_service(info) + zc.close() From 3535b374aed13faf1e1ce72b1830fab12b09e920 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 11 Aug 2021 16:19:51 +0000 Subject: [PATCH 10/33] [mdns] Always broadcast yunohost.local --- bin/yunomdns | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/yunomdns b/bin/yunomdns index 123935871..bc4e4c0e1 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -167,6 +167,8 @@ if __name__ == '__main__': if r == 'domains' or r == 'all': import glob config['domains'] = [ d.rsplit('/',1)[1][:-2] for d in glob.glob('/etc/nginx/conf.d/*.local.d') ] + if 'yunohost.local' not in config['domains']: + config['domains'].append('yunohost.local') print('Regenerated domains list: ' + str(config['domains'])) updated = True From cad5c39dae9b89c6f815250b6ed24351d7ae40fc Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 11 Aug 2021 16:21:32 +0000 Subject: [PATCH 11/33] [mdns] Remove logging and asyncio dependencies --- bin/yunomdns | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index bc4e4c0e1..4f698a0ac 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -3,7 +3,6 @@ """ WIP Pythonic declaration of mDNS .local domains for YunoHost -Based off https://github.com/jstasiak/python-zeroconf/blob/master/tests/test_asyncio.py """ import subprocess @@ -13,8 +12,6 @@ import sys import argparse import yaml -import asyncio -import logging import socket from time import sleep from typing import List, Dict @@ -98,7 +95,6 @@ def get_network_interfaces(): return devices if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG) ### @@ -110,7 +106,6 @@ if __name__ == '__main__': Configuration file: /etc/yunohost/mdns.yml Subdomains are not supported. ''') - parser.add_argument('--debug', action='store_true') parser.add_argument('--regen', nargs='?', const='as_stored', choices=['domains', 'interfaces', 'all', 'as_stored'], help=''' Regenerates selection into the configuration file then starts mDNS broadcasting. @@ -125,13 +120,6 @@ if __name__ == '__main__': able.add_argument('--disable', action='store_true') args = parser.parse_args() - if args.debug: - logging.getLogger('zeroconf').setLevel(logging.DEBUG) - logging.getLogger('asyncio').setLevel(logging.DEBUG) - else: - logging.getLogger('zeroconf').setLevel(logging.WARNING) - logging.getLogger('asyncio').setLevel(logging.WARNING) - ### # CONFIG ### From f53f36e332fb40c89a9467b9e558dab828da2df9 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 11 Aug 2021 17:58:50 +0000 Subject: [PATCH 12/33] [mdns] Fix yunohost.local broadcast --- bin/yunomdns | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index 4f698a0ac..7280aebc8 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -155,8 +155,6 @@ if __name__ == '__main__': if r == 'domains' or r == 'all': import glob config['domains'] = [ d.rsplit('/',1)[1][:-2] for d in glob.glob('/etc/nginx/conf.d/*.local.d') ] - if 'yunohost.local' not in config['domains']: - config['domains'].append('yunohost.local') print('Regenerated domains list: ' + str(config['domains'])) updated = True @@ -177,6 +175,9 @@ if __name__ == '__main__': 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']: From 9bef3105f1c2992bdac88c678ee30918a80024ed Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 11 Aug 2021 20:13:51 +0200 Subject: [PATCH 13/33] Add dependency to python3-zeroconf --- debian/control | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/control b/debian/control index ef5061fe7..cabff028b 100644 --- a/debian/control +++ b/debian/control @@ -13,7 +13,8 @@ Depends: ${python3:Depends}, ${misc:Depends} , moulinette (>= 4.2), ssowat (>= 4.0) , python3-psutil, python3-requests, python3-dnspython, python3-openssl , python3-miniupnpc, python3-dbus, python3-jinja2 - , python3-toml, python3-packaging, python3-publicsuffix + , python3-toml, python3-packaging, python3-publicsuffix, + , python3-zeroconf, , apt, apt-transport-https, apt-utils, dirmngr , php7.3-common, php7.3-fpm, php7.3-ldap, php7.3-intl , mariadb-server, php7.3-mysql From a2a50d0e453164510640508c02958f21bfd20b05 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 11 Aug 2021 20:29:29 +0200 Subject: [PATCH 14/33] mdns: Remove argument parsing because we won't really need this ? :Z --- bin/yunomdns | 71 +++------------------------------------------------- 1 file changed, 4 insertions(+), 67 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index 7280aebc8..3e3eea72f 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -6,10 +6,8 @@ Pythonic declaration of mDNS .local domains for YunoHost """ import subprocess -import os import re import sys -import argparse import yaml import socket @@ -96,30 +94,6 @@ def get_network_interfaces(): if __name__ == '__main__': - - ### - # ARGUMENTS - ### - - parser = argparse.ArgumentParser(description=''' - mDNS broadcast for .local domains. - Configuration file: /etc/yunohost/mdns.yml - Subdomains are not supported. - ''') - parser.add_argument('--regen', nargs='?', const='as_stored', choices=['domains', 'interfaces', 'all', 'as_stored'], - help=''' - Regenerates selection into the configuration file then starts mDNS broadcasting. - ''') - parser.add_argument('--set-regen', choices=['domains', 'interfaces', 'all', 'none'], - help=''' - Set which part of the configuration to be regenerated. - Implies --regen as_stored, with newly stored parameter. - ''') - able = parser.add_mutually_exclusive_group() - able.add_argument('--enable', action='store_true', help='Enables mDNS broadcast, and regenerates the configuration') - able.add_argument('--disable', action='store_true') - args = parser.parse_args() - ### # CONFIG ### @@ -128,48 +102,11 @@ if __name__ == '__main__': config = yaml.load(f) or {} updated = False - if args.enable: - config['enabled'] = True - args.regen = 'as_stored' - updated = True + required_fields = ["interfaces", "domains"] + missing_fields = [field for field in required_fields if field not in config] - if args.disable: - config['enabled'] = False - updated = True - - if args.set_regen: - config['regen'] = args.set_regen - args.regen = 'as_stored' - updated = True - - if args.regen: - if args.regen == 'as_stored': - r = config['regen'] - else: - r = args.regen - if r == 'none': - print('Regeneration disabled.') - if r == 'interfaces' or r == 'all': - config['interfaces'] = [ i for i in get_network_interfaces() ] - print('Regenerated interfaces list: ' + str(config['interfaces'])) - if r == 'domains' or r == 'all': - import glob - config['domains'] = [ d.rsplit('/',1)[1][:-2] for d in glob.glob('/etc/nginx/conf.d/*.local.d') ] - print('Regenerated domains list: ' + str(config['domains'])) - updated = True - - if updated: - with open('/etc/yunohost/mdns.yml', 'w') as f: - yaml.safe_dump(config, f, default_flow_style=False) - print('Configuration file updated.') - - ### - # MAIN SCRIPT - ### - - if config['enabled'] is not True: - print('YunomDNS is disabled.') - sys.exit(0) + if missing_fields: + print("The fields %s are required" % ', '.join(missing_fields)) if config['interfaces'] is None: print('No interface listed for broadcast.') From fe2e014b5667c48f9f983e0c17f7b1e1db884699 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Aug 2021 15:26:39 +0200 Subject: [PATCH 15/33] mdns: Rework mdns's conf handling such that it's generated by the regen-conf. Also drop avahi-daemon because not needed anymore. --- data/hooks/conf_regen/01-yunohost | 5 +- data/hooks/conf_regen/37-avahi-daemon | 37 --------- data/hooks/conf_regen/37-mdns | 75 +++++++++++++++++++ data/templates/avahi-daemon/avahi-daemon.conf | 68 ----------------- .../mdns}/yunomdns.service | 0 data/templates/yunohost/mdns.yml | 4 - data/templates/yunohost/services.yml | 2 +- debian/control | 2 +- debian/install | 1 - debian/postinst | 4 - 10 files changed, 78 insertions(+), 120 deletions(-) delete mode 100755 data/hooks/conf_regen/37-avahi-daemon create mode 100755 data/hooks/conf_regen/37-mdns delete mode 100644 data/templates/avahi-daemon/avahi-daemon.conf rename data/{other => templates/mdns}/yunomdns.service (100%) delete mode 100644 data/templates/yunohost/mdns.yml diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index d160b9e66..3d65d34cd 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -3,7 +3,6 @@ set -e services_path="/etc/yunohost/services.yml" -mdns_path="/etc/yunohost/mdns.yml" do_init_regen() { if [[ $EUID -ne 0 ]]; then @@ -19,11 +18,9 @@ do_init_regen() { [[ -f /etc/yunohost/current_host ]] \ || echo "yunohost.org" > /etc/yunohost/current_host - # copy default services, mdns, and firewall + # copy default services and firewall [[ -f $services_path ]] \ || cp services.yml "$services_path" - [[ -f $mdns_path ]] \ - || cp mdns.yml "$mdns_path" [[ -f /etc/yunohost/firewall.yml ]] \ || cp firewall.yml /etc/yunohost/firewall.yml diff --git a/data/hooks/conf_regen/37-avahi-daemon b/data/hooks/conf_regen/37-avahi-daemon deleted file mode 100755 index 4127d66ca..000000000 --- a/data/hooks/conf_regen/37-avahi-daemon +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -set -e - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/templates/avahi-daemon - - install -D -m 644 avahi-daemon.conf \ - "${pending_dir}/etc/avahi/avahi-daemon.conf" -} - -do_post_regen() { - regen_conf_files=$1 - - [[ -z "$regen_conf_files" ]] \ - || systemctl restart avahi-daemon -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/37-mdns b/data/hooks/conf_regen/37-mdns new file mode 100755 index 000000000..903b41a0f --- /dev/null +++ b/data/hooks/conf_regen/37-mdns @@ -0,0 +1,75 @@ +#!/bin/bash + +set -e + +_generate_config() { + echo "domains:" + echo " - yunohost.local" + for domain in $YNH_DOMAINS + do + # Only keep .local domains (don't keep + [[ "$domain" =~ [^.]+\.[^.]+\.local$ ]] && echo "Subdomain $domain cannot be handled by Bonjour/Zeroconf/mDNS" >&2 + [[ "$domain" =~ ^[^.]+\.local$ ]] || continue + echo " - $domain" + done + + echo "interfaces:" + local_network_interfaces="$(ip --brief a | grep ' 10\.\| 192\.168\.' | awk '{print $1}')" + for interface in $local_network_interfaces + do + echo " - $interface" + done +} + +do_init_regen() { + do_pre_regen + do_post_regen /etc/systemd/system/yunomdns.service + systemctl enable yunomdns +} + +do_pre_regen() { + pending_dir="$1" + + cd /usr/share/yunohost/templates/dnsmasq + cp yunomdns.service ${pending_dir}/etc/systemd/system/ + + getent passwd mdns &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group mdns + + _generate_config > ${pending_dir}/etc/yunohost/mdns.yml +} + +do_post_regen() { + regen_conf_files="$1" + + chown mdns:mdns ${pending_dir}/etc/yunohost/mdns.yml + + # If we changed the systemd ynh-override conf + if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/yunomdns.service$" + then + systemctl daemon-reload + fi + + [[ -z "$regen_conf_files" ]] \ + || systemctl restart yunomdns +} + +FORCE=${2:-0} +DRY_RUN=${3:-0} + +case "$1" in + pre) + do_pre_regen $4 + ;; + post) + do_post_regen $4 + ;; + init) + do_init_regen + ;; + *) + echo "hook called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/data/templates/avahi-daemon/avahi-daemon.conf b/data/templates/avahi-daemon/avahi-daemon.conf deleted file mode 100644 index d3542a411..000000000 --- a/data/templates/avahi-daemon/avahi-daemon.conf +++ /dev/null @@ -1,68 +0,0 @@ -# This file is part of avahi. -# -# avahi is free software; you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as -# published by the Free Software Foundation; either version 2 of the -# License, or (at your option) any later version. -# -# avahi is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public -# License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with avahi; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 -# USA. - -# See avahi-daemon.conf(5) for more information on this configuration -# file! - -[server] -host-name=yunohost -domain-name=local -#browse-domains=0pointer.de, zeroconf.org -use-ipv4=yes -use-ipv6=yes -#allow-interfaces=eth0 -#deny-interfaces=eth1 -#check-response-ttl=no -#use-iff-running=no -#enable-dbus=yes -#disallow-other-stacks=no -#allow-point-to-point=no -#cache-entries-max=4096 -#clients-max=4096 -#objects-per-client-max=1024 -#entries-per-entry-group-max=32 -ratelimit-interval-usec=1000000 -ratelimit-burst=1000 - -[wide-area] -enable-wide-area=yes - -[publish] -#disable-publishing=no -#disable-user-service-publishing=no -#add-service-cookie=no -#publish-addresses=yes -#publish-hinfo=yes -#publish-workstation=yes -#publish-domain=yes -#publish-dns-servers=192.168.50.1, 192.168.50.2 -#publish-resolv-conf-dns-servers=yes -#publish-aaaa-on-ipv4=yes -#publish-a-on-ipv6=no - -[reflector] -#enable-reflector=no -#reflect-ipv=no - -[rlimits] -#rlimit-as= -rlimit-core=0 -rlimit-data=4194304 -rlimit-fsize=0 -rlimit-nofile=768 -rlimit-stack=4194304 -rlimit-nproc=3 diff --git a/data/other/yunomdns.service b/data/templates/mdns/yunomdns.service similarity index 100% rename from data/other/yunomdns.service rename to data/templates/mdns/yunomdns.service diff --git a/data/templates/yunohost/mdns.yml b/data/templates/yunohost/mdns.yml deleted file mode 100644 index 3ed9e792b..000000000 --- a/data/templates/yunohost/mdns.yml +++ /dev/null @@ -1,4 +0,0 @@ -enabled: True -regen: all -interfaces: -domains: diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index 447829684..c7690fc9c 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -1,4 +1,3 @@ -avahi-daemon: {} dnsmasq: test_conf: dnsmasq --test dovecot: @@ -71,3 +70,4 @@ rmilter: null php5-fpm: null php7.0-fpm: null nslcd: null +avahi-daemon: null diff --git a/debian/control b/debian/control index cabff028b..c9306bef1 100644 --- a/debian/control +++ b/debian/control @@ -21,7 +21,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , openssh-server, iptables, fail2ban, dnsutils, bind9utils , openssl, ca-certificates, netcat-openbsd, iproute2 , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd - , dnsmasq, avahi-daemon, libnss-mdns, resolvconf, libnss-myhostname + , dnsmasq, libnss-mdns, resolvconf, libnss-myhostname , postfix, postfix-ldap, postfix-policyd-spf-perl, postfix-pcre , dovecot-core, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved, dovecot-antispam , rspamd, opendkim-tools, postsrsd, procmail, mailutils diff --git a/debian/install b/debian/install index e30a69a8b..1691a4849 100644 --- a/debian/install +++ b/debian/install @@ -5,7 +5,6 @@ doc/yunohost.8.gz /usr/share/man/man8/ data/actionsmap/* /usr/share/moulinette/actionsmap/ data/hooks/* /usr/share/yunohost/hooks/ data/other/yunoprompt.service /etc/systemd/system/ -data/other/yunomdns.service /etc/systemd/system/ data/other/password/* /usr/share/yunohost/other/password/ data/other/dpkg-origins/yunohost /etc/dpkg/origins data/other/dnsbl_list.yml /usr/share/yunohost/other/ diff --git a/debian/postinst b/debian/postinst index 7590197bd..ecae9b258 100644 --- a/debian/postinst +++ b/debian/postinst @@ -38,10 +38,6 @@ do_configure() { # Yunoprompt systemctl enable yunoprompt.service - - # Yunomdns - chown avahi:avahi /etc/yunohost/mdns.yml - systemctl enable yunomdns.service } # summary of how this script can be called: From 8dd65f78c8cc2e3631c2836b7596a238a8b654b4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Aug 2021 15:27:19 +0200 Subject: [PATCH 16/33] mdns: Propagate new service name to i18n strings --- locales/ar.json | 2 +- locales/ca.json | 2 +- locales/de.json | 2 +- locales/en.json | 2 +- locales/eo.json | 2 +- locales/es.json | 2 +- locales/fr.json | 2 +- locales/it.json | 2 +- locales/oc.json | 2 +- locales/zh_Hans.json | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 06e444f4a..6a057bdc8 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -83,7 +83,7 @@ "yunohost_installing": "عملية تنصيب يونوهوست جارية …", "yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب أو هو مثبت حاليا بشكل خاطئ. قم بتنفيذ الأمر 'yunohost tools postinstall'", "migrations_list_conflict_pending_done": "لا يمكنك استخدام --previous و --done معًا على نفس سطر الأوامر.", - "service_description_avahi-daemon": "يسمح لك بالنفاذ إلى خادومك عبر الشبكة المحلية باستخدام yunohost.local", + "service_description_mdns": "يسمح لك بالنفاذ إلى خادومك عبر الشبكة المحلية باستخدام yunohost.local", "service_description_metronome": "يُدير حسابات الدردشة الفورية XMPP", "service_description_nginx": "يقوم بتوفير النفاذ و السماح بالوصول إلى كافة مواقع الويب المستضافة على خادومك", "service_description_postfix": "يقوم بإرسال و تلقي الرسائل البريدية الإلكترونية", diff --git a/locales/ca.json b/locales/ca.json index 1e4c55f5d..c9f71c0ad 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -283,7 +283,7 @@ "service_already_started": "El servei «{service:s}» ja està funcionant", "service_already_stopped": "Ja s'ha aturat el servei «{service:s}»", "service_cmd_exec_failed": "No s'ha pogut executar l'ordre «{command:s}»", - "service_description_avahi-daemon": "Permet accedir al servidor via «yunohost.local» en la xarxa local", + "service_description_mdns": "Permet accedir al servidor via «yunohost.local» en la xarxa local", "service_description_dnsmasq": "Gestiona la resolució del nom de domini (DNS)", "service_description_dovecot": "Permet als clients de correu accedir/recuperar correus (via IMAP i POP3)", "service_description_fail2ban": "Protegeix contra els atacs de força bruta i a altres atacs provinents d'Internet", diff --git a/locales/de.json b/locales/de.json index 83647ec17..0760dc775 100644 --- a/locales/de.json +++ b/locales/de.json @@ -597,7 +597,7 @@ "service_description_fail2ban": "Schützt gegen Brute-Force-Angriffe und andere Angriffe aus dem Internet", "service_description_dovecot": "Ermöglicht es E-Mail-Clients auf Konten zuzugreifen (IMAP und POP3)", "service_description_dnsmasq": "Verarbeitet die Auflösung des Domainnamens (DNS)", - "service_description_avahi-daemon": "Erlaubt, den Server im lokalen Netz über 'yunohost.local' zu erreichen", + "service_description_mdns": "Erlaubt, den Server im lokalen Netz über 'yunohost.local' zu erreichen", "restore_backup_too_old": "Dieses Backup kann nicht wieder hergestellt werden, weil es von einer zu alten YunoHost Version stammt.", "service_description_slapd": "Speichert Benutzer, Domains und verbundene Informationen", "service_description_rspamd": "Spamfilter und andere E-Mail-Merkmale", diff --git a/locales/en.json b/locales/en.json index 3734b7cf3..5c73cba8b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -558,7 +558,7 @@ "service_already_started": "The service '{service:s}' is running already", "service_already_stopped": "The service '{service:s}' has already been stopped", "service_cmd_exec_failed": "Could not execute the command '{command:s}'", - "service_description_avahi-daemon": "Allows you to reach your server using 'yunohost.local' in your local network", + "service_description_mdns": "Allows you to reach your server using 'yunohost.local' in your local network", "service_description_dnsmasq": "Handles domain name resolution (DNS)", "service_description_dovecot": "Allows e-mail clients to access/fetch email (via IMAP and POP3)", "service_description_fail2ban": "Protects against brute-force and other kinds of attacks from the Internet", diff --git a/locales/eo.json b/locales/eo.json index d273593a9..b4d53f0f1 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -332,7 +332,7 @@ "hook_exec_failed": "Ne povis funkcii skripto: {path:s}", "global_settings_cant_open_settings": "Ne eblis malfermi agordojn, tial: {reason:s}", "user_created": "Uzanto kreita", - "service_description_avahi-daemon": "Permesas al vi atingi vian servilon uzante 'yunohost.local' en via loka reto", + "service_description_mdns": "Permesas al vi atingi vian servilon uzante 'yunohost.local' en via loka reto", "certmanager_attempt_to_replace_valid_cert": "Vi provas anstataŭigi bonan kaj validan atestilon por domajno {domain:s}! (Uzu --forte pretervidi)", "regenconf_updated": "Agordo ĝisdatigita por '{category}'", "update_apt_cache_warning": "Io iris malbone dum la ĝisdatigo de la kaŝmemoro de APT (paka administranto de Debian). Jen rubujo de la sources.list-linioj, kiuj povus helpi identigi problemajn liniojn:\n{sourceslist}", diff --git a/locales/es.json b/locales/es.json index f95451922..f2b8063cf 100644 --- a/locales/es.json +++ b/locales/es.json @@ -238,7 +238,7 @@ "service_description_fail2ban": "Protege contra ataques de fuerza bruta y otras clases de ataques desde Internet", "service_description_dovecot": "Permite a los clientes de correo acceder/obtener correo (vía IMAP y POP3)", "service_description_dnsmasq": "Maneja la resolución de nombres de dominio (DNS)", - "service_description_avahi-daemon": "Permite acceder a su servidor usando «yunohost.local» en su red local", + "service_description_mdns": "Permite acceder a su servidor usando «yunohost.local» en su red local", "server_reboot_confirm": "El servidor se reiniciará inmediatamente ¿está seguro? [{answers:s}]", "server_reboot": "El servidor se reiniciará", "server_shutdown_confirm": "El servidor se apagará inmediatamente ¿está seguro? [{answers:s}]", diff --git a/locales/fr.json b/locales/fr.json index f06acf2e5..565cc8032 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -232,7 +232,7 @@ "migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", "migrations_to_be_ran_manually": "La migration {id} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans l’interface admin, ou lancer `yunohost tools migrations run`.", "migrations_need_to_accept_disclaimer": "Pour lancer la migration {id}, vous devez accepter cet avertissement :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l’option --accept-disclaimer.", - "service_description_avahi-daemon": "Vous permet d’atteindre votre serveur en utilisant « yunohost.local » sur votre réseau local", + "service_description_mdns": "Vous permet d’atteindre votre serveur en utilisant « yunohost.local » sur votre réseau local", "service_description_dnsmasq": "Gère la résolution des noms de domaine (DNS)", "service_description_dovecot": "Permet aux clients de messagerie d’accéder/récupérer les courriels (via IMAP et POP3)", "service_description_fail2ban": "Protège contre les attaques brute-force et autres types d’attaques venant d’Internet", diff --git a/locales/it.json b/locales/it.json index 707f3afc2..3444b846d 100644 --- a/locales/it.json +++ b/locales/it.json @@ -428,7 +428,7 @@ "service_description_fail2ban": "Ti protegge dal brute-force e altri tipi di attacchi da Internet", "service_description_dovecot": "Consente ai client mail di accedere/recuperare le email (via IMAP e POP3)", "service_description_dnsmasq": "Gestisce la risoluzione dei domini (DNS)", - "service_description_avahi-daemon": "Consente di raggiungere il tuo server eseguendo 'yunohost.local' sulla tua LAN", + "service_description_mdns": "Consente di raggiungere il tuo server eseguendo 'yunohost.local' sulla tua LAN", "server_reboot_confirm": "Il server si riavvierà immediatamente, sei sicuro? [{answers:s}]", "server_reboot": "Il server si riavvierà", "server_shutdown_confirm": "Il server si spegnerà immediatamente, sei sicuro? [{answers:s}]", diff --git a/locales/oc.json b/locales/oc.json index 991383bc3..b5bd9474c 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -193,7 +193,7 @@ "user_unknown": "Utilizaire « {user:s} » desconegut", "user_update_failed": "Modificacion impossibla de l’utilizaire", "user_updated": "L’utilizaire es estat modificat", - "service_description_avahi-daemon": "permet d’aténher vòstre servidor via yunohost.local sus vòstre ret local", + "service_description_mdns": "permet d’aténher vòstre servidor via yunohost.local sus vòstre ret local", "service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)", "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles…", "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ? {answers:s}", diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index 78ba55133..0e971299f 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -226,7 +226,7 @@ "service_description_fail2ban": "防止来自互联网的暴力攻击和其他类型的攻击", "service_description_dovecot": "允许电子邮件客户端访问/获取电子邮件(通过IMAP和POP3)", "service_description_dnsmasq": "处理域名解析(DNS)", - "service_description_avahi-daemon": "允许您使用本地网络中的“ yunohost.local”访问服务器", + "service_description_mdns": "允许您使用本地网络中的“ yunohost.local”访问服务器", "service_started": "服务 '{service:s}' 已启动", "service_start_failed": "无法启动服务 '{service:s}'\n\n最近的服务日志:{logs:s}", "service_reloaded_or_restarted": "服务'{service:s}'已重新加载或重新启动", From ffc132f2c5192e393275250bb1073bd2ee499c7f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Aug 2021 16:59:54 +0200 Subject: [PATCH 17/33] Configure mdns during yunohost's debian postinst init --- debian/postinst | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/postinst b/debian/postinst index ecae9b258..8fc00bbe2 100644 --- a/debian/postinst +++ b/debian/postinst @@ -18,6 +18,7 @@ do_configure() { bash /usr/share/yunohost/hooks/conf_regen/46-nsswitch init bash /usr/share/yunohost/hooks/conf_regen/06-slapd init bash /usr/share/yunohost/hooks/conf_regen/15-nginx init + bash /usr/share/yunohost/hooks/conf_regen/37-mdns init fi else echo "Regenerating configuration, this might take a while..." From 4d0581bef21865903202a8cd911fd7dec0db7285 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Aug 2021 17:19:45 +0200 Subject: [PATCH 18/33] mdns: Misc fixes after tests on the battefield --- data/hooks/conf_regen/37-mdns | 6 ++++-- src/yunohost/domain.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/data/hooks/conf_regen/37-mdns b/data/hooks/conf_regen/37-mdns index 903b41a0f..c85e43a9a 100755 --- a/data/hooks/conf_regen/37-mdns +++ b/data/hooks/conf_regen/37-mdns @@ -30,18 +30,20 @@ do_init_regen() { do_pre_regen() { pending_dir="$1" - cd /usr/share/yunohost/templates/dnsmasq + cd /usr/share/yunohost/templates/mdns + mkdir -p ${pending_dir}/etc/systemd/system/ cp yunomdns.service ${pending_dir}/etc/systemd/system/ getent passwd mdns &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group mdns + mkdir -p ${pending_dir}/etc/yunohost _generate_config > ${pending_dir}/etc/yunohost/mdns.yml } do_post_regen() { regen_conf_files="$1" - chown mdns:mdns ${pending_dir}/etc/yunohost/mdns.yml + chown mdns:mdns /etc/yunohost/mdns.yml # If we changed the systemd ynh-override conf if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/yunomdns.service$" diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index aaac3a995..bca701dc6 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -163,7 +163,7 @@ def domain_add(operation_logger, domain, dyndns=False): # because it's one of the major service, but in the long term we # should identify the root of this bug... _force_clear_hashes(["/etc/nginx/conf.d/%s.conf" % domain]) - regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd"]) + regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"]) app_ssowatconf() except Exception as e: @@ -290,7 +290,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): "/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True ) - regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix"]) + regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"]) app_ssowatconf() hook_callback("post_domain_remove", args=[domain]) From a343490f304d9a3a0a8cc1e64e73a7eb83064fbe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Aug 2021 17:22:20 +0200 Subject: [PATCH 19/33] We shall not expose port 5353 publicly --- data/templates/yunohost/services.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index c7690fc9c..c781d54aa 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -52,7 +52,6 @@ yunohost-firewall: test_status: iptables -S | grep "^-A INPUT" | grep " --dport" | grep -q ACCEPT category: security yunomdns: - needs_exposed_ports: [5353] category: mdns glances: null nsswitch: null From 212ea635df64ca8a2d2a9167529c52204d89c5ea Mon Sep 17 00:00:00 2001 From: tituspijean Date: Thu, 12 Aug 2021 15:52:25 +0000 Subject: [PATCH 20/33] [mdns] Fix service user --- data/templates/mdns/yunomdns.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/templates/mdns/yunomdns.service b/data/templates/mdns/yunomdns.service index 36d938035..ce2641b5d 100644 --- a/data/templates/mdns/yunomdns.service +++ b/data/templates/mdns/yunomdns.service @@ -3,8 +3,8 @@ Description=YunoHost mDNS service After=network.target [Service] -User=avahi -Group=avahi +User=mdns +Group=mdns Type=simple ExecStart=/usr/bin/yunomdns StandardOutput=syslog From f1444bc36ffea201890e6e676ef71459e20c30ba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Aug 2021 18:40:43 +0200 Subject: [PATCH 21/33] mdns: misc fixes for ip parsing --- bin/yunomdns | 53 ++++++++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index 3e3eea72f..be5a87be0 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -47,7 +47,7 @@ def _extract_inet(string, skip_netmask=False, skip_loopback=True): 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})*)?)" + 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 = {} @@ -74,14 +74,16 @@ def _extract_inet(string, skip_netmask=False, skip_loopback=True): # 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 addr show") - for d in re.split(r"^(?:[0-9]+: )", output, flags=re.MULTILINE): - # Extract device name (1) and its addresses (2) - m = re.match(r"([^\s@]+)(?:@[\S]+)?: (.*)", d, flags=re.DOTALL) - if m: - devices_raw[m.group(1)] = m.group(2) + 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 = { @@ -122,25 +124,18 @@ if __name__ == '__main__': ips = [] # Human-readable IPs b_ips = [] # Binary-convered IPs - # Parse the IPs and prepare their binary version - addressed = False - try: - ip = interfaces[interface]['ipv4'].split('/')[0] - if len(ip)>0: addressed = True - ips.append(ip) - b_ips.append(socket.inet_pton(socket.AF_INET, ip)) - except: - pass - try: - ip = interfaces[interface]['ipv6'].split('/')[0] - if len(ip)>0: addressed = True - ips.append(ip) - b_ips.append(socket.inet_pton(socket.AF_INET6, ip)) - except: - pass + 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 addressed: + if ips: # Create a Zeroconf object, and store the ServiceInfos zc = Zeroconf(interfaces=ips) zcs[zc]=[] @@ -151,11 +146,11 @@ if __name__ == '__main__': 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+'.', + 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) From 7ca637d8f82f0a888d3d8af93950e0692b125821 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Aug 2021 18:54:20 +0200 Subject: [PATCH 22/33] yaml.load -> yaml.safe_load --- bin/yunomdns | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index be5a87be0..862a1f477 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """ -WIP Pythonic declaration of mDNS .local domains for YunoHost """ @@ -101,7 +100,7 @@ if __name__ == '__main__': ### with open('/etc/yunohost/mdns.yml', 'r') as f: - config = yaml.load(f) or {} + config = yaml.safe_load(f) or {} updated = False required_fields = ["interfaces", "domains"] From 849ccb043a94bbc60c608cf12665e9453df03455 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Aug 2021 19:13:26 +0200 Subject: [PATCH 23/33] mdns: diagnosis expects a 'data' key instead of 'results' --- data/hooks/diagnosis/12-dnsrecords.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index 89816847d..49e39c775 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -65,14 +65,14 @@ class DNSRecordsDiagnoser(Diagnoser): if is_subdomain: yield dict( meta={"domain": domain, "category": "basic"}, - results={}, + data={}, status="WARNING", summary="diagnosis_domain_subdomain_localdomain", ) else: yield dict( meta={"domain": domain, "category": "basic"}, - results={}, + data={}, status="INFO", summary="diagnosis_domain_localdomain", ) From 7f3eeafbed74af010c31e46f8753c2a4bd165fdd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Aug 2021 19:40:36 +0200 Subject: [PATCH 24/33] mdns/diagnosis: following suggestion IRL: extend the no-check mecanism for .local to .onion and other special-use TLDs --- data/hooks/diagnosis/12-dnsrecords.py | 30 ++++++++++----------------- locales/en.json | 3 +-- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index 49e39c775..c29029de9 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -13,7 +13,7 @@ from yunohost.diagnosis import Diagnoser from yunohost.domain import domain_list, _build_dns_conf, _get_maindomain YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] - +SPECIAL_USE_TLDS = ["local", "localhost", "onion", "dev", "test"] class DNSRecordsDiagnoser(Diagnoser): @@ -29,9 +29,9 @@ class DNSRecordsDiagnoser(Diagnoser): for domain in all_domains: self.logger_debug("Diagnosing DNS conf for %s" % domain) is_subdomain = domain.split(".", 1)[1] in all_domains - is_localdomain = domain.endswith(".local") + is_specialusedomain = any(domain.endswith("." + tld) for tld in SPECIAL_USE_TLDS) for report in self.check_domain( - domain, domain == main_domain, is_subdomain=is_subdomain, is_localdomain=is_localdomain + domain, domain == main_domain, is_subdomain=is_subdomain, is_specialusedomain=is_specialusedomain ): yield report @@ -49,7 +49,7 @@ class DNSRecordsDiagnoser(Diagnoser): for report in self.check_expiration_date(domains_from_registrar): yield report - def check_domain(self, domain, is_main_domain, is_subdomain, is_localdomain): + def check_domain(self, domain, is_main_domain, is_subdomain, is_specialusedomain): expected_configuration = _build_dns_conf( domain, include_empty_AAAA_if_no_ipv6=True @@ -60,22 +60,14 @@ class DNSRecordsDiagnoser(Diagnoser): if is_subdomain: categories = ["basic"] - if is_localdomain: + if is_specialusedomain: categories = [] - if is_subdomain: - yield dict( - meta={"domain": domain, "category": "basic"}, - data={}, - status="WARNING", - summary="diagnosis_domain_subdomain_localdomain", - ) - else: - yield dict( - meta={"domain": domain, "category": "basic"}, - data={}, - status="INFO", - summary="diagnosis_domain_localdomain", - ) + yield dict( + meta={"domain": domain}, + data={}, + status="INFO", + summary="diagnosis_dns_specialusedomain", + ) for category in categories: diff --git a/locales/en.json b/locales/en.json index 2b8e8b32f..cad1c8dcb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -183,6 +183,7 @@ "diagnosis_dns_discrepancy": "The following DNS record does not seem to follow the recommended configuration:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help about configuring DNS records.", "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force.", + "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) and is therefore not expected to have actual DNS records.", "diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains", "diagnosis_domain_not_found_details": "The domain {domain} doesn't exist in WHOIS database or is expired!", "diagnosis_domain_expiration_not_found_details": "The WHOIS information for domain {domain} doesn't seem to contain the information about the expiration date?", @@ -190,8 +191,6 @@ "diagnosis_domain_expiration_warning": "Some domains will expire soon!", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", "diagnosis_domain_expires_in": "{domain} expires in {days} days.", - "diagnosis_domain_localdomain": "Domain {domain}, with a .local TLD, is not expected to have DNS records as it can be discovered through mDNS.", - "diagnosis_domain_subdomain_localdomain": "Domain {domain} is a subdomain of a .local domain. Zeroconf/mDNS discovery only works with first-level domains.", "diagnosis_services_running": "Service {service} is running!", "diagnosis_services_conf_broken": "Configuration is broken for service {service}!", "diagnosis_services_bad_status": "Service {service} is {status} :(", From e493b89d6a5eb2301e6663277aa03ac404329e87 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Aug 2021 20:16:35 +0200 Subject: [PATCH 25/33] diagnosis: Zblerg, .dev is actually an actual TLD --- data/hooks/diagnosis/12-dnsrecords.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index c29029de9..1db4af685 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -13,7 +13,8 @@ from yunohost.diagnosis import Diagnoser from yunohost.domain import domain_list, _build_dns_conf, _get_maindomain YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] -SPECIAL_USE_TLDS = ["local", "localhost", "onion", "dev", "test"] +SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"] + class DNSRecordsDiagnoser(Diagnoser): From 3c07e55017d2d2c42a45c9aea027396f1121913f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Aug 2021 18:14:15 +0200 Subject: [PATCH 26/33] [fix] hook_exec / subprocess.Popen explodes if you feed non-string values in env variables --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index ef753d4d6..fad2033a6 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1233,7 +1233,7 @@ def app_remove(operation_logger, app, purge=False): env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") - env_dict["YNH_APP_PURGE"] = purge + env_dict["YNH_APP_PURGE"] = str(purge) operation_logger.extra.update({"env": env_dict}) operation_logger.flush() From e3f11bec0982e3ba9e3b9800a4acc1d104c56107 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Aug 2021 18:41:47 +0200 Subject: [PATCH 27/33] NO_BACKUP_UPGRADE: helpers expect 0 or 1 --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index fad2033a6..57498c644 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -619,7 +619,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version) env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version) - env_dict["NO_BACKUP_UPGRADE"] = no_safety_backup + env_dict["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0" # We'll check that the app didn't brutally edit some system configuration manually_modified_files_before_install = manually_modified_files() From b452838a1797641f82778e60f4e50e1722837eba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Aug 2021 19:16:29 +0200 Subject: [PATCH 28/33] Update changelog for 4.2.8 --- debian/changelog | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/debian/changelog b/debian/changelog index 57da44532..b1894758a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,16 @@ +yunohost (4.2.8) stable; urgency=low + + - [fix] ynh_permission_has_user not behaving properly when checking if a group is allowed (f0590907) + - [enh] use yaml safeloader everywhere ([#1287](https://github.com/YunoHost/yunohost/pull/1287)) + - [enh] Add --no-safety-backup option to "yunohost app upgrade" ([#1286](https://github.com/YunoHost/yunohost/pull/1286)) + - [enh] Add --purge option to "yunohost app remove" ([#1285](https://github.com/YunoHost/yunohost/pull/1285)) + - [enh] Multimedia helper: check that home folder exists ([#1255](https://github.com/YunoHost/yunohost/pull/1255)) + - [i18n] Translations updated for French, Galician, German, Portuguese + + Thanks to all contributors <3 ! (José M, Kay0u, Krakinou, ljf, Luca, mifegui, ppr, sagessylu) + + -- Alexandre Aubin Thu, 19 Aug 2021 19:11:19 +0200 + yunohost (4.2.7) stable; urgency=low Notable changes: From ecf136d5db27caae21656091b948eb825369b248 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Aug 2021 20:28:20 +0200 Subject: [PATCH 29/33] Auto-enable yunomdns on legacy systems --- data/hooks/conf_regen/37-mdns | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/hooks/conf_regen/37-mdns b/data/hooks/conf_regen/37-mdns index c85e43a9a..1d7381e26 100755 --- a/data/hooks/conf_regen/37-mdns +++ b/data/hooks/conf_regen/37-mdns @@ -51,6 +51,12 @@ do_post_regen() { systemctl daemon-reload fi + # Legacy stuff to enable the new yunomdns service on legacy systems + if [[ -e /etc/avahi/avahi-daemon.conf ]] && grep -q 'yunohost' /etc/avahi/avahi-daemon.conf + then + systemctl enable yunomdns + fi + [[ -z "$regen_conf_files" ]] \ || systemctl restart yunomdns } From b8c8ac0b2de6e2a5c91f2c50dab54209ec619faa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Aug 2021 20:29:28 +0200 Subject: [PATCH 30/33] Gotta also remove libnss-mdns if we want to get rid of avahi-daemon --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index c9306bef1..37eccb5dd 100644 --- a/debian/control +++ b/debian/control @@ -21,7 +21,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , openssh-server, iptables, fail2ban, dnsutils, bind9utils , openssl, ca-certificates, netcat-openbsd, iproute2 , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd - , dnsmasq, libnss-mdns, resolvconf, libnss-myhostname + , dnsmasq, resolvconf, libnss-myhostname , postfix, postfix-ldap, postfix-policyd-spf-perl, postfix-pcre , dovecot-core, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved, dovecot-antispam , rspamd, opendkim-tools, postsrsd, procmail, mailutils From cd917a9123de55245f5bc39612c82430fd4e123a Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Sun, 22 Aug 2021 21:57:50 +0200 Subject: [PATCH 31/33] [fix] Wording in path question --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 693e9d24d..2cb3fc329 100644 --- a/locales/en.json +++ b/locales/en.json @@ -32,7 +32,7 @@ "app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps}", "app_manifest_invalid": "Something is wrong with the app manifest: {error}", "app_manifest_install_ask_domain": "Choose the domain where this app should be installed", - "app_manifest_install_ask_path": "Choose the path where this app should be installed", + "app_manifest_install_ask_path": "Choose the web path after the domain where this app should be installed", "app_manifest_install_ask_password": "Choose an administration password for this app", "app_manifest_install_ask_admin": "Choose an administrator user for this app", "app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?", From c9d73af4013816ccb5ba2eba47a3cb804b44d2ee Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 23 Aug 2021 15:31:43 +0200 Subject: [PATCH 32/33] i18n: mdns -> yunomdns --- locales/en.json | 2 +- locales/fr.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index cad1c8dcb..f2658c412 100644 --- a/locales/en.json +++ b/locales/en.json @@ -561,7 +561,7 @@ "service_already_started": "The service '{service}' is running already", "service_already_stopped": "The service '{service}' has already been stopped", "service_cmd_exec_failed": "Could not execute the command '{command}'", - "service_description_mdns": "Allows you to reach your server using 'yunohost.local' in your local network", + "service_description_yunomdns": "Allows you to reach your server using 'yunohost.local' in your local network", "service_description_dnsmasq": "Handles domain name resolution (DNS)", "service_description_dovecot": "Allows e-mail clients to access/fetch email (via IMAP and POP3)", "service_description_fail2ban": "Protects against brute-force and other kinds of attacks from the Internet", diff --git a/locales/fr.json b/locales/fr.json index 72afe80dd..d40b39c62 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -234,7 +234,7 @@ "migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", "migrations_to_be_ran_manually": "La migration {id} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans l’interface admin, ou lancer `yunohost tools migrations run`.", "migrations_need_to_accept_disclaimer": "Pour lancer la migration {id}, vous devez accepter cet avertissement :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l’option --accept-disclaimer.", - "service_description_mdns": "Vous permet d’atteindre votre serveur en utilisant « yunohost.local » sur votre réseau local", + "service_description_yunomdns": "Vous permet d’atteindre votre serveur en utilisant « yunohost.local » sur votre réseau local", "service_description_dnsmasq": "Gère la résolution des noms de domaine (DNS)", "service_description_dovecot": "Permet aux clients de messagerie d’accéder/récupérer les courriels (via IMAP et POP3)", "service_description_fail2ban": "Protège contre les attaques brute-force et autres types d’attaques venant d’Internet", From 07cd1428d24fec44f3c6ca0d516f19d28d107f97 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 26 Aug 2021 20:06:39 +0200 Subject: [PATCH 33/33] Update locales/en.json --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 2cb3fc329..109c4b16b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -32,7 +32,7 @@ "app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps}", "app_manifest_invalid": "Something is wrong with the app manifest: {error}", "app_manifest_install_ask_domain": "Choose the domain where this app should be installed", - "app_manifest_install_ask_path": "Choose the web path after the domain where this app should be installed", + "app_manifest_install_ask_path": "Choose the url path (after the domain) where this app should be installed", "app_manifest_install_ask_password": "Choose an administration password for this app", "app_manifest_install_ask_admin": "Choose an administrator user for this app", "app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?",