diff --git a/plinth/modules/names/forms.py b/plinth/modules/names/forms.py new file mode 100644 index 000000000..186b611f9 --- /dev/null +++ b/plinth/modules/names/forms.py @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +"""Forms for the names app.""" + +from django import forms +from django.utils.translation import gettext_lazy as _ + +from plinth.utils import format_lazy + + +class NamesConfigurationForm(forms.Form): + """Form to configure names app.""" + + dns_over_tls = forms.ChoiceField( + label=_('Use DNS-over-TLS for resolving domains (global preference)'), + widget=forms.RadioSelect, choices=[ + ('yes', + format_lazy( + 'Yes. Encrypt connections to the DNS server.

This improves privacy as domain name ' + 'queries will not be made as plain text over the network. It ' + 'also improves security as responses from the server cannot ' + 'be manipulated. If the configured DNS servers do not ' + 'support DNS-over-TLS, all name resolutions will fail. If ' + 'your DNS provider (likely your ISP) does not support ' + 'DNS-over-TLS or blocks some domains, you can configure ' + 'well-known public DNS servers in individual network ' + 'connection settings.

', allow_markup=True)), + ('opportunistic', + format_lazy( + 'Opportunistic.

Encrypt connections to ' + 'the DNS server if the server supports DNS-over-TLS. ' + 'Otherwise, use unencrypted connections. There is no ' + 'protection against response manipulation.

', + allow_markup=True)), + ('no', + format_lazy( + 'No.

Do not encrypt domain name ' + 'resolutions.

', allow_markup=True)), + ], initial='no') diff --git a/plinth/modules/names/privileged.py b/plinth/modules/names/privileged.py index 37ec1deb1..8b1c464fd 100644 --- a/plinth/modules/names/privileged.py +++ b/plinth/modules/names/privileged.py @@ -3,26 +3,37 @@ import pathlib +import augeas + from plinth import action_utils from plinth.actions import privileged fallback_conf = pathlib.Path( '/etc/systemd/resolved.conf.d/freedombox-fallback.conf') +override_conf = pathlib.Path('/etc/systemd/resolved.conf.d/freedombox.conf') source_fallback_conf = pathlib.Path( '/usr/share/freedombox' '/etc/systemd/resolved.conf.d/freedombox-fallback.conf') @privileged -def set_resolved_configuration(dns_fallback: bool | None = None): +def set_resolved_configuration(dns_fallback: bool | None = None, + dns_over_tls: str | None = None): """Set systemd-resolved configuration options.""" if dns_fallback is not None: _set_enable_dns_fallback(dns_fallback) + if dns_over_tls is not None: + _set_resolved_configuration(dns_over_tls) + + action_utils.service_reload('systemd-resolved') + def get_resolved_configuration() -> dict[str, bool]: """Return systemd-resolved configuration.""" - return {'dns_fallback': fallback_conf.exists()} + configuration = _get_resolved_configuration() + configuration['dns_fallback'] = fallback_conf.exists() + return configuration def _set_enable_dns_fallback(dns_fallback: bool): @@ -34,4 +45,28 @@ def _set_enable_dns_fallback(dns_fallback: bool): else: fallback_conf.unlink(missing_ok=True) - action_utils.service_reload('systemd-resolved') + +def _load_augeas(): + """Initialize Augeas.""" + aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + + augeas.Augeas.NO_MODL_AUTOLOAD) + aug.transform('Systemd', str(override_conf)) + aug.set('/augeas/context', '/files' + str(override_conf)) + aug.load() + return aug + + +def _get_resolved_configuration(): + """Return overridden configuration for systemd-resolved.""" + aug = _load_augeas() + return {'dns_over_tls': aug.get('Resolve/DNSOverTLS/value') or 'no'} + + +def _set_resolved_configuration(dns_over_tls: str | None = None): + """Write configuration into a systemd-resolved override file.""" + aug = _load_augeas() + + if dns_over_tls is not None: + aug.set('Resolve/DNSOverTLS/value', dns_over_tls) + + aug.save() diff --git a/plinth/modules/names/templates/names.html b/plinth/modules/names/templates/names.html index 2c6f1b409..2ba35f9a4 100644 --- a/plinth/modules/names/templates/names.html +++ b/plinth/modules/names/templates/names.html @@ -6,7 +6,10 @@ {% load bootstrap %} {% load i18n %} -{% block configuration %} +{% block status %} + {{ block.super }} + +

{% trans "Domains" %}

diff --git a/plinth/modules/names/views.py b/plinth/modules/names/views.py index 736d4116a..a01a7bbd7 100644 --- a/plinth/modules/names/views.py +++ b/plinth/modules/names/views.py @@ -3,9 +3,13 @@ FreedomBox app for name services. """ +from django.contrib import messages +from django.utils.translation import gettext_lazy as _ + from plinth.views import AppView -from . import components +from . import components, privileged +from .forms import NamesConfigurationForm class NamesAppView(AppView): @@ -13,6 +17,14 @@ class NamesAppView(AppView): app_id = 'names' template_name = 'names.html' + prefix = 'names' + form_class = NamesConfigurationForm + + def get_initial(self): + """Return the values to fill in the form.""" + initial = super().get_initial() + initial.update(privileged.get_resolved_configuration()) + return initial def get_context_data(self, *args, **kwargs): """Add additional context data for template.""" @@ -20,6 +32,18 @@ class NamesAppView(AppView): context['status'] = get_status() return context + def form_valid(self, form): + """Apply the changes submitted in the form.""" + old_data = form.initial + form_data = form.cleaned_data + + if old_data['dns_over_tls'] != form_data['dns_over_tls']: + privileged.set_resolved_configuration( + dns_over_tls=form_data['dns_over_tls']) + messages.success(self.request, _('Configuration updated')) + + return super().form_valid(form) + def get_status(): """Get configured services per name."""