diff --git a/.reuse/dep5 b/.reuse/dep5 index 55c1e6d3b..a64bb3d4e 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -48,6 +48,11 @@ Files: dss-network-plugin/50-dss-network-plugin.rules src/dde-network-core.pc.in Copyright: UnionTech Software Technology Co., Ltd. License: LGPL-3.0-or-later +# policy +Files: src/misc/*.policy +Copyright: UnionTech Software Technology Co., Ltd. +License: LGPL-3.0-or-later + # sh Files: *.sh Copyright: UnionTech Software Technology Co., Ltd. diff --git a/dcc-network/operation/dccnetwork.cpp b/dcc-network/operation/dccnetwork.cpp index 887dbb06f..4e2b99ff3 100644 --- a/dcc-network/operation/dccnetwork.cpp +++ b/dcc-network/operation/dccnetwork.cpp @@ -12,6 +12,8 @@ #include #include +#include +#include #include #include #include @@ -182,6 +184,11 @@ bool DccNetwork::netCheckAvailable() return m_manager->netCheckAvailable(); } +bool DccNetwork::resolvedAvailable() const +{ + return QDBusConnection::systemBus().interface()->isServiceRegistered("org.freedesktop.resolve1"); +} + QVariantMap DccNetwork::toMap(QMap map) { QVariantMap retMap; diff --git a/dcc-network/operation/dccnetwork.h b/dcc-network/operation/dccnetwork.h index 6e5879876..17d1e30bc 100644 --- a/dcc-network/operation/dccnetwork.h +++ b/dcc-network/operation/dccnetwork.h @@ -20,11 +20,13 @@ class DccNetwork : public QObject Q_OBJECT Q_PROPERTY(NetManager* manager READ manager NOTIFY managerChanged) Q_PROPERTY(NetItem* root READ root NOTIFY rootChanged) + Q_PROPERTY(bool resolvedAvailable READ resolvedAvailable NOTIFY resolvedAvailableChanged) public: explicit DccNetwork(QObject *parent = nullptr); ~DccNetwork() override; NetItem *root() const; + bool resolvedAvailable() const; Q_INVOKABLE static bool CheckPasswordValid(const QString &key, const QString &password); NetManager *manager() const { return m_manager; } @@ -42,6 +44,7 @@ public Q_SLOTS: void managerChanged(NetManager *manager); void rootChanged(); + void resolvedAvailableChanged(); protected Q_SLOTS: void init(); diff --git a/dcc-network/qml/PageVPNSettings.qml b/dcc-network/qml/PageVPNSettings.qml index 61afab965..77344486e 100644 --- a/dcc-network/qml/PageVPNSettings.qml +++ b/dcc-network/qml/PageVPNSettings.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 - 2027 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // SPDX-License-Identifier: GPL-3.0-or-later import QtQuick 2.15 import QtQuick.Layouts 1.15 @@ -132,6 +132,7 @@ DccObject { type: NetType.VPNControlItem parentName: root.parentUrl + "/body" weight: 1000 + resolvedAvailable: dccData ? dccData.resolvedAvailable : false onEditClicked: modified = true } SectionIPv6 { @@ -140,8 +141,21 @@ DccObject { parentName: root.parentUrl + "/body" visible: vpnType & (NetUtils.VpnTypeEnum["openvpn"] | NetUtils.VpnTypeEnum["openconnect"]) weight: 1100 + resolvedAvailable: dccData ? dccData.resolvedAvailable : false onEditClicked: modified = true } + Connections { + target: sectionIPv4 + function onDnsPriorityChanged(priority) { + sectionIPv6.setDnsPriority(priority) + } + } + Connections { + target: sectionIPv6 + function onDnsPriorityChanged(priority) { + sectionIPv4.setDnsPriority(priority) + } + } SectionDNS { id: sectionDNS parentName: root.parentUrl + "/body" diff --git a/dcc-network/qml/SectionIPv4.qml b/dcc-network/qml/SectionIPv4.qml index 8fc92aa95..390f8bb4a 100644 --- a/dcc-network/qml/SectionIPv4.qml +++ b/dcc-network/qml/SectionIPv4.qml @@ -22,7 +22,10 @@ DccObject { property string errorKey: "" property string errorMsg: "" + property bool neverDefault: false + property bool resolvedAvailable: false signal editClicked + signal dnsPriorityChanged(int priority) function setConfig(c) { errorKey = "" @@ -33,6 +36,7 @@ DccObject { } else { root.config = c method = root.config.method + neverDefault = root.config && root.config["never-default"] === true resetAddressData() } } @@ -141,6 +145,11 @@ DccObject { addressDataChanged() editClicked() } + function setDnsPriority(dp) { + root.config["dns-priority"] = dp + ipv4DnsMode.dnsPriority = dp + root.editClicked() + } name: "ipv4Title" displayName: qsTr("IPv4") @@ -245,17 +254,69 @@ DccObject { weight: root.weight + 90 visible: type === NetType.VPNControlItem displayName: qsTr("Only applied in corresponding resources") + description: qsTr("When enabled, only traffic to the target network is routed through the VPN. Other traffic continues to use the local network connection.") canSearch: false - pageType: DccObject.Item - page: D.CheckBox { - text: dccObj.displayName - checked: root.config && root.config.hasOwnProperty("never-default") ? root.config["never-default"] : false + backgroundType: DccObject.Normal + pageType: DccObject.Editor + page: D.Switch { + checked: root.neverDefault onClicked: { + root.neverDefault = checked root.config["never-default"] = checked root.editClicked() } } } + + DccObject { + id: ipv4DnsMode + property int dnsPriority: 0 + readonly property int dnsModeIndex: dnsPriority === 100 ? 1 : dnsPriority === -100 ? 2 : 0 + readonly property int dnsPriorityFromConfig: root.config && root.config["dns-priority"] !== undefined ? root.config["dns-priority"] : 0 + name: "ipv4DnsMode" + parentName: root.parentName + weight: root.weight + 95 + visible: type === NetType.VPNControlItem && root.neverDefault && root.resolvedAvailable + displayName: qsTr("VPN DNS Mode") + description: { + switch (dnsPriority) { + case 100: return qsTr("The system uses both local DNS and VPN DNS for resolution, and prefers the result returned first.") + case -100: return qsTr("Prefer VPN DNS. All DNS queries are sent through the VPN connection.") + default: return qsTr("Do not specify how VPN DNS is used. Keep the current system DNS resolution policy.") + } + } + canSearch: false + backgroundType: DccObject.Normal + pageType: DccObject.Editor + page: ComboBox { + flat: true + textRole: "text" + valueRole: "value" + currentIndex: ipv4DnsMode.dnsModeIndex + + model: [ + { value: 0, text: qsTr("Not Set") }, + { value: 100, text: qsTr("Secondary") }, + { value: -100, text: qsTr("Preferred") } + ] + onActivated: { + root.config["dns-priority"] = currentValue + ipv4DnsMode.dnsPriority = currentValue + root.editClicked() + root.dnsPriorityChanged(currentValue) + } + Component.onCompleted: { + ipv4DnsMode.dnsPriority = ipv4DnsMode.dnsPriorityFromConfig + } + Connections { + target: root + function onConfigChanged() { + ipv4DnsMode.dnsPriority = ipv4DnsMode.dnsPriorityFromConfig + } + } + } + } + DccRepeater { model: root.addressData delegate: DccObject { diff --git a/dcc-network/qml/SectionIPv6.qml b/dcc-network/qml/SectionIPv6.qml index e810069d5..8424be977 100644 --- a/dcc-network/qml/SectionIPv6.qml +++ b/dcc-network/qml/SectionIPv6.qml @@ -23,7 +23,17 @@ DccObject { property string errorKey: "" property string errorMsg: "" + property bool neverDefault: false + property bool resolvedAvailable: false + signal editClicked + signal dnsPriorityChanged(int priority) + + function setDnsPriority(dp) { + root.config["dns-priority"] = dp + ipv6DnsMode.dnsPriority = dp + root.editClicked() + } function setConfig(c) { errorKey = "" @@ -35,6 +45,7 @@ DccObject { } else { root.config = c method = root.config.method + neverDefault = root.config && root.config["never-default"] === true addressData = (root.config && root.config.hasOwnProperty("address-data")) ? root.config["address-data"] : [] gateway = [] gateway[0] = (root.config && root.config.hasOwnProperty("gateway")) ? root.config["gateway"] : "" @@ -246,17 +257,67 @@ DccObject { weight: root.weight + 90 visible: root.visible && type === NetType.VPNControlItem displayName: qsTr("Only applied in corresponding resources") + description: qsTr("When enabled, only traffic to the target network is routed through the VPN. Other traffic continues to use the local network connection.") canSearch: false - pageType: DccObject.Item - page: D.CheckBox { - text: dccObj.displayName - checked: root.config.hasOwnProperty("never-default") ? root.config["never-default"] : false + backgroundType: DccObject.Normal + pageType: DccObject.Editor + page: D.Switch { + checked: root.neverDefault onClicked: { + root.neverDefault = checked root.config["never-default"] = checked root.editClicked() } } } + DccObject { + id: ipv6DnsMode + property int dnsPriority: 0 + readonly property int dnsModeIndex: dnsPriority === 100 ? 1 : dnsPriority === -100 ? 2 : 0 + readonly property int dnsPriorityFromConfig: root.config && root.config["dns-priority"] !== undefined ? root.config["dns-priority"] : 0 + name: "ipv6DnsMode" + parentName: root.parentName + weight: root.weight + 95 + visible: root.visible && root.neverDefault && root.resolvedAvailable + displayName: qsTr("VPN DNS Mode") + description: { + switch (dnsPriority) { + case 100: return qsTr("The system uses both local DNS and VPN DNS for resolution, and prefers the result returned first.") + case -100: return qsTr("Prefer VPN DNS. All DNS queries are sent through the VPN connection.") + default: return qsTr("Do not specify how VPN DNS is used. Keep the current system DNS resolution policy.") + } + } + canSearch: false + backgroundType: DccObject.Normal + pageType: DccObject.Editor + page: ComboBox { + flat: true + textRole: "text" + valueRole: "value" + currentIndex: ipv6DnsMode.dnsModeIndex + + model: [ + { value: 0, text: qsTr("Not Set") }, + { value: 100, text: qsTr("Secondary") }, + { value: -100, text: qsTr("Preferred") } + ] + onActivated: { + root.config["dns-priority"] = currentValue + ipv6DnsMode.dnsPriority = currentValue + root.editClicked() + root.dnsPriorityChanged(currentValue) + } + Component.onCompleted: { + ipv6DnsMode.dnsPriority = ipv6DnsMode.dnsPriorityFromConfig + } + Connections { + target: root + function onConfigChanged() { + ipv6DnsMode.dnsPriority = ipv6DnsMode.dnsPriorityFromConfig + } + } + } + } DccRepeater { model: root.addressData delegate: DccObject { diff --git a/dcc-network/translations/network_en.ts b/dcc-network/translations/network_en.ts index 4217f9caf..b562e305a 100644 --- a/dcc-network/translations/network_en.ts +++ b/dcc-network/translations/network_en.ts @@ -695,6 +695,38 @@ Only one gateway is allowed + + When enabled, only traffic to the target network is routed through the VPN. Other traffic continues to use the local network connection. + + + + VPN DNS Mode + + + + The system uses both local DNS and VPN DNS for resolution, and prefers the result returned first. + + + + Prefer VPN DNS. All DNS queries are sent through the VPN connection. + + + + Do not specify how VPN DNS is used. Keep the current system DNS resolution policy. + + + + Not Set + + + + Secondary + + + + Preferred + + SectionIPv6 @@ -766,6 +798,38 @@ Only one gateway is allowed + + When enabled, only traffic to the target network is routed through the VPN. Other traffic continues to use the local network connection. + + + + VPN DNS Mode + + + + The system uses both local DNS and VPN DNS for resolution, and prefers the result returned first. + + + + Prefer VPN DNS. All DNS queries are sent through the VPN connection. + + + + Do not specify how VPN DNS is used. Keep the current system DNS resolution policy. + + + + Not Set + + + + Secondary + + + + Preferred + + SectionPPP diff --git a/dcc-network/translations/network_zh_CN.ts b/dcc-network/translations/network_zh_CN.ts index 812266146..d198635ee 100644 --- a/dcc-network/translations/network_zh_CN.ts +++ b/dcc-network/translations/network_zh_CN.ts @@ -703,6 +703,38 @@ Only one gateway is allowed 只允许一个网关 + + When enabled, only traffic to the target network is routed through the VPN. Other traffic continues to use the local network connection. + 开启后,仅目标网络流量通过 VPN,其他流量仍使用本地网络连接。 + + + VPN DNS Mode + VPN DNS 模式 + + + The system uses both local DNS and VPN DNS for resolution, and prefers the result returned first. + 系统会同时使用本地 DNS 和 VPN DNS 进行解析,并优先采用先返回的结果。 + + + Prefer VPN DNS. All DNS queries are sent through the VPN connection. + 优先使用 VPN DNS,所有 DNS 解析请求将通过 VPN 连接发送。 + + + Do not specify how VPN DNS is used. Keep the current system DNS resolution policy. + 不额外指定 VPN DNS 的使用方式,保持系统当前 DNS 解析策略。 + + + Not Set + 不设置 + + + Secondary + 作为备选 + + + Preferred + 作为首选 + SectionIPv6 @@ -774,6 +806,38 @@ Only one gateway is allowed 只允许一个网关 + + When enabled, only traffic to the target network is routed through the VPN. Other traffic continues to use the local network connection. + 开启后,仅目标网络流量通过 VPN,其他流量仍使用本地网络连接。 + + + VPN DNS Mode + VPN DNS 模式 + + + The system uses both local DNS and VPN DNS for resolution, and prefers the result returned first. + 系统会同时使用本地 DNS 和 VPN DNS 进行解析,并优先采用先返回的结果。 + + + Prefer VPN DNS. All DNS queries are sent through the VPN connection. + 优先使用 VPN DNS,所有 DNS 解析请求将通过 VPN 连接发送。 + + + Do not specify how VPN DNS is used. Keep the current system DNS resolution policy. + 不额外指定 VPN DNS 的使用方式,保持系统当前 DNS 解析策略。 + + + Not Set + 不设置 + + + Secondary + 作为备选 + + + Preferred + 作为首选 + SectionPPP diff --git a/dcc-network/translations/network_zh_HK.ts b/dcc-network/translations/network_zh_HK.ts index 652271d04..e59431196 100644 --- a/dcc-network/translations/network_zh_HK.ts +++ b/dcc-network/translations/network_zh_HK.ts @@ -703,6 +703,38 @@ Only one gateway is allowed 只允許一個網關 + + When enabled, only traffic to the target network is routed through the VPN. Other traffic continues to use the local network connection. + 開啟後,僅目標網絡流量通過 VPN,其他流量仍使用本地網絡連接。 + + + VPN DNS Mode + VPN DNS 模式 + + + The system uses both local DNS and VPN DNS for resolution, and prefers the result returned first. + 系統會同時使用本地 DNS 和 VPN DNS 進行解析,並優先採用先返回的結果。 + + + Prefer VPN DNS. All DNS queries are sent through the VPN connection. + 優先使用 VPN DNS,所有 DNS 解析請求將通過 VPN 連接發送。 + + + Do not specify how VPN DNS is used. Keep the current system DNS resolution policy. + 不額外指定 VPN DNS 的使用方式,保持系統當前 DNS 解析策略。 + + + Not Set + 不設定 + + + Secondary + 作為備選 + + + Preferred + 作為首選 + SectionIPv6 @@ -774,6 +806,38 @@ Only one gateway is allowed 只允許一個網關 + + When enabled, only traffic to the target network is routed through the VPN. Other traffic continues to use the local network connection. + 開啟後,僅目標網絡流量通過 VPN,其他流量仍使用本地網絡連接。 + + + VPN DNS Mode + VPN DNS 模式 + + + The system uses both local DNS and VPN DNS for resolution, and prefers the result returned first. + 系統會同時使用本地 DNS 和 VPN DNS 進行解析,並優先採用先返回的結果。 + + + Prefer VPN DNS. All DNS queries are sent through the VPN connection. + 優先使用 VPN DNS,所有 DNS 解析請求將通過 VPN 連接發送。 + + + Do not specify how VPN DNS is used. Keep the current system DNS resolution policy. + 不額外指定 VPN DNS 的使用方式,保持系統當前 DNS 解析策略。 + + + Not Set + 不設定 + + + Secondary + 作為備選 + + + Preferred + 作為首選 + SectionPPP diff --git a/dcc-network/translations/network_zh_TW.ts b/dcc-network/translations/network_zh_TW.ts index ba55bd06e..c52b861da 100644 --- a/dcc-network/translations/network_zh_TW.ts +++ b/dcc-network/translations/network_zh_TW.ts @@ -703,6 +703,38 @@ Only one gateway is allowed 只允許一個閘道器 + + When enabled, only traffic to the target network is routed through the VPN. Other traffic continues to use the local network connection. + 開啟後,僅目標網路流量透過 VPN,其他流量仍使用本地網路連線。 + + + VPN DNS Mode + VPN DNS 模式 + + + The system uses both local DNS and VPN DNS for resolution, and prefers the result returned first. + 系統會同時使用本地 DNS 和 VPN DNS 進行解析,並優先採用先回傳的結果。 + + + Prefer VPN DNS. All DNS queries are sent through the VPN connection. + 優先使用 VPN DNS,所有 DNS 解析請求將透過 VPN 連線發送。 + + + Do not specify how VPN DNS is used. Keep the current system DNS resolution policy. + 不額外指定 VPN DNS 的使用方式,保持系統當前 DNS 解析策略。 + + + Not Set + 不設定 + + + Secondary + 作為備選 + + + Preferred + 作為首選 + SectionIPv6 @@ -774,6 +806,38 @@ Only one gateway is allowed 只允許一個閘道器 + + When enabled, only traffic to the target network is routed through the VPN. Other traffic continues to use the local network connection. + 開啟後,僅目標網路流量透過 VPN,其他流量仍使用本地網路連線。 + + + VPN DNS Mode + VPN DNS 模式 + + + The system uses both local DNS and VPN DNS for resolution, and prefers the result returned first. + 系統會同時使用本地 DNS 和 VPN DNS 進行解析,並優先採用先回傳的結果。 + + + Prefer VPN DNS. All DNS queries are sent through the VPN connection. + 優先使用 VPN DNS,所有 DNS 解析請求將透過 VPN 連線發送。 + + + Do not specify how VPN DNS is used. Keep the current system DNS resolution policy. + 不額外指定 VPN DNS 的使用方式,保持系統當前 DNS 解析策略。 + + + Not Set + 不設定 + + + Secondary + 作為備選 + + + Preferred + 作為首選 + SectionPPP diff --git a/net-view/operation/private/netmanagerthreadprivate.cpp b/net-view/operation/private/netmanagerthreadprivate.cpp index 054cb8118..c7f8f28fc 100644 --- a/net-view/operation/private/netmanagerthreadprivate.cpp +++ b/net-view/operation/private/netmanagerthreadprivate.cpp @@ -1465,6 +1465,23 @@ void NetManagerThreadPrivate::doGetConnectInfo(const QString &id, NetType::NetIt typeMap.insert("optionalDevice", optionalDevice); retParam[deviceKey] = typeMap; } + // For VPN connections, ensure ipv6.dns-priority is preserved + // (NMQt's Ipv6Setting doesn't expose this field; ipv4 has native support) + if (settings->connectionType() == ConnectionSettings::Vpn) { + auto msg = QDBusMessage::createMethodCall("org.freedesktop.NetworkManager", conn->path(), "org.freedesktop.NetworkManager.Settings.Connection", "GetSettings"); + QDBusPendingReply dbusSettingsReply = QDBusConnection::systemBus().call(msg, QDBus::Block, 100); + if (!dbusSettingsReply.isError()) { + const NMVariantMapMap &rawSettings = dbusSettingsReply.value(); + if (rawSettings.contains("ipv6")) { + int dp = rawSettings["ipv6"].value("dns-priority", 0).toInt(); + if (dp != 0) { + QVariantMap ipv6Map = retParam.value("ipv6").toMap(); + ipv6Map["dns-priority"] = dp; + retParam["ipv6"] = ipv6Map; + } + } + } + } if (param.value("check").toBool()) { retParam.insert("check", true); } @@ -1600,8 +1617,20 @@ void NetManagerThreadPrivate::doSetConnectInfo(const QString &id, NetType::NetIt if (!settings->autoconnect()) { usedAdd = true; } + NMVariantMapMap addSettings = settings->toMap(); + + // ipv6.dns-priority is NOT preserved (NMQt's Ipv6Setting doesn't expose it) + { + const QVariantMap &ipv6Param = map.value("ipv6"); + if (ipv6Param.contains("dns-priority")) { + QVariantMap ipv6Map = addSettings.value("ipv6"); + ipv6Map["dns-priority"] = ipv6Param.value("dns-priority"); + addSettings["ipv6"] = ipv6Map; + } + } + if (usedAdd || devPath.isEmpty()) { - reply = NetworkManager::addConnection(settings->toMap()); + reply = NetworkManager::addConnection(addSettings); } else { // 其他场景 QVariantMap options; @@ -1609,7 +1638,7 @@ void NetManagerThreadPrivate::doSetConnectInfo(const QString &id, NetType::NetIt options.insert("persist", "memory"); options.insert("flags", MANULCONNECTION); } - reply = NetworkManager::addAndActivateConnection2(settings->toMap(), devPath, QString(), options); + reply = NetworkManager::addAndActivateConnection2(addSettings, devPath, QString(), options); } reply.waitForFinished(); const QString &connPath = reply.argumentAt(0).value().path(); @@ -1627,6 +1656,16 @@ void NetManagerThreadPrivate::doSetConnectInfo(const QString &id, NetType::NetIt } NMVariantMapMap finalSettings = settings->toMap(); + // ipv6.dns-priority is NOT preserved (NMQt's Ipv6Setting doesn't expose it) + { + const QVariantMap &ipv6Param = map.value("ipv6"); + if (ipv6Param.contains("dns-priority")) { + QVariantMap ipv6Map = finalSettings.value("ipv6"); + ipv6Map["dns-priority"] = ipv6Param.value("dns-priority"); + finalSettings["ipv6"] = ipv6Map; + } + } + QDBusPendingReply<> reply = connection->isUnsaved() ? connection->updateUnsaved(finalSettings) : connection->update(finalSettings); reply.waitForFinished(); if (reply.isError()) { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7b7c6e0ad..e85be0ab1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,7 @@ find_package(Qt6 COMPONENTS Core DBus Network Gui LinguistTools REQUIRED) find_package(PkgConfig REQUIRED) find_package(Dtk6 COMPONENTS Core REQUIRED) find_package(KF6NetworkManagerQt REQUIRED) +find_package(PolkitQt6-1 REQUIRED) pkg_check_modules(LibNM REQUIRED IMPORTED_TARGET libnm) @@ -54,6 +55,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE Dtk6::Core KF6::NetworkManagerQt PkgConfig::LibNM + PolkitQt6-1::Core udev ) @@ -84,3 +86,6 @@ install(FILES ${INTERFACEFILES} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/libddene install(FILES ../config/org.deepin.dde.network.json DESTINATION ${CMAKE_INSTALL_DATADIR}/dsg/configs/org.deepin.dde.network) # 安装 .qm 文件 install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/dde-network-core/translations) +# 安装 polkit 策略 +set(POLKIT_ACTION_DIR misc/polkit-action) +install(FILES ${POLKIT_ACTION_DIR}/com.deepin.dde.network.policy DESTINATION ${CMAKE_INSTALL_DATADIR}/polkit-1/actions) diff --git a/src/impl/networkmanager/vpncontrollernm.cpp b/src/impl/networkmanager/vpncontrollernm.cpp index 344e6484e..caceb0f88 100644 --- a/src/impl/networkmanager/vpncontrollernm.cpp +++ b/src/impl/networkmanager/vpncontrollernm.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2018 - 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2018 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -10,6 +10,7 @@ #include #include #include +#include "vpndnsroutecontroller.h" #define NETWORKSERVICE "org.deepin.dde.Network1" #define NETWORKPATH "/org/deepin/dde/Network1" @@ -32,6 +33,8 @@ VPNController_NM::~VPNController_NM() void VPNController_NM::initMember() { + m_dnsRouteController = new VpnDnsRouteController(this); + QList newItems; NetworkManager::Connection::List connections = NetworkManager::listConnections(); for (NetworkManager::Connection::Ptr connection : connections) { @@ -91,6 +94,17 @@ VPNItem *VPNController_NM::addVpnConnection(const NetworkManager::Connection::Pt connect(connection.data(), &NetworkManager::Connection::updated, vpnItem, [ connection, vpnItem, createJson, this ] { // 更新数据 vpnItem->setConnection(createJson(connection)); + qCDebug(DNC) << "[DNS-TRACE] NetworkManager::Connection::updated" << connection->path(); + auto activeConns = findActiveConnection(); + for (const auto &ac : activeConns) { + if (!ac.isNull() && !ac->connection().isNull() + && ac->connection()->path() == connection->path() + && m_dnsRouteController) { + m_dnsRouteController->requestApplyDnsModeIfChanged(ac, true); + break; + } + } + Q_EMIT activeConnectionChanged(); Q_EMIT itemChanged(m_items); }); @@ -203,6 +217,10 @@ void VPNController_NM::onConnectionAdded(const QString &path) void VPNController_NM::onConnectionRemoved(const QString &path) { qCInfo(DNC) << "On connection removed, remove connection: " << path; + + if (m_dnsRouteController) + m_dnsRouteController->cleanupConnection(path); + for (VPNItem *item : m_items) { if (item->connection()->path() != path) continue; @@ -261,6 +279,9 @@ void VPNController_NM::onActiveConnectionsChanged() Q_EMIT activeConnectionChanged(); }); + connect(activeConnection.data(), &NetworkManager::ActiveConnection::ipV4ConfigChanged, + this, &VPNController_NM::onVpnIp4ConfigChanged, Qt::UniqueConnection); + QList vpnItems = vpnCategoryItems[activeServiceType]; for (VPNItem *vpnItem : vpnItems) { // 查找该类型的VPN活动连接,并且修改其状态 @@ -279,6 +300,20 @@ void VPNController_NM::onActiveConnectionsChanged() } } +void VPNController_NM::onVpnIp4ConfigChanged() +{ + auto *ac = qobject_cast(sender()); + if (!ac || ac->state() != NetworkManager::ActiveConnection::State::Activated) + return; + + NetworkManager::ActiveConnection::Ptr acPtr = NetworkManager::findActiveConnection(ac->path()); + + qCDebug(DNC) << "[DNS-TRACE] requestApplyDnsModeIfChanged triggered by ipV4ConfigChanged" << ac->path(); + + if (!acPtr.isNull() && m_dnsRouteController) + m_dnsRouteController->requestApplyDnsModeIfChanged(acPtr, false); +} + void VPNController_NM::onPropertiesChanged(const QString &interfaceName, const QVariantMap &changedProperties) { if (interfaceName != NETWORKINTERFACE) diff --git a/src/impl/networkmanager/vpncontrollernm.h b/src/impl/networkmanager/vpncontrollernm.h index 09887cd0c..c01c13500 100644 --- a/src/impl/networkmanager/vpncontrollernm.h +++ b/src/impl/networkmanager/vpncontrollernm.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2018 - 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2018 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -12,6 +12,8 @@ namespace dde { namespace network { +class VpnDnsRouteController; + class VPNController_NM : public VPNController { Q_OBJECT @@ -44,10 +46,12 @@ private Q_SLOTS: void onConnectionRemoved(const QString &path); void onActiveConnectionsChanged(); void onPropertiesChanged(const QString &interfaceName, const QVariantMap &changedProperties); + void onVpnIp4ConfigChanged(); private: QList m_items; QMap m_vpnConnectionsMap; + VpnDnsRouteController *m_dnsRouteController = nullptr; }; } diff --git a/src/impl/vpndnsroutecontroller.cpp b/src/impl/vpndnsroutecontroller.cpp new file mode 100644 index 000000000..0e3871162 --- /dev/null +++ b/src/impl/vpndnsroutecontroller.cpp @@ -0,0 +1,472 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "vpndnsroutecontroller.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "networkconst.h" + +using namespace dde::network; + +namespace { +constexpr const char *const ResolvedService = "org.freedesktop.resolve1"; +constexpr const char *const ResolvedPath = "/org/freedesktop/resolve1"; +constexpr const char *const ResolvedInterface = "org.freedesktop.resolve1.Manager"; +constexpr const char *const NMService = "org.freedesktop.NetworkManager"; +constexpr const char *const NMConnActiveInterface = "org.freedesktop.NetworkManager.Connection.Active"; +constexpr const char *const NMSettingsConnInterface = "org.freedesktop.NetworkManager.Settings.Connection"; + +QString getActiveConnectionIp4Config(const QString &acPath) +{ + QDBusInterface iface(NMService, acPath, NMConnActiveInterface, QDBusConnection::systemBus()); + return iface.property("Ip4Config").value().path(); +} + +QList collectDnsFromDevice(const NetworkManager::Device::Ptr &dev) +{ + QList addrList; + for (const QHostAddress &addr : dev->ipV4Config().nameservers()) { + if (!addr.isNull()) + addrList << addr; + } + + for (const QHostAddress &addr : dev->ipV6Config().nameservers()) { + if (!addr.isNull()) + addrList << addr; + } + + return addrList; +} + +QByteArray packIpv4(quint32 ipv4) +{ + QByteArray bytes(4, '\0'); + bytes[0] = static_cast((ipv4 >> 24) & 0xFF); + bytes[1] = static_cast((ipv4 >> 16) & 0xFF); + bytes[2] = static_cast((ipv4 >> 8) & 0xFF); + bytes[3] = static_cast(ipv4 & 0xFF); + return bytes; +} + +QByteArray packIpv6(const Q_IPV6ADDR &ipv6) +{ + return QByteArray(reinterpret_cast(ipv6.c), sizeof(ipv6.c)); +} + +int readConnectionDnsPriority(const QString &connectionPath) +{ + QDBusMessage msg = QDBusMessage::createMethodCall( + NMService, connectionPath, NMSettingsConnInterface, "GetSettings"); + QDBusPendingReply reply = QDBusConnection::systemBus().call(msg, QDBus::Block, 500); + if (reply.isError() || !reply.value().contains("ipv4")) + return 0; + + return reply.value()["ipv4"].value("dns-priority", 0).toInt(); +} + +} // namespace + +using DnsEntry = QPair; +using DomainEntry = QPair; + +static QDBusArgument &operator<<(QDBusArgument &arg, const DnsEntry &entry) +{ + arg.beginStructure(); + arg << entry.first << entry.second; + arg.endStructure(); + return arg; +} + +static const QDBusArgument &operator>>(const QDBusArgument &arg, DnsEntry &entry) +{ + arg.beginStructure(); + arg >> entry.first >> entry.second; + arg.endStructure(); + return arg; +} + +static QDBusArgument &operator<<(QDBusArgument &arg, const DomainEntry &entry) +{ + arg.beginStructure(); + arg << entry.first << entry.second; + arg.endStructure(); + return arg; +} + +static const QDBusArgument &operator>>(const QDBusArgument &arg, DomainEntry &entry) +{ + arg.beginStructure(); + arg >> entry.first >> entry.second; + arg.endStructure(); + return arg; +} + +static bool s_typesRegistered = false; + +void VpnDnsRouteWorker::registerMetaTypesOnce() +{ + if (s_typesRegistered) + return; + + qRegisterMetaType("DnsEntry"); + qDBusRegisterMetaType(); + qRegisterMetaType>("QList"); + qDBusRegisterMetaType>(); + + qRegisterMetaType("DomainEntry"); + qDBusRegisterMetaType(); + qRegisterMetaType>("QList"); + qDBusRegisterMetaType>(); + + s_typesRegistered = true; +} + + +// ========== VpnDnsRouteWorker ========== + +VpnDnsRouteWorker::VpnDnsRouteWorker(QObject *parent) + : QObject(parent) +{ +} + +VpnDnsRouteWorker::~VpnDnsRouteWorker() +{ +} + +void VpnDnsRouteWorker::ensureResolvedInterface() +{ + if (!m_resolvedInterface) { + m_resolvedInterface = new QDBusInterface(ResolvedService, ResolvedPath, ResolvedInterface, QDBusConnection::systemBus(), this); + } +} + +bool VpnDnsRouteWorker::checkPolkitAuthorization() +{ + PolkitQt1::Authority *authority = PolkitQt1::Authority::instance(); + if (!authority) { + qCWarning(DNC) << "[POLKIT] Failed to get PolkitQt1::Authority instance"; + return false; + } + + if (authority->hasError()) { + qCDebug(DNC) << "[POLKIT] Clearing previous authority error:" << authority->errorDetails(); + authority->clearError(); + } + + PolkitQt1::UnixProcessSubject subject(QCoreApplication::applicationPid()); + PolkitQt1::Authority::Result result = authority->checkAuthorizationSync( + QStringLiteral("com.deepin.dde.network.configure-dns"), + subject, + PolkitQt1::Authority::AllowUserInteraction); + + if (authority->hasError()) { + qCWarning(DNC) << "[POLKIT] Authorization check error:" << authority->errorDetails(); + authority->clearError(); + return false; + } + + return result == PolkitQt1::Authority::Yes; +} + +VpnDnsMode VpnDnsRouteWorker::getVpnDnsModeFromConnection(const QString &connectionPath) const +{ + int dp = readConnectionDnsPriority(connectionPath); + if (dp < 0) { + return VpnDnsMode::VpnDnsModePreferred; + } else if (dp > 0) { + return VpnDnsMode::VpnDnsModeSecondary; + } + return VpnDnsMode::VpnDnsModeNotSet; +} + +void VpnDnsRouteWorker::onCheckAndApplyDnsModeIfChanged(const QString &vpnConnectionPath, bool isCompare) +{ + int currentPriority = readConnectionDnsPriority(vpnConnectionPath); + + if (!m_lastDnsPriority.contains(vpnConnectionPath)) { + m_lastDnsPriority[vpnConnectionPath] = currentPriority; + qCDebug(DNC) << "[DNS-TRACE] onCheckAndApplyDnsModeIfChanged: first time caching dns-priority:" + << currentPriority << "for:" << vpnConnectionPath; + return; + } + + int lastPriority = m_lastDnsPriority.value(vpnConnectionPath); + if (isCompare && currentPriority == lastPriority) { + qCDebug(DNC) << "[DNS-TRACE] onCheckAndApplyDnsModeIfChanged: dns-priority unchanged," + << "skipping apply for:" << vpnConnectionPath; + return; + } + + m_lastDnsPriority[vpnConnectionPath] = currentPriority; + + NetworkManager::ActiveConnection::List allActiveConns = NetworkManager::activeConnections(); + for (const auto &ac : allActiveConns) { + if (!ac.isNull() && !ac->connection().isNull() + && ac->connection()->path() == vpnConnectionPath) { + onApplyDnsModeForVpnAc(ac); + break; + } + } +} + +void VpnDnsRouteWorker::onCleanupConnection(const QString &connectionPath) +{ + m_lastDnsPriority.remove(connectionPath); + qCDebug(DNC) << "[DNS-TRACE] cleaned up dns-priority cache for:" << connectionPath; +} + +void VpnDnsRouteWorker::onApplyDnsModeForVpnAc(const NetworkManager::ActiveConnection::Ptr &vpnAc) +{ + if (vpnAc.isNull() || vpnAc->connection().isNull()) { + return; + } + + if (!QDBusConnection::systemBus().interface()->isServiceRegistered("org.freedesktop.resolve1")) { + qCDebug(DNC) << "[DNS-TRACE] systemd-resolved is not available, skipping DNS route"; + return; + } + + const QString vpnIp4ConfigPath = getActiveConnectionIp4Config(vpnAc->path()); + if (vpnIp4ConfigPath.isEmpty() || vpnIp4ConfigPath == "/") { + qCDebug(DNC) << "[DNS-TRACE] VPN AC has no Ip4Config:" << vpnAc->id(); + return; + } + + NetworkManager::ActiveConnection::List allActiveConnections = NetworkManager::activeConnections(); + for (const NetworkManager::ActiveConnection::Ptr &tunAc : allActiveConnections) { + if (tunAc.isNull() || tunAc->connection().isNull() || tunAc == vpnAc) + continue; + + if (tunAc->connection()->settings()->connectionType() != NetworkManager::ConnectionSettings::ConnectionType::Tun) + continue; + + const QString tunIp4ConfigPath = getActiveConnectionIp4Config(tunAc->path()); + if (tunIp4ConfigPath != vpnIp4ConfigPath) + continue; + + for (const QString &devPath : tunAc->devices()) { + NetworkManager::Device::Ptr dev = NetworkManager::findNetworkInterface(devPath); + if (dev.isNull()) + continue; + + if (dev->type() != NetworkManager::Device::Type::Tun && + dev->type() != NetworkManager::Device::Type::Generic && + dev->type() != NetworkManager::Device::Type::IpTunnel) + continue; + + int ifindex = static_cast(if_nametoindex(dev->interfaceName().toStdString().c_str())); + if (ifindex <= 0) + continue; + + const QList dnsList = collectDnsFromDevice(dev); + if (dnsList.isEmpty()) + continue; + + VpnDnsMode dnsMode = getVpnDnsModeFromConnection(vpnAc->connection()->path()); + ensureResolvedInterface(); + if (!checkPolkitAuthorization()) { + qCDebug(DNC) << "[DNS-TRACE] Polkit authorization denied, skipping DNS route configuration"; + return; + } + + switch (dnsMode) { + case VpnDnsMode::VpnDnsModePreferred: + applyPreferredDnsMode(ifindex, dnsList); + break; + case VpnDnsMode::VpnDnsModeSecondary: + applySecondaryDnsMode(ifindex, dnsList); + break; + case VpnDnsMode::VpnDnsModeNotSet: + default: + applyNotSetDnsMode(ifindex); + break; + } + return; + } + } + qCDebug(DNC) << "[DNS-TRACE] No matching Tun AC found for VPN:" << vpnAc->id(); +} + +bool VpnDnsRouteWorker::applyPreferredDnsMode(int ifindex, const QList &dnsServers) +{ + if (ifindex <= 0) { + qCWarning(DNC) << "Invalid ifindex:" << ifindex; + return false; + } + + qCDebug(DNC) << "[DNS-TRACE] Applying Preferred DNS route for ifindex" << ifindex << "dns:" << dnsServers; + return setLinkDns(ifindex, dnsServers) + && setLinkDomains(ifindex, {"."}, {true}) + && setLinkDefaultRoute(ifindex, false); +} + +bool VpnDnsRouteWorker::applySecondaryDnsMode(int ifindex, const QList &dnsServers) +{ + if (ifindex <= 0) { + qCWarning(DNC) << "Invalid ifindex:" << ifindex; + return false; + } + + qCDebug(DNC) << "[DNS-TRACE] Applying Secondary DNS route for ifindex" << ifindex << "dns:" << dnsServers; + return setLinkDns(ifindex, dnsServers) + && setLinkDomains(ifindex, {}, {}) + && setLinkDefaultRoute(ifindex, true); +} + +bool VpnDnsRouteWorker::applyNotSetDnsMode(int ifindex) +{ + if (ifindex <= 0) { + qCWarning(DNC) << "Invalid ifindex:" << ifindex; + return false; + } + + qCDebug(DNC) << "[DNS-TRACE] Applying Not Set DNS route (RevertLink) for ifindex" << ifindex; + return revertLink(ifindex); +} + +bool VpnDnsRouteWorker::setLinkDns(int ifindex, const QList &dnsServers) +{ + registerMetaTypesOnce(); + + QList dnsEntries; + for (const QHostAddress &addr : dnsServers) { + if (addr.protocol() == QAbstractSocket::IPv4Protocol) { + dnsEntries.append({2, packIpv4(addr.toIPv4Address())}); + } else if (addr.protocol() == QAbstractSocket::IPv6Protocol) { + dnsEntries.append({10, packIpv6(addr.toIPv6Address())}); + } + } + + QDBusPendingCall pending = m_resolvedInterface->asyncCall("SetLinkDNS", ifindex, QVariant::fromValue(dnsEntries)); + pending.waitForFinished(); + if (pending.isError()) { + qCWarning(DNC) << "[DNS-TRACE] SetLinkDNS failed:" << pending.error().message(); + return false; + } + return true; +} + +bool VpnDnsRouteWorker::setLinkDomains(int ifindex, const QStringList &domains, const QList &routes) +{ + registerMetaTypesOnce(); + + QList domainEntries; + for (int i = 0; i < domains.size() && i < routes.size(); ++i) { + domainEntries.append({domains[i], routes[i]}); + } + + QDBusPendingCall pending = m_resolvedInterface->asyncCall("SetLinkDomains", ifindex, QVariant::fromValue(domainEntries)); + pending.waitForFinished(); + if (pending.isError()) { + qCWarning(DNC) << "[DNS-TRACE] SetLinkDomains failed:" << pending.error().message(); + return false; + } + return true; +} + +bool VpnDnsRouteWorker::setLinkDefaultRoute(int ifindex, bool enable) +{ + QDBusPendingCall pending = m_resolvedInterface->asyncCall("SetLinkDefaultRoute", ifindex, enable); + pending.waitForFinished(); + if (pending.isError()) { + qCWarning(DNC) << "[DNS-TRACE] SetLinkDefaultRoute failed:" << pending.error().message(); + return false; + } + return true; +} + +bool VpnDnsRouteWorker::revertLink(int ifindex) +{ + QDBusPendingCall pending = m_resolvedInterface->asyncCall("RevertLink", ifindex); + pending.waitForFinished(); + if (pending.isError()) { + qCWarning(DNC) << "[DNS-TRACE] RevertLink failed:" << pending.error().message(); + return false; + } + return true; +} + + +// ========== VpnDnsRouteController ========== + +VpnDnsRouteController::VpnDnsRouteController(QObject *parent) + : QObject(parent) +{ + m_workerThread = new QThread(); + m_worker = new VpnDnsRouteWorker(); + m_worker->moveToThread(m_workerThread); + + connect(this, &VpnDnsRouteController::requestCheckAndApplyDnsModeIfChanged, + m_worker, &VpnDnsRouteWorker::onCheckAndApplyDnsModeIfChanged); + connect(this, &VpnDnsRouteController::requestCleanupConnection, + m_worker, &VpnDnsRouteWorker::onCleanupConnection); + + // 正常退出路径:quit + wait → Worker 在子线程事件循环退出前被 deleteLater 删除 + connect(m_workerThread, &QThread::finished, m_worker, &QObject::deleteLater); + connect(m_workerThread, &QThread::finished, m_workerThread, &QObject::deleteLater, Qt::QueuedConnection); + + m_workerThread->start(); +} + +VpnDnsRouteController::~VpnDnsRouteController() +{ + if (!m_workerThread) + return; + + disconnect(this, nullptr, m_worker, nullptr); + + if (m_workerThread && m_workerThread->isRunning()) { + m_workerThread->quit(); + + if (m_workerThread->wait(3000)) { + m_worker = nullptr; + } else { + m_workerThread->terminate(); + m_workerThread->wait(500); + if (m_worker) + { + delete m_worker; + m_worker = nullptr; + } + } + } + + if (m_workerThread) { + delete m_workerThread; + m_workerThread = nullptr; + } +} + +void VpnDnsRouteController::requestApplyDnsModeIfChanged(const NetworkManager::ActiveConnection::Ptr &vpnAc, bool isCompare) +{ + if (vpnAc.isNull() || vpnAc->connection().isNull()) + return; + + const QString connPath = vpnAc->connection()->path(); + qCDebug(DNC) << "[DNS-TRACE] requestApplyDnsModeIfChanged delegating to worker thread for:" << connPath; + Q_EMIT requestCheckAndApplyDnsModeIfChanged(connPath, isCompare); +} + +void VpnDnsRouteController::cleanupConnection(const QString &connectionPath) +{ + Q_EMIT requestCleanupConnection(connectionPath); +} diff --git a/src/impl/vpndnsroutecontroller.h b/src/impl/vpndnsroutecontroller.h new file mode 100644 index 000000000..a62091c07 --- /dev/null +++ b/src/impl/vpndnsroutecontroller.h @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef VPNDNSROUTECONTROLLER_H +#define VPNDNSROUTECONTROLLER_H + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QHostAddress; +class QDBusInterface; +class QDBusPendingCallWatcher; +class QThread; +class QTimer; +QT_END_NAMESPACE + +namespace dde { +namespace network { + +enum VpnDnsMode +{ + VpnDnsModeNotSet = 0, + VpnDnsModeSecondary = 1, + VpnDnsModePreferred = 2 +}; + + +class VpnDnsRouteWorker : public QObject +{ + Q_OBJECT + +public: + explicit VpnDnsRouteWorker(QObject *parent = nullptr); + ~VpnDnsRouteWorker() override; + +public Q_SLOTS: + void onApplyDnsModeForVpnAc(const NetworkManager::ActiveConnection::Ptr &vpnAc); + void onCheckAndApplyDnsModeIfChanged(const QString &vpnConnectionPath, bool isCompare); + void onCleanupConnection(const QString &connectionPath); + +private: + VpnDnsMode getVpnDnsModeFromConnection(const QString &connectionPath) const; + void ensureResolvedInterface(); + bool checkPolkitAuthorization(); + + bool applyPreferredDnsMode(int ifindex, const QList &dnsServers); + bool applySecondaryDnsMode(int ifindex, const QList &dnsServers); + bool applyNotSetDnsMode(int ifindex); + + bool setLinkDns(int ifindex, const QList &dnsServers); + bool setLinkDomains(int ifindex, const QStringList &domains, const QList &routes); + bool setLinkDefaultRoute(int ifindex, bool enable); + bool revertLink(int ifindex); + static void registerMetaTypesOnce(); + + QDBusInterface *m_resolvedInterface = nullptr; + QMap m_lastDnsPriority; // 缓存每个连接的 dns-priority,worker 线程持有 +}; + + +class VpnDnsRouteController : public QObject +{ + Q_OBJECT + +public: + explicit VpnDnsRouteController(QObject *parent = nullptr); + ~VpnDnsRouteController() override; + void requestApplyDnsModeIfChanged(const NetworkManager::ActiveConnection::Ptr &vpnAc, bool isCompare); + void cleanupConnection(const QString &connectionPath); + +Q_SIGNALS: + void requestCheckAndApplyDnsModeIfChanged(const QString &vpnConnectionPath, bool isCompare); + void requestCleanupConnection(const QString &connectionPath); + +private: + QThread *m_workerThread = nullptr; + VpnDnsRouteWorker *m_worker = nullptr; +}; + +} // namespace network +} // namespace dde +#endif // VPNDNSROUTECONTROLLER_H diff --git a/src/misc/polkit-action/com.deepin.dde.network.policy b/src/misc/polkit-action/com.deepin.dde.network.policy new file mode 100644 index 000000000..00dbfccf5 --- /dev/null +++ b/src/misc/polkit-action/com.deepin.dde.network.policy @@ -0,0 +1,27 @@ + + + + LinuxDeepin + https://www.deepin.com/ + + + Configure VPN DNS Mode + Authentication is required to configure VPN DNS Mode + Configure VPN DNS Mode + Authentication is required to configure VPN DNS Mode + 配置 VPN DNS 模式 + 需要授权配置 VPN DNS 模式 + 配置 VPN DNS 模式 + 需要授權配置 VPN DNS 模式 + 配置 VPN DNS 模式 + 需要授權配置 VPN DNS 模式 + + auth_admin + auth_admin + auth_admin_keep + + org.freedesktop.resolve1.set-dns-servers org.freedesktop.resolve1.set-domains org.freedesktop.resolve1.set-default-route org.freedesktop.resolve1.revert + +