From b4e6c03bd78c73ca946e706b58089a24077b706c Mon Sep 17 00:00:00 2001
From: Sunil Mohan Adapa
Date: Thu, 23 Apr 2020 15:41:15 -0700
Subject: [PATCH] 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
[jvalleroy: Fix copied form_valid comment]
Signed-off-by: James Valleroy
Reviewed-by: James Valleroy
---
actions/coturn | 116 +++++++++++++
debian/copyright | 6 +
functional_tests/features/coturn.feature | 27 +++
plinth/modules/coturn/__init__.py | 156 ++++++++++++++++++
.../data/etc/plinth/modules-enabled/coturn | 1 +
.../system/coturn.service.d/freedombox.conf | 20 +++
.../firewalld/services/coturn-freedombox.xml | 15 ++
plinth/modules/coturn/forms.py | 27 +++
plinth/modules/coturn/manifest.py | 10 ++
plinth/modules/coturn/templates/coturn.html | 32 ++++
plinth/modules/coturn/tests/__init__.py | 0
plinth/modules/coturn/urls.py | 12 ++
plinth/modules/coturn/views.py | 42 +++++
static/themes/default/icons/coturn.png | Bin 0 -> 4708 bytes
static/themes/default/icons/coturn.svg | 67 ++++++++
15 files changed, 531 insertions(+)
create mode 100755 actions/coturn
create mode 100644 functional_tests/features/coturn.feature
create mode 100644 plinth/modules/coturn/__init__.py
create mode 100644 plinth/modules/coturn/data/etc/plinth/modules-enabled/coturn
create mode 100644 plinth/modules/coturn/data/lib/systemd/system/coturn.service.d/freedombox.conf
create mode 100644 plinth/modules/coturn/data/usr/lib/firewalld/services/coturn-freedombox.xml
create mode 100644 plinth/modules/coturn/forms.py
create mode 100644 plinth/modules/coturn/manifest.py
create mode 100644 plinth/modules/coturn/templates/coturn.html
create mode 100644 plinth/modules/coturn/tests/__init__.py
create mode 100644 plinth/modules/coturn/urls.py
create mode 100644 plinth/modules/coturn/views.py
create mode 100644 static/themes/default/icons/coturn.png
create mode 100644 static/themes/default/icons/coturn.svg
diff --git a/actions/coturn b/actions/coturn
new file mode 100755
index 000000000..e50f2af14
--- /dev/null
+++ b/actions/coturn
@@ -0,0 +1,116 @@
+#!/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()
diff --git a/debian/copyright b/debian/copyright
index 018d9095f..30063f136 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -41,6 +41,12 @@ Copyright: Cockpit Authors (https://github.com/cockpit-project/cockpit/blob/mast
Comment: https://github.com/cockpit-project/cockpit/blob/master/src/branding/default/logo.svg
License: LGPL-2.1+
+Files: static/themes/default/icons/coturn.png
+ static/themes/default/icons/coturn.svg
+Copyright: Kmg Design (https://thenounproject.com/kmgdesignid/)
+Comment: Video Call by Kmg Design from the Noun Project https://thenounproject.com/term/video-call/3256092/
+License: CC-BY-3.0-US
+
Files: static/themes/default/icons/deluge.png
Copyright: 2007 Andrew Wedderburn
Comment: https://upload.wikimedia.org/wikipedia/commons/thumb/8/85//Deluge-Logo.svg/2000px-Deluge-Logo.svg.png
diff --git a/functional_tests/features/coturn.feature b/functional_tests/features/coturn.feature
new file mode 100644
index 000000000..b707c02fe
--- /dev/null
+++ b/functional_tests/features/coturn.feature
@@ -0,0 +1,27 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+@apps @coturn @backups
+Feature: Coturn STUN/TURN Server
+ Run the Coturn STUN/TURN server.
+
+Background:
+ Given I'm a logged in user
+ And advanced mode is on
+ And the coturn application is installed
+
+Scenario: Enable coturn application
+ Given the coturn application is disabled
+ When I enable the coturn application
+ Then the coturn service should be running
+
+# TODO: Improve this by checking that secret and domain did not change
+Scenario: Backup and restore coturn
+ Given the coturn application is enabled
+ When I create a backup of the coturn app data
+ And I restore the coturn app data backup
+ Then the coturn service should be running
+
+Scenario: Disable coturn application
+ Given the coturn application is enabled
+ When I disable the coturn application
+ Then the coturn service should not be running
diff --git a/plinth/modules/coturn/__init__.py b/plinth/modules/coturn/__init__.py
new file mode 100644
index 000000000..2f80bea4a
--- /dev/null
+++ b/plinth/modules/coturn/__init__.py
@@ -0,0 +1,156 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+FreedomBox app to configure Coturn server.
+"""
+
+import json
+import pathlib
+
+from django.utils.translation import ugettext_lazy as _
+
+from plinth import actions
+from plinth import app as app_module
+from plinth import menu
+from plinth.daemon import Daemon
+from plinth.modules import names
+from plinth.modules.firewall.components import Firewall
+from plinth.modules.letsencrypt.components import LetsEncrypt
+from plinth.modules.users.components import UsersAndGroups
+
+from .manifest import backup # noqa, pylint: disable=unused-import
+
+version = 1
+
+managed_services = ['coturn']
+
+managed_packages = ['coturn']
+
+managed_paths = [pathlib.Path('/etc/coturn/')]
+
+_description = [
+ _('Coturn is a server to facilitate audio/video calls and conferences by '
+ 'providing an implementation of TURN and STUN protocols. WebRTC, SIP '
+ 'and other communication servers can use it to establish a call between '
+ 'parties who are otherwise unable connect to each other.'),
+ _('It is not meant to be used directly by users. Servers such as '
+ 'matrix-synapse need to be configured with the details provided here.'),
+]
+
+port_forwarding_info = [
+ ('UDP', 3478),
+ ('TCP', 3478),
+ ('UDP', 3479),
+ ('TCP', 3479),
+ ('UDP', 5349),
+ ('TCP', 5349),
+ ('UDP', 5350),
+ ('TCP', 5350),
+ # XXX: Add relay ports here
+]
+
+app = None
+
+
+class CoturnApp(app_module.App):
+ """FreedomBox app for Coturn."""
+
+ app_id = 'coturn'
+
+ def __init__(self):
+ """Create components for the app."""
+ super().__init__()
+
+ info = app_module.Info(app_id=self.app_id, version=version,
+ name=_('Coturn'), icon_filename='coturn',
+ short_description=_('VoIP Helper'),
+ description=_description, manual_page='Coturn')
+ self.add(info)
+
+ menu_item = menu.Menu('menu-coturn', info.name, info.short_description,
+ info.icon_filename, 'coturn:index',
+ parent_url_name='apps', advanced=True)
+ self.add(menu_item)
+
+ firewall = Firewall('firewall-coturn', info.name,
+ ports=['coturn-freedombox'], is_external=True)
+ self.add(firewall)
+
+ letsencrypt = LetsEncrypt(
+ 'letsencrypt-coturn', domains=get_domains,
+ daemons=managed_services, should_copy_certificates=True,
+ private_key_path='/etc/coturn/certs/pkey.pem',
+ certificate_path='/etc/coturn/certs/cert.pem',
+ user_owner='turnserver', group_owner='turnserver',
+ managing_app='coturn')
+ self.add(letsencrypt)
+
+ daemon = Daemon(
+ 'daemon-coturn', managed_services[0],
+ listen_ports=[(3478, 'udp4'), (3478, 'udp6'), (3478, 'tcp4'),
+ (3478, 'tcp6'), (3479, 'udp4'), (3479, 'udp6'),
+ (3479, 'tcp4'), (3479, 'tcp6'), (5349, 'udp4'),
+ (5349, 'udp6'), (5349, 'tcp4'), (5349, 'tcp6'),
+ (5350, 'udp4'), (5350, 'udp6'), (5350, 'tcp4'),
+ (5350, 'tcp6')])
+ self.add(daemon)
+
+ users_and_groups = UsersAndGroups('users-and-groups-coturn',
+ reserved_usernames=['turnserver'])
+ self.add(users_and_groups)
+
+
+def init():
+ """Initialize the Coturn module."""
+ global app
+ app = CoturnApp()
+
+ setup_helper = globals()['setup_helper']
+ if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
+ app.set_enabled(True)
+
+
+def setup(helper, old_version=None):
+ """Install and configure the module."""
+ helper.install(managed_packages)
+ helper.call('post', actions.superuser_run, 'coturn', ['setup'])
+ helper.call('post', app.enable)
+ app.get_component('letsencrypt-coturn').setup_certificates()
+
+
+def get_available_domains():
+ """Return an iterator with all domains able to have a certificate."""
+ return (domain.name for domain in names.components.DomainName.list()
+ if domain.domain_type.can_have_certificate)
+
+
+def get_domain():
+ """Read TLS domain from config file select first available if none."""
+ config = get_config()
+ if config['realm']:
+ return get_config()['realm']
+
+ domain = next(get_available_domains(), None)
+ set_domain(domain)
+
+ return domain
+
+
+def get_domains():
+ """Return a list with the configured domains."""
+ domain = get_domain()
+ if domain:
+ return [domain]
+
+ return []
+
+
+def set_domain(domain):
+ """Set the TLS domain by writing a file to data directory."""
+ if domain:
+ actions.superuser_run('coturn', ['set-domain', domain])
+
+
+def get_config():
+ """Return the coturn server configuration."""
+ output = actions.superuser_run('coturn', ['get-config'])
+ return json.loads(output)
diff --git a/plinth/modules/coturn/data/etc/plinth/modules-enabled/coturn b/plinth/modules/coturn/data/etc/plinth/modules-enabled/coturn
new file mode 100644
index 000000000..c31be5ce2
--- /dev/null
+++ b/plinth/modules/coturn/data/etc/plinth/modules-enabled/coturn
@@ -0,0 +1 @@
+plinth.modules.coturn
diff --git a/plinth/modules/coturn/data/lib/systemd/system/coturn.service.d/freedombox.conf b/plinth/modules/coturn/data/lib/systemd/system/coturn.service.d/freedombox.conf
new file mode 100644
index 000000000..a59637990
--- /dev/null
+++ b/plinth/modules/coturn/data/lib/systemd/system/coturn.service.d/freedombox.conf
@@ -0,0 +1,20 @@
+[Service]
+ExecStart=
+ExecStart=/usr/bin/turnserver -c /etc/coturn/freedombox.conf --pidfile=/dev/null --log-file=-
+ExecStartPost=
+LockPersonality=yes
+NoNewPrivileges=yes
+PIDFile=
+PrivateDevices=yes
+PrivateMounts=yes
+PrivateTmp=yes
+ProtectControlGroups=yes
+ProtectHome=yes
+ProtectKernelLogs=yes
+ProtectKernelModules=yes
+ProtectKernelTunables=yes
+ProtectSystem=strict
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
+RestrictRealtime=yes
+SystemCallArchitectures=native
+Type=simple
diff --git a/plinth/modules/coturn/data/usr/lib/firewalld/services/coturn-freedombox.xml b/plinth/modules/coturn/data/usr/lib/firewalld/services/coturn-freedombox.xml
new file mode 100644
index 000000000..33832d60b
--- /dev/null
+++ b/plinth/modules/coturn/data/usr/lib/firewalld/services/coturn-freedombox.xml
@@ -0,0 +1,15 @@
+
+
+ Coturn STUN/TURN server
+ Coturn is a server to facilitate audio/video calls and conferences by providing implementation of TURN and STUN protocols. WebRTC, SIP and other communication servers can use it to establish a call between parties who are otherwise unable connect with each other. Enable this if you are running the communications server and wish to perform audio/video calls.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plinth/modules/coturn/forms.py b/plinth/modules/coturn/forms.py
new file mode 100644
index 000000000..53f89d199
--- /dev/null
+++ b/plinth/modules/coturn/forms.py
@@ -0,0 +1,27 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+Forms for Coturn app.
+"""
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from plinth.modules import coturn
+
+
+def get_domain_choices():
+ """Double domain entries for inclusion in the choice field."""
+ return ((domain, domain) for domain in coturn.get_available_domains())
+
+
+class CoturnForm(forms.Form):
+ """Form to select a TLS domain for Coturn."""
+
+ domain = forms.ChoiceField(
+ choices=get_domain_choices,
+ label=_('TLS domain'),
+ help_text=_(
+ 'Select a domain to use TLS with. If the list is empty, please '
+ 'configure at least one domain with certificates.'),
+ required=False,
+ )
diff --git a/plinth/modules/coturn/manifest.py b/plinth/modules/coturn/manifest.py
new file mode 100644
index 000000000..fc0748a4b
--- /dev/null
+++ b/plinth/modules/coturn/manifest.py
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+from plinth.modules.backups.api import validate as validate_backup
+
+backup = validate_backup({
+ 'secrets': {
+ 'directories': ['/etc/coturn']
+ },
+ 'services': ['coturn']
+})
diff --git a/plinth/modules/coturn/templates/coturn.html b/plinth/modules/coturn/templates/coturn.html
new file mode 100644
index 000000000..f90905bd8
--- /dev/null
+++ b/plinth/modules/coturn/templates/coturn.html
@@ -0,0 +1,32 @@
+{% extends "app.html" %}
+{% comment %}
+# SPDX-License-Identifier: AGPL-3.0-or-later
+{% endcomment %}
+
+{% load bootstrap %}
+{% load i18n %}
+
+{% block status %}
+ {{ block.super }}
+
+
Status
+
+
+ {% blocktrans trimmed %}
+ Use the following URLs to configure your communication server:
+ {% endblocktrans %}
+
+