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 <sunil@medhas.org>
Reviewed-by: Veiko Aasa <veiko17@disroot.org>
This commit is contained in:
Sunil Mohan Adapa 2024-09-03 14:02:15 -07:00 committed by Veiko Aasa
parent 01968d7d7e
commit 2abf2dc88c
No known key found for this signature in database
GPG Key ID: 478539CAE680674E
7 changed files with 107 additions and 0 deletions

View File

@ -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. <p '
'class="help-block">Use the global preference.</p>',
allow_markup=True)),
('yes',
format_lazy(
'Yes. Encrypt connections to the DNS server. <p '
'class="help-block">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.</p>',
allow_markup=True)),
('opportunistic',
format_lazy(
'Opportunistic. <p class="help-block">Encrypt connections to '
'the DNS server if the server supports DNS-over-TLS. '
'Otherwise, use unencrypted connections. There is no '
'protection against response manipulation.</p>',
allow_markup=True)),
('no',
format_lazy(
'No. <p class="help-block">Do not encrypt domain name '
'resolutions for this connection.</p>', 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

View File

@ -256,6 +256,15 @@
<p>{% trans "This connection is not active." %}</p>
{% endif %}
<h3>{% trans "Privacy" %}</h3>
<div class="list-group list-group-two-column">
<div class="list-group-item">
<span class="primary">{% trans "DNS-over-TLS" %}</span>
<span class="secondary">{{ connection.dns_over_tls_string }}</span>
</div>
</div>
<h3>{% trans "Security" %}</h3>
{% if connection.zone == "internal" %}

View File

@ -37,6 +37,10 @@
</div>
</div>
{% if form.dns_over_tls %}
{% include "connections_fields_privacy.html" %}
{% endif %}
{% if form.ssid %}
{% include "connections_fields_wifi.html" %}
{% endif %}

View File

@ -0,0 +1,28 @@
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
<div class='field-group'>
<div class="card-header" id="header-privacy">
<h4>
<button class="btn btn-block text-left dropdown-toggle {{ form.errors|yesno:",collapsed" }}"
type="button" data-toggle="collapse"
data-target="#collapse-privacy"
aria-expanded="{{ form.errors|yesno:"true,false" }}"
aria-controls="collapse-privacy">
{% trans "Privacy" %}
</button>
</h4>
</div>
<div id="collapse-privacy" class="collapse {{ form.errors|yesno:"show," }}"
aria-labelledby="header-privacy"
data-parent="#accordion-connections-fields">
<div class="card-body">
{{ form.dns_over_tls|bootstrap }}
</div>
</div>
</div>

View File

@ -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():

View File

@ -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'])

View File

@ -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()