Skip to content

fix: Add automatic VPN connection after network state monitoring#522

Merged
deepin-bot[bot] merged 1 commit intolinuxdeepin:masterfrom
ut003640:master
Mar 19, 2026
Merged

fix: Add automatic VPN connection after network state monitoring#522
deepin-bot[bot] merged 1 commit intolinuxdeepin:masterfrom
ut003640:master

Conversation

@ut003640
Copy link
Contributor

@ut003640 ut003640 commented Mar 19, 2026

VPN auto-connection is not started by NetworkManager, but by external network services. This adds the VPN startup functionality.

Log: Fix VPN not auto-connecting
PMS: BUG-287121

fix: 增加监听网络状态后自动连接VPN

VPN自动连接并非NetworkManager启动,而是由外部网络服务启动,这里加上启动VPN的功能

Log: 修复VPN没有自动连接

Summary by Sourcery

Add automatic VPN activation when the network service detects a successful network connection.

New Features:

  • Automatically start autoconnect-configured VPN connections when NetworkManager reports the system as connected.

Enhancements:

  • Listen to NetworkManager status changes in the network service plugin to trigger VPN handling logic.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 19, 2026

Reviewer's Guide

Adds automatic VPN activation when NetworkManager reports a successful network connection, wiring status change notifications into NetworkThread and selecting appropriate autoconnect VPN profiles to start via D-Bus.

Sequence diagram for automatic VPN activation on network status change

sequenceDiagram
    participant NetworkManagerNotifier as NetworkManager_Notifier
    participant NetworkThread as NetworkThread
    participant NetworkManager as NetworkManager
    participant DBus as DBus

    NetworkManagerNotifier->>NetworkThread: statusChanged(status)
    activate NetworkThread
    NetworkThread->>NetworkThread: onStatusChanged(status)
    alt vpnEnabled is false
        NetworkThread-->>NetworkManagerNotifier: return
    else status is not Connected
        NetworkThread-->>NetworkManagerNotifier: return
    else status is Connected and vpnEnabled is true
        NetworkThread->>NetworkManager: activeConnections()
        NetworkManager-->>NetworkThread: ActiveConnection_List
        NetworkThread->>NetworkThread: build actConnections
        NetworkThread->>NetworkManager: listConnections()
        NetworkManager-->>NetworkThread: Connection_List
        NetworkThread->>NetworkThread: filter autoconnect VPN connections
        NetworkThread->>NetworkThread: select latest per serviceType
        loop for each selected VPN connection
            NetworkThread->>NetworkManager: activateConnection(connectionPath, rootPath, rootPath)
            NetworkManager-->>DBus: org.freedesktop.NetworkManager.ActivateConnection
            DBus-->>NetworkThread: QDBusPendingReply
            alt activation error
                NetworkThread->>NetworkThread: log warning
            else activation success
                NetworkThread->>NetworkThread: log debug
            end
        end
    end
    deactivate NetworkThread
Loading

Updated class diagram for NetworkThread with onStatusChanged

