diff --git a/plinth/modules/networks/forms.py b/plinth/modules/networks/forms.py index 86c085ff1..32cd089e7 100644 --- a/plinth/modules/networks/forms.py +++ b/plinth/modules/networks/forms.py @@ -30,6 +30,37 @@ class ConnectionForm(forms.Form): help_text=_('The firewall zone will control which services are ' 'available over this interfaces. Select Internal only ' 'for trusted networks.'), choices=network.ZONES) + dns_over_tls = forms.ChoiceField( + label=_('Use DNS-over-TLS'), widget=forms.RadioSelect, choices=[ + ('default', + format_lazy( + 'Default. Unspecified for this connection.

Use the global preference.

', + allow_markup=True)), + ('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 a ' + 'well-known public DNS server below.

', + 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 for this connection.

', allow_markup=True)), + ], initial='default') ipv4_method = forms.ChoiceField( label=_('IPv4 Addressing Method'), widget=forms.RadioSelect, choices=[ ('auto', @@ -127,6 +158,7 @@ class ConnectionForm(forms.Form): 'name': self.cleaned_data['name'], 'interface': self.cleaned_data['interface'], 'zone': self.cleaned_data['zone'], + 'dns_over_tls': self.cleaned_data['dns_over_tls'], } settings['ipv4'] = self.get_ipv4_settings() settings['ipv6'] = self.get_ipv6_settings() @@ -191,6 +223,7 @@ class EthernetForm(ConnectionForm): class PPPoEForm(EthernetForm): """Form to create a new PPPoE connection.""" + dns_over_tls = None ipv4_method = None ipv4_address = None ipv4_netmask = None diff --git a/plinth/modules/networks/templates/connection_show.html b/plinth/modules/networks/templates/connection_show.html index f1993d992..8b53d8509 100644 --- a/plinth/modules/networks/templates/connection_show.html +++ b/plinth/modules/networks/templates/connection_show.html @@ -256,6 +256,15 @@

{% trans "This connection is not active." %}

{% endif %} +

{% trans "Privacy" %}

+ +
+
+ {% trans "DNS-over-TLS" %} + {{ connection.dns_over_tls_string }} +
+
+

{% trans "Security" %}

{% if connection.zone == "internal" %} diff --git a/plinth/modules/networks/templates/connections_fields.html b/plinth/modules/networks/templates/connections_fields.html index 4e4c38f92..99206e5c8 100644 --- a/plinth/modules/networks/templates/connections_fields.html +++ b/plinth/modules/networks/templates/connections_fields.html @@ -37,6 +37,10 @@ + {% if form.dns_over_tls %} + {% include "connections_fields_privacy.html" %} + {% endif %} + {% if form.ssid %} {% include "connections_fields_wifi.html" %} {% endif %} diff --git a/plinth/modules/networks/templates/connections_fields_privacy.html b/plinth/modules/networks/templates/connections_fields_privacy.html new file mode 100644 index 000000000..9e292fc08 --- /dev/null +++ b/plinth/modules/networks/templates/connections_fields_privacy.html @@ -0,0 +1,28 @@ +{% comment %} +# SPDX-License-Identifier: AGPL-3.0-or-later +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +
+
+

+ +

+
+ +
+
+ {{ form.dns_over_tls|bootstrap }} +
+
+
diff --git a/plinth/modules/networks/views.py b/plinth/modules/networks/views.py index 81c00f84d..70ad8f0a2 100644 --- a/plinth/modules/networks/views.py +++ b/plinth/modules/networks/views.py @@ -114,6 +114,16 @@ WIRELESS_MODE_STRINGS = { 'mesh': gettext_lazy('mesh point'), } +# i18n for connection.dns_over_tls +# https://networkmanager.dev/docs/libnm/latest/NMSettingConnection.html# +# NMSettingConnectionDnsOverTls +DNS_OVER_TLS_STRINGS = { + 'default': gettext_lazy('default'), + 'no': gettext_lazy('no'), + 'opportunistic': gettext_lazy('opportunistic'), + 'yes': gettext_lazy('yes'), +} + class NetworksAppView(AppView): """Show networks app main page.""" @@ -149,6 +159,8 @@ def show(request, uuid): connection_status = network.get_status_from_connection(connection) connection_status['zone_string'] = dict(network.ZONES).get( connection_status['zone'], connection_status['zone']) + connection_status['dns_over_tls_string'] = DNS_OVER_TLS_STRINGS.get( + connection_status['dns_over_tls'], connection_status['dns_over_tls']) connection_status['ipv4']['method_string'] = CONNECTION_METHOD_STRINGS.get( connection_status['ipv4']['method'], connection_status['ipv4']['method']) @@ -248,6 +260,9 @@ def edit(request, uuid): form_data['zone'] = 'external' if settings_connection.get_connection_type() != 'pppoe': + form_data['dns_over_tls'] = \ + settings_connection.get_dns_over_tls().value_nick + settings_ipv4 = connection.get_setting_ip4_config() form_data['ipv4_method'] = settings_ipv4.get_method() if settings_ipv4.get_num_addresses(): diff --git a/plinth/network.py b/plinth/network.py index 6e6fb105a..43dcb0399 100644 --- a/plinth/network.py +++ b/plinth/network.py @@ -101,6 +101,8 @@ def get_status_from_connection(connection): status['uuid'] = connection.get_uuid() status['type'] = connection.get_connection_type() status['zone'] = connection.get_setting_connection().get_zone() + status['dns_over_tls'] = \ + connection.get_setting_connection().get_dns_over_tls().value_nick status['interface_name'] = connection.get_interface_name() status['primary'] = _is_primary(connection) @@ -333,6 +335,16 @@ def _update_common_settings(connection, connection_uuid, common): if 'zone' in common: settings.set_property(nm.SETTING_CONNECTION_ZONE, common['zone']) + if 'dns_over_tls' in common: + values = { + 'default': nm.SettingConnectionDnsOverTls.DEFAULT, + 'no': nm.SettingConnectionDnsOverTls.NO, + 'opportunistic': nm.SettingConnectionDnsOverTls.OPPORTUNISTIC, + 'yes': nm.SettingConnectionDnsOverTls.YES + } + settings.set_property(nm.SETTING_CONNECTION_DNS_OVER_TLS, + values[common['dns_over_tls']]) + if 'autoconnect' in common: settings.set_property(nm.SETTING_CONNECTION_AUTOCONNECT, common['autoconnect']) diff --git a/plinth/tests/test_network.py b/plinth/tests/test_network.py index 3b029c4ce..1c6742a6d 100644 --- a/plinth/tests/test_network.py +++ b/plinth/tests/test_network.py @@ -17,6 +17,7 @@ ethernet_settings = { 'name': 'plinth_test_eth', 'interface': 'eth0', 'zone': 'internal', + 'dns_over_tls': 'opportunistic', }, 'ipv4': { 'method': 'auto', @@ -36,6 +37,7 @@ wifi_settings = { 'name': 'plinth_test_wifi', 'interface': 'wlan0', 'zone': 'external', + 'dns_over_tls': 'yes', }, 'ipv4': { 'method': 'auto', @@ -161,6 +163,7 @@ def test_edit_ethernet_connection(network, ethernet_uuid): ethernet_settings2['common']['name'] = 'plinth_test_eth_new' ethernet_settings2['common']['interface'] = 'eth1' ethernet_settings2['common']['zone'] = 'external' + ethernet_settings2['common']['dns_over_tls'] = 'no' ethernet_settings2['common']['autoconnect'] = False ethernet_settings2['ipv4']['method'] = 'auto' network.edit_connection(connection, ethernet_settings2) @@ -171,6 +174,7 @@ def test_edit_ethernet_connection(network, ethernet_uuid): settings_connection = connection.get_setting_connection() assert settings_connection.get_interface_name() == 'eth1' assert settings_connection.get_zone() == 'external' + assert settings_connection.get_dns_over_tls().value_nick == 'no' assert not settings_connection.get_autoconnect() settings_ipv4 = connection.get_setting_ip4_config() @@ -214,6 +218,7 @@ def test_edit_wifi_connection(network, wifi_uuid): wifi_settings2['common']['name'] = 'plinth_test_wifi_new' wifi_settings2['common']['interface'] = 'wlan1' wifi_settings2['common']['zone'] = 'external' + wifi_settings2['common']['dns_over_tls'] = 'opportunistic' wifi_settings2['common']['autoconnect'] = False wifi_settings2['ipv4']['method'] = 'auto' wifi_settings2['wireless']['ssid'] = 'plinthtestwifi2' @@ -229,6 +234,7 @@ def test_edit_wifi_connection(network, wifi_uuid): settings_connection = connection.get_setting_connection() assert settings_connection.get_interface_name() == 'wlan1' assert settings_connection.get_zone() == 'external' + assert settings_connection.get_dns_over_tls().value_nick == 'opportunistic' assert not settings_connection.get_autoconnect() settings_wireless = connection.get_setting_wireless()