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 %}
+
+
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()