classDiagram
    class NetworkThread {
        - NetworkEnabledConfig* m_networkConfig
        - QList~NetworkManager_Device_Ptr~ m_devices
        + void init()
        + QString enableDevice(NetworkManager_Device_Ptr device)
        + QString disableDevice(NetworkManager_Device_Ptr device)
        + void onStatusChanged(NetworkManager_Status status)
        - void disableVpn()
        - bool airplaneWifiEnabled()
    }

    class NetworkEnabledConfig {
        + bool vpnEnabled()
    }

    class NetworkManager_Notifier {
        + void deviceAdded()
        + void deviceRemoved()
        + void statusChanged(NetworkManager_Status status)
    }

    class NetworkManager {
        + static NetworkManager_Notifier* notifier()
        + static NetworkManager_ActiveConnection_List activeConnections()
        + static NetworkManager_Connection_List listConnections()
        + static QDBusPendingReply activateConnection(QString connectionPath, QString devicePath, QString specificObjectPath)
    }

    class NetworkManager_ActiveConnection {
        + NetworkManager_ActiveConnection_State state()
        + NetworkManager_Connection_Ptr connection()
    }

    class NetworkManager_Connection {
        + QString path()
        + QString name()
        + NetworkManager_ConnectionSettings_Ptr settings()
    }

    class NetworkManager_ConnectionSettings {
        + bool autoconnect()
        + NetworkManager_ConnectionSettings_ConnectionType connectionType()
        + quint64 timestamp()
        + NetworkManager_VpnSetting_Ptr setting(NetworkManager_Setting_SettingType type)
    }

    class NetworkManager_VpnSetting {
        + QString serviceType()
    }

    NetworkManager_Notifier --> NetworkThread : emits_statusChanged
    NetworkThread --> NetworkEnabledConfig : uses
    NetworkThread --> NetworkManager : uses
    NetworkManager --> NetworkManager_Notifier : returns
    NetworkManager --> NetworkManager_ActiveConnection : manages
    NetworkManager --> NetworkManager_Connection : manages
    NetworkManager_ActiveConnection --> NetworkManager_Connection : refers
    NetworkManager_Connection --> NetworkManager_ConnectionSettings : has
    NetworkManager_ConnectionSettings --> NetworkManager_VpnSetting : returns
Loading

Flow diagram for NetworkThread onStatusChanged VPN selection logic

flowchart TD
    A[onStatusChanged status] --> B{vpnEnabled}
    B -- false --> Z[Return]
    B -- true --> C{status is Connected}
    C -- false --> Z
    C -- true --> D[Get activeConnections]
    D --> E[Build actConnections list of non activating and non activated connections]
    E --> F[Get all connections via listConnections]
    F --> G[Iterate connections]
    G --> H{connection path in actConnections}
    H -- true --> G
    H -- false --> I{settings autoconnect is true}
    I -- false --> G
    I -- true --> J{connectionType is Vpn}
    J -- false --> G
    J -- true --> K[Get VpnSetting and serviceType]
    K --> L{serviceType in vpnContainer}
    L -- false --> M[Add connection to vpnContainer]
    L -- true --> N{timestamp newer than stored}
    N -- true --> O[Replace stored connection]
    N -- false --> G
    M --> G
    O --> G
    G -->|loop done| P[Iterate vpnContainer]
    P --> Q[For each selected VPN connection call activateConnection via DBus]
    Q --> R{reply is error}
    R -- true --> S[Log warning]
    R -- false --> T[Log success]
    S --> U[End]
    T --> U[End]
Loading

File-Level Changes

Change Details Files
Wire NetworkThread into NetworkManager status changes to trigger VPN autoconnect logic on successful connections.
  • Connect NetworkManager::Notifier::statusChanged to a new NetworkThread::onStatusChanged slot in init() and invoke it once with the current status at startup.
  • Include NetworkManagerQt/Manager in the header and additional NetworkManagerQt connection-related headers in the implementation file to support status and connection handling.
network-service-plugin/src/system/networkthread.cpp
network-service-plugin/src/system/networkthread.h
Implement automatic VPN connection logic based on active connections and autoconnect VPN profiles.
  • Add NetworkThread::onStatusChanged(NetworkManager::Status) that early-returns when VPN is disabled or not in Connected state.
  • Collect paths of currently activating/activated active connections to avoid reconnecting them.
  • Iterate all NetworkManager connections, filtering to autoconnect-enabled VPN types that are not in the active list.
  • Group candidate VPN connections by service type and select, per type, the one with the most recent timestamp as the preferred connection.
  • For each selected VPN connection, call NetworkManager::activateConnection over D-Bus and log success or failure with qDebug/qWarning.
network-service-plugin/src/system/networkthread.cpp
Update metadata in NetworkThread header.
  • Extend SPDX-FileCopyrightText year range from 2025 to 2025-2026.
network-service-plugin/src/system/networkthread.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In onStatusChanged, the logic building actConnections appears inverted: you add connections whose state is not Activated/Activating and then skip any connection whose path is in actConnections, which contradicts the comment // 过滤掉正在连接的 and likely ends up skipping inactive connections instead of active/activating ones.
  • Calling onStatusChanged(NetworkManager::status()) directly from init() assumes that m_networkConfig and the NetworkManager state are already fully initialized; if there are code paths where this is not true, you may want to guard this call or defer it until initialization is guaranteed complete.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `onStatusChanged`, the logic building `actConnections` appears inverted: you add connections whose state is *not* `Activated`/`Activating` and then skip any connection whose path is in `actConnections`, which contradicts the comment `// 过滤掉正在连接的` and likely ends up skipping inactive connections instead of active/activating ones.
- Calling `onStatusChanged(NetworkManager::status())` directly from `init()` assumes that `m_networkConfig` and the NetworkManager state are already fully initialized; if there are code paths where this is not true, you may want to guard this call or defer it until initialization is guaranteed complete.

## Individual Comments

### Comment 1
<location path="network-service-plugin/src/system/networkthread.cpp" line_range="389-390" />
<code_context>
+
+    QStringList actConnections;
+    NetworkManager::ActiveConnection::List allactiveConnections = NetworkManager::activeConnections();
+    for (const NetworkManager::ActiveConnection::Ptr &activeConnection : allactiveConnections) {
+        if (activeConnection->state() != NetworkManager::ActiveConnection::State::Activated
+            && activeConnection->state() != NetworkManager::ActiveConnection::State::Activating)
+            actConnections << activeConnection->connection()->path();
</code_context>
<issue_to_address>
**issue (bug_risk):** The filter for `actConnections` looks inverted and will skip the wrong connections.

This condition collects connections whose state is neither `Activated` nor `Activating`. Later, `actConnections.contains(conn->path())` is used to skip connections that are “currently connecting”, but with this logic you instead skip those that are *not* active/activating, which inverts the intended behavior (`// 过滤掉正在连接的`). Please flip the condition so only `Activated`/`Activating` connections are added to `actConnections` (e.g. use `==` with `||`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@ut003640 ut003640 force-pushed the master branch 2 times, most recently from b10947f to 1a6b332 Compare March 19, 2026 05:16
caixr23
caixr23 previously approved these changes Mar 19, 2026
VPN auto-connection is not started by NetworkManager, but by external network services. This adds the VPN startup functionality.

Log: Fix VPN not auto-connecting
PMS: BUG-287121

fix: 增加监听网络状态后自动连接VPN

VPN自动连接并非NetworkManager启动,而是由外部网络服务启动,这里加上启动VPN的功能

Log: 修复VPN没有自动连接
@deepin-ci-robot
Copy link

deepin pr auto review

这段代码主要实现了一个功能:在网络状态变为已连接后,自动查找并连接设置了"自动连接"属性的 VPN。以下是对这段 git diff 的详细审查,包括语法逻辑、代码质量、性能和安全方面的改进意见。

1. 语法逻辑

  • 头文件包含顺序与位置
    • .cpp 文件中,#include <NetworkManagerQt/Manager> 被移除,而在 .h 文件中新增了该头文件。这是合理的,因为 NetworkManager::status() 需要该头文件,且放在头文件中声明依赖更清晰。不过,建议将第三方库(如 Qt, NetworkManagerQt)的头文件包含放在项目自定义头文件(如 "constants.h")之前,这是 C++ 的常见惯例,有助于减少隐藏的依赖问题。
  • 版权年份更新
    • .h 文件中的版权年份从 2025 更新为 2025-2026,这符合规范。
  • 异步处理逻辑
    • 使用 QDBusPendingCallWatcher 处理 activateConnection 的异步结果是正确的做法,避免了阻塞主线程。

2. 代码质量

  • 命名规范
    • 变量名 allactiveConnections 中的 active 是小写,建议改为驼峰式命名 allActiveConnections 以符合 Qt/C++ 的常见编码风格。
  • 注释与可读性
    • // 检查到网络状态为连接成功后,检查本地是否存在自动连接的VPN,如果存在,就让它连接 这段注释很好地解释了代码意图。
    • // 优先使用最后一次连接成功的连接 逻辑清晰,通过比较 timestamp 来实现,易于理解。
  • Lambda 表达式中的捕获
    • connect(watcher, ..., [this, connection_name](...) { ... }) 中按值捕获了 connection_name,这是安全的,因为 QString 是隐式共享的,且 watcher 是异步执行的。捕获 this 指针也是必要的,因为可能需要访问类成员(尽管当前 lambda 内未显式使用,但保留是合理的)。
  • 资源管理
    • connect(watcher, &QDBusPendingCallWatcher::finished, watcher, &QDBusPendingCallWatcher::deleteLater); 这一行确保了 watcher 在完成后会自动删除,防止内存泄漏,做得很好。

3. 代码性能

  • 循环效率
    • 代码中存在两次遍历:第一次遍历 allactiveConnections 构建 activeConnectionPaths 列表,第二次遍历 allConnections 查找候选 VPN。
    • activeConnectionPaths 是一个 QStringListcontains 操作的时间复杂度是 O(n)。如果连接数量非常多,这可能会导致性能瓶颈。
    • 改进建议:将 activeConnectionPaths 改为 QSet<QString>,这样 contains 操作的平均时间复杂度会降低到 O(1),显著提高查找效率。
  • 不必要的拷贝
    • activeConnectionPaths << activeConnection->connection()->path(); 这里会产生字符串的拷贝。虽然 QString 的开销很小,但在高频调用下仍需注意。使用 QSet 后,可以直接插入。
  • DBUS 调用
    • NetworkManager::listConnections()NetworkManager::activeConnections() 可能会涉及 DBUS 通信,这属于相对昂贵的操作。目前的逻辑是在网络状态变更时触发,频率较低,因此性能是可以接受的。

4. 代码安全

  • 空指针检查
    • 代码中已经对 activeConnection->connection().isNull() 进行了检查,这是很好的防御性编程。
    • setting.isNull() 检查也确保了 VpnSetting 的有效性。
  • 并发与竞态条件
    • 该函数由信号 statusChanged 触发。如果在短时间内网络状态频繁变化(例如网络极不稳定),可能会触发多次 onStatusChanged,导致多次调用 activateConnection
    • 虽然代码中检查了 status != NetworkManager::Status::Connected 作为第一道防线,但 NetworkManager::activateConnection 是异步的。如果用户在 VPN 连接过程中断开网络,可能会导致 watcher 回调时状态已不一致。
    • 改进建议:可以考虑在 onStatusChanged 开始时增加一个简单的防抖机制(Debounce)或者状态标志位,防止在短时间内重复触发连接逻辑。或者,在 watcher 的回调中再次检查当前网络状态是否依然允许 VPN 连接。
  • 错误处理
    • qWarning()qDebug() 用于记录日志,有助于调试,但在生产环境中,可能需要更完善的日志系统或错误上报机制。

总结与修改建议代码

以下是结合上述建议优化后的代码片段(主要优化了数据结构以提高性能):

void NetworkThread::onStatusChanged(NetworkManager::Status status)
{
    // 防御性检查:确保VPN功能已开启且网络已连接
    if (!m_networkConfig->vpnEnabled())
        return;

    if (status != NetworkManager::Status::Connected)
        return;

    // 使用 QSet 提高查找效率
    QSet<QString> activeConnectionPaths;
    NetworkManager::ActiveConnection::List allActiveConnections = NetworkManager::activeConnections();
    for (const NetworkManager::ActiveConnection::Ptr &activeConnection : allActiveConnections) {
        if (activeConnection->connection().isNull())
            continue;

        if (activeConnection->state() == NetworkManager::ActiveConnection::State::Activated
            || activeConnection->state() == NetworkManager::ActiveConnection::State::Activating) {
            activeConnectionPaths.insert(activeConnection->connection()->path());
        }
    }

    QMap<QString, NetworkManager::Connection::Ptr> candidateVpns;
    NetworkManager::Connection::List allConnections = NetworkManager::listConnections();
    
    for (const NetworkManager::Connection::Ptr &conn : allConnections) {
        // 过滤掉正在连接的或者已经连接成功的
        if (activeConnectionPaths.contains(conn->path()))
            continue;

        // 这里只查找设置了自动连接的
        if (!conn->settings()->autoconnect())
            continue;

        // 查找VPN的连接方式
        NetworkManager::ConnectionSettings::ConnectionType type = conn->settings()->connectionType();
        if (type == NetworkManager::ConnectionSettings::ConnectionType::Vpn) {
            NetworkManager::VpnSetting::Ptr setting = conn->settings()->setting(NetworkManager::Setting::SettingType::Vpn).dynamicCast<NetworkManager::VpnSetting>();
            if (setting.isNull())
                continue;

            QString serviceType = setting->serviceType();
            if (candidateVpns.contains(serviceType)) {
                // 优先使用最后一次连接成功的连接
                const NetworkManager::Connection::Ptr &lastSetting = candidateVpns[serviceType];
                if (lastSetting->settings()->timestamp() < conn->settings()->timestamp())
                    candidateVpns[serviceType] = conn;
            } else {
                candidateVpns[serviceType] = conn;
            }
        }
    }

    for (auto it = candidateVpns.begin(); it != candidateVpns.end(); ++it) {
        const NetworkManager::Connection::Ptr &connection = it.value();
        
        // 再次检查当前网络状态,防止在网络快速切换时发起无效连接
        if (NetworkManager::status() != NetworkManager::Status::Connected) {
            qWarning() << "Network status changed, aborting VPN connection attempt for:" << connection->name();
            break; 
        }

        QDBusPendingReply<QDBusObjectPath> reply = NetworkManager::activateConnection(connection->path(), "/", "/");
        QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
        QString connectionName = connection->name();
        
        connect(watcher, &QDBusPendingCallWatcher::finished, watcher, &QDBusPendingCallWatcher::deleteLater);
        connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, connectionName](QDBusPendingCallWatcher *watcher) {
            QDBusPendingReply<QDBusObjectPath> reply = *watcher;
            if (reply.isError()) {
                qWarning() << "Failed to connect VPN:" << connectionName << "Reason:" << reply.error().message();
            } else {
                qDebug() << "VPN connection initiated successfully:" << connectionName;
            }
        });
    }
}

主要变更点:

  1. activeConnectionPathsQStringList 改为 QSet<QString>,并使用 insert 代替 <<,优化查找性能。
  2. 在发起连接前增加了一次状态检查,防止在网络快速切换(如 Connected -> Disconnected -> Connected 快速发生)时发起无效的连接请求。
  3. 修正了变量命名 allactiveConnections -> allActiveConnections
  4. connection_name 重命名为 connectionName 以符合驼峰命名法。

@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: caixr23, ut003640

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ut003640
Copy link
Contributor Author

/merge

@deepin-bot
Copy link
Contributor

deepin-bot bot commented Mar 19, 2026

This pr cannot be merged! (status: unstable)

@ut003640
Copy link
Contributor Author

/forcemerge

@deepin-bot
Copy link
Contributor

deepin-bot bot commented Mar 19, 2026

This pr force merged! (status: unstable)

@deepin-bot deepin-bot bot merged commit ba502cf into linuxdeepin:master Mar 19, 2026
17 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants