From 2abf2dc88c679a0cb3ec69d5e9f624dd4428c715 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 3 Sep 2024 14:02:15 -0700 Subject: [PATCH] networks: Add support for DNS-over-TLS for individual connections - Expose Network Manager per-connection setting for DNS-over-TLS. Support all four values: default, no, opportunistic, and yes. - Create a new collapsible section all 'Privacy' for this setting the connection create/edit form. Strictly speaking this is related to security and censorship resistance too. - Don't show the DoT field for PPPoE connection types are DNS servers are not relevant. - Show the status of DoT for a connection in the connection status page. Tests: - In all Add New Connection forms except PPPoE form, the privacy section shows up as expected. - For each value for DoT, create a new connection and set the value for DoT to the desired value and observe that the connection status page shows DoT to the set value. - For each value for DoT, edit an existing connection and set the value for the DoT to the desired value and observe that the connection status page shows DoT to the set value. - Connection status page shows the values for DoT as expected. - Update the primary Internet connection for the machine. Set the value to 'yes' and notice that DNS resolutions fail. Set the value to 'opportunistic' or 'no' and the DNS resolutions pass. In each case, 'resolvectl' shows the correct DoT value for the connection. When 1.1.1.1 is set as DNS server, all values of DoT in the connection succeed. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/networks/forms.py | 33 +++++++++++++++++++ .../networks/templates/connection_show.html | 9 +++++ .../templates/connections_fields.html | 4 +++ .../templates/connections_fields_privacy.html | 28 ++++++++++++++++ plinth/modules/networks/views.py | 15 +++++++++ plinth/network.py | 12 +++++++ plinth/tests/test_network.py | 6 ++++ 7 files changed, 107 insertions(+) create mode 100644 plinth/modules/networks/templates/connections_fields_privacy.html 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()