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
251
bin/yunomdns
251
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
|
||||
|
||||
# If at least one IP is listed
|
||||
if addressed:
|
||||
# Create a ServiceInfo object for each .local domain
|
||||
for d in local_domains:
|
||||
for d in config['domains']:
|
||||
d_domain=d.replace('.local','')
|
||||
infos.append(
|
||||
AsyncServiceInfo(
|
||||
type_="_device-info._tcp.local.",
|
||||
name=d_domain+f"._device-info._tcp.local.",
|
||||
type_='_device-info._tcp.local.',
|
||||
name=d_domain+'._device-info._tcp.local.',
|
||||
addresses=b_ips,
|
||||
port=80,
|
||||
server=d+'.',
|
||||
)
|
||||
)
|
||||
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, store it, and start Service registration
|
||||
# Create an AsyncZeroconf object, and store registration task
|
||||
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))
|
||||
aiozcs[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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
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
|
||||
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
|
||||
|
|
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/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/
|
||||
|
|
4
debian/postinst
vendored
4
debian/postinst
vendored
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue