Sunil Mohan Adapa b4e6c03bd7
coturn: New app to manage Coturn TURN/STUN server
- Shows URLs and shared secret that communication servers like matrix-synapse
should be configured to. Later we will implement auto-configuring those servers.

- Allow selecting domain for the sake of TLS/DTLS certificate installation.

- Simplify systemd service file options. Drop log file and pid file support as
they are not needed with systemd. Add security options.

- Set custom configuration file by overriding systemd service file options so
that we don't have a problem with conffile prompts.

- Implement functional tests (and automatic diagnostics).

- Custom icon selected from the Noun project as Coturn project does not have
one.

- Backup/restore configuration file and certificates.

- Document some questions regarding configuration options.

Tests performed:

- App is not listed in the app page if 'advanced' flag is disabled.

- App name, icon and short description shows up correctly in apps page.

- App name, icon, short description, description, manual link, enable/disable
button and diagnostics link show up currently in app page.

- Verify that configuration used by coturn server is the FreedomBox
configuration by checking the cert path in the log output.

- PID file is not created in /var/run/turnserver/. It goes into /dev/null
according to the log output.

- No log file is created other than what is collected by systemd from command
line.

- systemctl show coturn.service shows all the intended restrictions such as
NoNewPrivileges, Protect* options.

- Run functional tests.

- Ensure that backup of configuration file works by taking backup, changing the
secret and restoring. During backup and restore coturn should be stopped and
started as per logs.

- Build Debian package. No warnings about the copyright file.

- Enabling the app enables the service and runs it.

- Disabling the app disables the service and stop it.

- All diagnostics tests pass.

- Diagnostic tests show firewall port coturn-freedombox for internal and
external networks, service coturn, and each listening port for udp4, udp6, tcp4
and tcp6.

- Information in the firewall page shows up properly. Enabling the app opens
firewall ports, and disabling it closes them.

- When the app is installed, if a cert domain is available, it will be used.
When multiple domains are available, one of them is picked.

- Status shows 4 URLs with the currently selected domain and secret key.

- Changing domain to another domain succeeds and reflects in the status
information.

- When no domain is configured. Installing the app succeeds. No domain is shown
in the list of domains.

- When domain is changed, the certificates files in /etc/coturn/certs are
overwritten.

- Certificates have the ownership turnserver:turnserver. Public key is cert.pem
has 644 permissions. Private is pkey.pem has 600 permissions. /etc/coturn/certs
is owned by root:root.

- Let's encrypt certificates are setup immediately after install.

- Port forwarding information shows all ports except for relay ports.

- Trying to create a user with username 'turnserver' throws an error. This
happens even when coturn is not installed yet.

- After installing coturn, the configuration file /etc/coturn/freedombox.conf is
created with ownership root:turnserver and permissions 640. The directory
/etc/coturn is created with ownership root:root and permissions 755.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
[jvalleroy: Fix copied form_valid comment]
Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2020-05-02 18:51:23 -04:00

117 lines
3.2 KiB
Python
Executable File

#!/usr/bin/python3
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Configuration helper for Coturn daemon.
"""
import argparse
import json
import pathlib
import random
import shutil
import string
import augeas
from plinth import action_utils
CONFIG_FILE = pathlib.Path('/etc/coturn/freedombox.conf')
def parse_arguments():
"""Return parsed command line arguments as dictionary."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
subparsers.add_parser('setup', help='Setup Coturn server')
subparsers.add_parser('get-config',
help='Return the current configuration')
subparser = subparsers.add_parser('set-domain', help='Set the TLS domain')
subparser.add_argument('domain_name', help='TLS domain name to set')
subparsers.required = True
return parser.parse_args()
def _key_path(key):
"""Return the augeas path for a key."""
return '/files' + str(CONFIG_FILE) + '/' + key
def subcommand_setup(_):
"""Setup Coturn server."""
CONFIG_FILE.parent.mkdir(exist_ok=True)
if not CONFIG_FILE.exists():
CONFIG_FILE.touch(0o640)
shutil.chown(CONFIG_FILE, group='turnserver')
action_utils.service_daemon_reload()
action_utils.service_try_restart('coturn')
aug = augeas_load()
# XXX: Should we set listen, relay IP address to :: or dynamically
# XXX: Should we set external-ip
aug.set(_key_path('min-port'), '49152')
aug.set(_key_path('max-port'), '50175')
aug.set(_key_path('use-auth-secret'), 'true')
if not aug.get(_key_path('static-auth-secret')):
secret = ''.join(
random.choice(string.ascii_letters + string.digits)
for _ in range(64))
aug.set(_key_path('static-auth-secret'), secret)
aug.set(_key_path('cert'), '/etc/coturn/certs/cert.pem')
aug.set(_key_path('pkey'), '/etc/coturn/certs/pkey.pem')
aug.set(_key_path('no-tlsv1'), 'true')
aug.set(_key_path('no-tlsv1_1'), 'true')
aug.set(_key_path('no-cli'), 'true')
aug.save()
def subcommand_get_config(_):
"""Return the current configuration in JSON format."""
aug = augeas_load()
config = {
'static_auth_secret': aug.get(_key_path('static-auth-secret')),
'realm': aug.get(_key_path('realm')),
}
print(json.dumps(config))
def subcommand_set_domain(arguments):
"""Set the TLS domain.
This value is usually not stored. So, set realm value even though it is not
needed to set realm for REST API based authentication.
"""
aug = augeas_load()
aug.set(_key_path('realm'), arguments.domain_name)
aug.save()
def augeas_load():
"""Initialize Augeas."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Simplevars/lens', 'Simplevars.lns')
aug.set('/augeas/load/Simplevars/incl[last() + 1]', str(CONFIG_FILE))
aug.load()
return aug
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
if __name__ == '__main__':
main()