[PATCH v2] systemd-resolved: Use DNS servers of the default service

Jussi Laakkonen <[email protected]> Mon, 26 Jan 2026 14:59:00 +0200
Newsgroups dev.linux.lists.connman
Message-ID <[email protected]>
Register to get the notifies about the default service, service state
and offline mode changes. When default service changes set it as
default route in resolved, revert other links' status and flush the cache
so only the default service's DNSs are used. This brings the similar
behavior that is is in dnsproxy.c, and prevents leaking of DNS queries
outside VPNs as well. Thus, effectively making only the DNS servers of
the default online service to be used.

To ensure that the servers are gone the domains and servers lists are
cleared when the service is disconnecting. The current default index is
being tracked and when it is being reset, the cache will be flushed as
well.

---
Changes since v2:
 - Send the correct patch file without the internal bug reference.

 src/dns-systemd-resolved.c | 340 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 337 insertions(+), 3 deletions(-)

diff --git a/src/dns-systemd-resolved.c b/src/dns-systemd-resolved.c
index 912ab3fe..9dd0328c 100644
--- a/src/dns-systemd-resolved.c
+++ b/src/dns-systemd-resolved.c
@@ -36,7 +36,7 @@
 #define SYSTEMD_RESOLVED_SERVICE "org.freedesktop.resolve1"
 #define SYSTEMD_RESOLVED_PATH "/org/freedesktop/resolve1"
 
