mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Do not rely on Moulinette, integration with Yunohost of mDNS broadcast
This commit is contained in:
parent
9e93efa895
commit
99aacd8b51
7 changed files with 244 additions and 54 deletions
269
bin/yunomdns
269
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
|
Based off https://github.com/jstasiak/python-zeroconf/blob/master/tests/test_asyncio.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
|
import yaml
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
from typing import List
|
from typing import List, Dict
|
||||||
|
|
||||||
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 DNSEntry, DNSRecord
|
||||||
from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf
|
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:
|
# Helper command taken from Moulinette
|
||||||
tasks = [aiozc.async_register_service(info) for info in infos]
|
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)
|
background_tasks = await asyncio.gather(*tasks)
|
||||||
await asyncio.gather(*background_tasks)
|
await asyncio.gather(*background_tasks)
|
||||||
|
|
||||||
async def unregister_services(aiozc: AsyncZeroconf, infos: List[AsyncServiceInfo]) -> None:
|
async def unregister_services(aiozcs: Dict[AsyncZeroconf, List[AsyncServiceInfo]]) -> None:
|
||||||
tasks = [aiozc.async_unregister_service(info) for info in infos]
|
for aiozc, infos in aiozcs.items():
|
||||||
|
for info in infos:
|
||||||
|
tasks.append(aiozc.async_unregister_service(info))
|
||||||
background_tasks = await asyncio.gather(*tasks)
|
background_tasks = await asyncio.gather(*tasks)
|
||||||
await asyncio.gather(*background_tasks)
|
await asyncio.gather(*background_tasks)
|
||||||
|
|
||||||
async def close_aiozc(aiozc: AsyncZeroconf) -> None:
|
async def close_aiozcs(aiozcs: Dict[AsyncZeroconf, List[AsyncServiceInfo]]) -> None:
|
||||||
await aiozc.async_close()
|
tasks = []
|
||||||
|
for aiozc in aiozcs:
|
||||||
|
tasks.append(aiozc.async_close())
|
||||||
|
background_tasks = await asyncio.gather(*tasks)
|
||||||
|
await asyncio.gather(*background_tasks)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
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('--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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
logging.getLogger('zeroconf').setLevel(logging.DEBUG)
|
logging.getLogger('zeroconf').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('asyncio').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()
|
|
||||||
else:
|
else:
|
||||||
wanted_interfaces = []
|
logging.getLogger('zeroconf').setLevel(logging.WARNING)
|
||||||
print('No interface listed for broadcast.')
|
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()
|
interfaces = get_network_interfaces()
|
||||||
for interface in wanted_interfaces:
|
for interface in config['interfaces']:
|
||||||
infos = []
|
infos = [] # List of ServiceInfo objects, to feed Zeroconf
|
||||||
ips = [] # Human-readable IPs
|
ips = [] # Human-readable IPs
|
||||||
b_ips = [] # Binary-convered IPs
|
b_ips = [] # Binary-convered IPs
|
||||||
|
|
||||||
# Parse the IPs and prepare their binary version
|
# Parse the IPs and prepare their binary version
|
||||||
|
addressed = False
|
||||||
try:
|
try:
|
||||||
ip = interfaces[interface]['ipv4'].split('/')[0]
|
ip = interfaces[interface]['ipv4'].split('/')[0]
|
||||||
|
if len(ip)>0: addressed = True
|
||||||
ips.append(ip)
|
ips.append(ip)
|
||||||
b_ips.append(socket.inet_pton(socket.AF_INET, ip))
|
b_ips.append(socket.inet_pton(socket.AF_INET, ip))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
ip = interfaces[interface]['ipv6'].split('/')[0]
|
ip = interfaces[interface]['ipv6'].split('/')[0]
|
||||||
|
if len(ip)>0: addressed = True
|
||||||
ips.append(ip)
|
ips.append(ip)
|
||||||
b_ips.append(socket.inet_pton(socket.AF_INET6, ip))
|
b_ips.append(socket.inet_pton(socket.AF_INET6, ip))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Create a ServiceInfo object for each .local domain
|
# If at least one IP is listed
|
||||||
for d in local_domains:
|
if addressed:
|
||||||
d_domain=d.replace('.local','')
|
# Create a ServiceInfo object for each .local domain
|
||||||
infos.append(
|
for d in config['domains']:
|
||||||
AsyncServiceInfo(
|
d_domain=d.replace('.local','')
|
||||||
type_="_device-info._tcp.local.",
|
infos.append(
|
||||||
name=d_domain+f"._device-info._tcp.local.",
|
AsyncServiceInfo(
|
||||||
addresses=b_ips,
|
type_='_device-info._tcp.local.',
|
||||||
port=80,
|
name=d_domain+'._device-info._tcp.local.',
|
||||||
server=d+'.',
|
addresses=b_ips,
|
||||||
|
port=80,
|
||||||
|
server=d+'.',
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
infos.append(
|
||||||
print('Adding '+d+' with addresses '+str(ips)+' on interface '+interface)
|
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
|
# Run registration
|
||||||
aiozc = AsyncZeroconf(interfaces=ips)
|
loop.run_until_complete(register_services(aiozcs))
|
||||||
aiozcs.append(aiozc)
|
print("Registration complete. Press Ctrl-c or stop service to exit...")
|
||||||
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:
|
try:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(0.1)
|
time.sleep(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
print("Unregistering...")
|
print("Unregistering...")
|
||||||
for aiozc in aiozcs:
|
loop.run_until_complete(unregister_services(aiozcs))
|
||||||
loop.run_until_complete(unregister_services(aiozc, infos))
|
|
||||||
loop.run_until_complete(close_aiozc(aiozc))
|
|
||||||
print("Unregistration complete.")
|
print("Unregistration complete.")
|
||||||
|
loop.run_until_complete(close_aiozcs(aiozcs))
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
services_path="/etc/yunohost/services.yml"
|
services_path="/etc/yunohost/services.yml"
|
||||||
|
mdns_path="/etc/yunohost/mdns.yml"
|
||||||
|
|
||||||
do_init_regen() {
|
do_init_regen() {
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
@ -18,9 +19,11 @@ do_init_regen() {
|
||||||
[[ -f /etc/yunohost/current_host ]] \
|
[[ -f /etc/yunohost/current_host ]] \
|
||||||
|| echo "yunohost.org" > /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 ]] \
|
[[ -f $services_path ]] \
|
||||||
|| cp services.yml "$services_path"
|
|| cp services.yml "$services_path"
|
||||||
|
[[ -f $mdns_path ]] \
|
||||||
|
|| cp mdns.yml "$mdns_path"
|
||||||
[[ -f /etc/yunohost/firewall.yml ]] \
|
[[ -f /etc/yunohost/firewall.yml ]] \
|
||||||
|| cp firewall.yml /etc/yunohost/firewall.yml
|
|| cp firewall.yml /etc/yunohost/firewall.yml
|
||||||
|
|
||||||
|
|
13
data/other/yunomdns.service
Normal file
13
data/other/yunomdns.service
Normal file
|
@ -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
|
4
data/templates/yunohost/mdns.yml
Normal file
4
data/templates/yunohost/mdns.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
enabled: True
|
||||||
|
regen: all
|
||||||
|
interfaces:
|
||||||
|
domains:
|
|
@ -52,6 +52,8 @@ yunohost-firewall:
|
||||||
need_lock: true
|
need_lock: true
|
||||||
test_status: iptables -S | grep "^-A INPUT" | grep " --dport" | grep -q ACCEPT
|
test_status: iptables -S | grep "^-A INPUT" | grep " --dport" | grep -q ACCEPT
|
||||||
category: security
|
category: security
|
||||||
|
yunomdns:
|
||||||
|
needs_exposed_ports: [5353]
|
||||||
glances: null
|
glances: null
|
||||||
nsswitch: null
|
nsswitch: null
|
||||||
ssl: null
|
ssl: null
|
||||||
|
|
1
debian/install
vendored
1
debian/install
vendored
|
@ -5,6 +5,7 @@ doc/yunohost.8.gz /usr/share/man/man8/
|
||||||
data/actionsmap/* /usr/share/moulinette/actionsmap/
|
data/actionsmap/* /usr/share/moulinette/actionsmap/
|
||||||
data/hooks/* /usr/share/yunohost/hooks/
|
data/hooks/* /usr/share/yunohost/hooks/
|
||||||
data/other/yunoprompt.service /etc/systemd/system/
|
data/other/yunoprompt.service /etc/systemd/system/
|
||||||
|
data/other/yunomdns.service /etc/systemd/system/
|
||||||
data/other/password/* /usr/share/yunohost/other/password/
|
data/other/password/* /usr/share/yunohost/other/password/
|
||||||
data/other/dpkg-origins/yunohost /etc/dpkg/origins
|
data/other/dpkg-origins/yunohost /etc/dpkg/origins
|
||||||
data/other/dnsbl_list.yml /usr/share/yunohost/other/
|
data/other/dnsbl_list.yml /usr/share/yunohost/other/
|
||||||
|
|
4
debian/postinst
vendored
4
debian/postinst
vendored
|
@ -38,6 +38,10 @@ do_configure() {
|
||||||
|
|
||||||
# Yunoprompt
|
# Yunoprompt
|
||||||
systemctl enable yunoprompt.service
|
systemctl enable yunoprompt.service
|
||||||
|
|
||||||
|
# Yunomdns
|
||||||
|
chown avahi:avahi /etc/yunohost/mdns.yml
|
||||||
|
systemctl enable yunomdns.service
|
||||||
}
|
}
|
||||||
|
|
||||||
# summary of how this script can be called:
|
# summary of how this script can be called:
|
||||||
|
|
Loading…
Add table
Reference in a new issue