-struct mdns_data {
+struct resolved_dbus_data {
 	int index;
 	bool enabled;
 };
@@ -48,6 +48,7 @@ static GDBusProxy *resolved_proxy;
 
 /* update after a full set of instructions has been received */
 static guint update_interfaces_source;
+static gint current_default_index = -1;
 
 struct dns_interface {
 	GList *domains;
@@ -55,6 +56,7 @@ struct dns_interface {
 	int index;
 	bool needs_domain_update;
 	bool needs_server_update;
+	bool enabled;
 };
 
 static gboolean compare_index(gconstpointer a, gconstpointer b)
@@ -405,7 +407,7 @@ static int setup_resolved(void)
 }
 
 static void setlinkmulticastdns_append(DBusMessageIter *iter, void *user_data) {
-	struct mdns_data *data = user_data;
+	struct resolved_dbus_data *data = user_data;
 	char *val = "no";
 
 	if (data->enabled)
@@ -419,7 +421,7 @@ static void setlinkmulticastdns_append(DBusMessageIter *iter, void *user_data) {
 
 int __connman_dnsproxy_set_mdns(int index, bool enabled)
 {
-	struct mdns_data data = { .index = index, .enabled = enabled };
+	struct resolved_dbus_data data = { .index = index, .enabled = enabled };
 
 	if (!resolved_proxy)
 		return -ENOENT;
@@ -434,6 +436,332 @@ int __connman_dnsproxy_set_mdns(int index, bool enabled)
 	return 0;
 }
 
+static void setlinkdefaultroute_append(DBusMessageIter *iter, void *user_data)
+{
+	struct resolved_dbus_data *data = user_data;
+	dbus_bool_t val = data->enabled;
+
+	DBG("SetLinkDefaultRoute: %d/%s", data->index, val ? "yes" : "no");
+
+	dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &data->index);
+	dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &val);
+}
+
+static int resolved_set_default_route(int index, bool enabled)
+{
+	struct resolved_dbus_data data = { .index = index, .enabled = enabled };
+
+	if (!resolved_proxy)
+		return -ENOENT;
+
+	if (index < 0)
+		return -ENODEV;
+
+	if (!g_dbus_proxy_method_call(resolved_proxy, "SetLinkDefaultRoute",
+			setlinkdefaultroute_append, NULL, &data, NULL))
+		return -EINVAL;
+
+	return 0;
+}
+
+static int resolved_flush_cache()
+{
+	DBG("");
+
+	if (!resolved_proxy)
+		return -ENOENT;
+
+	if (!g_dbus_proxy_method_call(resolved_proxy, "FlushCaches",
+			NULL, NULL, NULL, NULL))
+		return -EINVAL;
+
+	return 0;
+}
+
+static void setrevertlink_append(DBusMessageIter *iter, void *user_data)
+{
+	int index = GPOINTER_TO_INT(user_data);
+
+	DBG("RevertLink: %d", index);
+
+	dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &index);
+}
+
+static int resolved_disable_interface(int index)
+{
+	if (!resolved_proxy)
+		return -ENOENT;
+
+	if (index < 0)
+		return -ENODEV;
+
+	if (!g_dbus_proxy_method_call(resolved_proxy, "RevertLink",
+			setrevertlink_append, NULL, GINT_TO_POINTER(index),
+			NULL))
+		return -EINVAL;
+
+	return 0;
+}
+
+struct interface_search_data {
+	bool enable;
+	bool forced;
+	bool disconnected;
+	int index4;
+	int index6;
+	int vpn_index;
+};
+
+static bool interface_index_match(int index,
+				struct interface_search_data *search_data)
+{
+	if (search_data->forced)
+		return true;
+
+	if (index == search_data->index4 || index == search_data->index6) {
+		DBG("interface %d match", index);
+		return true;
+	}
+
+	if (index == search_data->vpn_index) {
+		DBG("VPN interface %d match", index);
+		return true;
+	}
+
+	return false;
+}
+
+static void toggle_interface_dns(struct dns_interface *iface, bool enable)
+{
+	DBG("%s interface %p/%d", enable ? "enable" : "disable", iface,
+								iface->index);
+
+	if (iface->enabled == enable) {
+		DBG("already %s", enable ? "enabled" : "disabled");
+		return;
+	}
+
+	iface->enabled = enable;
+
+	if (!enable) {
+		DBG("disabling interface %d", iface->index);
+		resolved_disable_interface(iface->index);
+	} else {
+		DBG("re-enabling interface %d", iface->index);
+		set_systemd_resolved_values(iface);
+	}
+
+	if (is_empty(iface)) {
+		DBG("removing interface %d", iface->index);
+		g_hash_table_remove(interface_hash,
+					GUINT_TO_POINTER(iface->index));
+	}
+}
+
+static void check_interface(gpointer key, gpointer value, gpointer user_data)
+{
+	struct dns_interface *iface = value;
+	struct interface_search_data *search_data = user_data;
+
+	if (interface_index_match(iface->index, search_data)) {
+		/*
+		 * Clear domains and servers before updating the status to
+		 * resolved when interface is disconnected.
+		 */
+		if (!search_data->enable && search_data->disconnected) {
+			DBG("clear domains and servers of %d", iface->index);
+
+			g_list_free_full(iface->domains, g_free);
+			iface->domains = NULL;
+
+			g_list_free_full(iface->servers, g_free);
+			iface->servers = NULL;
+		}
+
+		toggle_interface_dns(iface, search_data->enable);
+	/* If DNSs of an interface are to be enabled disable the others */
+	} else if (search_data->enable) {
+		toggle_interface_dns(iface, false);
+	} else {
+		return;
+	}
+
+	if (!update_interfaces_source)
+		update_interfaces_source = g_idle_add(update_systemd_resolved,
+									NULL);
+}
+
+static void resolved_offline_mode(bool enabled)
+{
+	struct interface_search_data search_data = { 0 };
+	int err;
+
+	DBG("enabled %d", enabled);
+
+	search_data.enable = !enabled;
+	search_data.forced = true;
+
+	g_hash_table_foreach(interface_hash, check_interface, &search_data);
+
+	/* Offline mode is not on, skip default unset and flushing of cache */
+	if (!enabled)
+		return;
+
+	err = resolved_set_default_route(current_default_index, false);
+	if (err && err != -ENODEV)
+		DBG("failed to unset interface %d as default route",
+							current_default_index);
+
+	current_default_index = -1;
+
+	resolved_flush_cache();
+}
+
+static void resolved_default_changed(struct connman_service *service)
+{
+	struct connman_ipconfig *ipconfig;
+	struct interface_search_data search_data = { 0 };
+	int index;
+	int err;
+
+	DBG("service %p interface %d", service,
+					__connman_service_get_index(service));
+
+	if (!service) {
+		/* When no services are active, then disable DNSs */
+		resolved_offline_mode(true);
+		return;
+	}
+
+	if (connman_service_get_type(service) == CONNMAN_SERVICE_TYPE_VPN) {
+		/* VPN is either or take both to be sure */
+		search_data.index4 = search_data.index6 = -1;
+		search_data.vpn_index = __connman_service_get_index(service);
+
+		if (search_data.vpn_index < 0)
+			return;
+
+		index = search_data.vpn_index;
+	} else {
+		ipconfig = __connman_service_get_ip4config(service);
+		search_data.index4 = __connman_ipconfig_get_index(ipconfig);
+
+		ipconfig = __connman_service_get_ip6config(service);
+		search_data.index6 = __connman_ipconfig_get_index(ipconfig);
+
+		if (search_data.index4 < 0 && search_data.index6 < 0)
+			return;
+
+		/*
+		 * In case non-split-routed VPN is set as split routed the DNS
+		 * servers the VPN must be enabled as well, when the transport
+		 * becomes the default service. Try first with IPv4 and then
+		 * attempt using IPv6 if not set. VPN can be either or.
+		 */
+		search_data.vpn_index = __connman_gateway_get_vpn_index(
+							search_data.index4);
+		if (search_data.vpn_index < 0)
+			search_data.vpn_index =
+					__connman_gateway_get_vpn_index(
+							search_data.index6);
+
+		if (search_data.index4 == search_data.index6)
+			index = search_data.index4;
+		else
+			index = search_data.index6; // Prefer IPv6, as usual.
+	}
+
+	if (current_default_index != index) {
+		err = resolved_set_default_route(current_default_index, false);
+		if (err && err != -ENODEV)
+			DBG("failed to unset %d as default",
+							current_default_index);
+
+		current_default_index = -1;
+
+		err = resolved_set_default_route(index, true);
+		if (err && err != -ENODEV)
+			DBG("failed to set %d as default", index);
+		else
+			current_default_index = index;
+	}
+
+	search_data.enable = true;
+	g_hash_table_foreach(interface_hash, check_interface, &search_data);
+
+	resolved_flush_cache();
+}
+
+static void resolved_service_state_changed(struct connman_service *service,
+			enum connman_service_state state)
+{
+	struct connman_ipconfig *ipconfig;
+	struct interface_search_data search_data = { 0 };
+	int index;
+	int err;
+
+	index = __connman_service_get_index(service);
+	DBG("service %p interface %d state %d", service, index, state);
+
+	switch (state) {
+	case CONNMAN_SERVICE_STATE_DISCONNECT:
+	case CONNMAN_SERVICE_STATE_IDLE:
+	case CONNMAN_SERVICE_STATE_FAILURE:
+	case CONNMAN_SERVICE_STATE_UNKNOWN:
+		DBG("disable interface %d", index);
+		break;
+	case CONNMAN_SERVICE_STATE_ASSOCIATION:
+	case CONNMAN_SERVICE_STATE_CONFIGURATION:
+	case CONNMAN_SERVICE_STATE_ONLINE:
+	case CONNMAN_SERVICE_STATE_READY:
+		DBG("connecting/connected mode, skip interface %d", index);
+		return;
+	}
+
+	ipconfig = __connman_service_get_ip4config(service);
+	search_data.index4 = __connman_ipconfig_get_index(ipconfig);
+
+	ipconfig = __connman_service_get_ip6config(service);
+	search_data.index6 = __connman_ipconfig_get_index(ipconfig);
+
+	if (current_default_index == search_data.index4)
+		index = search_data.index4;
+	else if (current_default_index == search_data.index6)
+		index = search_data.index6;
+	else
+		index = -1;
+
+	if (index != -1) {
+		err = resolved_set_default_route(index, false);
+		if (err && err != -ENODEV)
+			DBG("failed to unset %d as default route", index);
+
+		current_default_index = -1;
+		resolved_flush_cache(); // Do when index is unset
+	}
+
+	/*
+	 * Disable DNS servers when the service is not connected and clear the
+	 * domains and servers. Use both indexes as service can have two
+	 * separate interfaces, both of which can be registered to resolved.
+	 */
+	search_data.enable = false;
+	search_data.disconnected = true;
+
+	g_hash_table_foreach(interface_hash, check_interface, &search_data);
+
+	if (!update_interfaces_source)
+		update_interfaces_source = g_idle_add(update_systemd_resolved,
+									NULL);
+}
+
+static struct connman_notifier resolved_notifier = {
+	.name			= "systemd-resolved",
+	.default_changed	= resolved_default_changed,
+	.offline_mode		= resolved_offline_mode,
+	.service_state_changed	= resolved_service_state_changed,
+};
+
 int __connman_dnsproxy_init(void)
 {
 	int ret;
@@ -451,6 +779,10 @@ int __connman_dnsproxy_init(void)
 	if (!interface_hash)
 		return -ENOMEM;
 
+	ret = connman_notifier_register(&resolved_notifier);
+	if (ret < 0)
+		DBG("cannot register notifier");
+
 	return 0;
 }
 
@@ -458,6 +790,8 @@ void __connman_dnsproxy_cleanup(void)
 {
 	DBG("");
 
+	connman_notifier_unregister(&resolved_notifier);
+
 	if (update_interfaces_source) {
 		/*
 		 * It might be that we don't get to an idle loop anymore, so
-- 
2.47.3