Skip to content

Mdm integration#198

Open
riccardomanfrin wants to merge 11 commits into
mainfrom
mdm_integration
Open

Mdm integration#198
riccardomanfrin wants to merge 11 commits into
mainfrom
mdm_integration

Conversation

@riccardomanfrin

@riccardomanfrin riccardomanfrin commented Jun 15, 2026

Copy link
Copy Markdown

Adds Android MDM integration

[1] OS broadcast Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED
        │
        ▼
[2] VPNService.mdmPolicyChangedReceiver.onReceive()         tool/VPNService.java
        │
        ▼
[3] engineRestarter.requestRestartNow()                     tool/EngineRestarter.java:requestRestartNow
        ├─ restartScheduled = true
        └─ handler.post(restartRunnable)                    [immediate, no debounce]
        │
        ▼
[4] EngineRestarter.restartEngine()                         (private, run on UI thread by handler)
        ├─ check engineRunner.isRunning() → true
        ├─ wrap connection listener in FilteringConnectionListener
        │     [filtra eventi disconnect del teardown del vecchio engine]
        ├─ engineRunner.addServiceStateListenerForRestart(serviceStateListener)
        ├─ engineRunner.snapshotExternalListeners() + suppressServiceStateListener(...)
        │     [blocks other listeners during restart window]
        ├─ notifyConnecting(savedListener)                  [UI: orange circle in progress]
        └─ engineRunner.stop()                              tool/EngineRunner.java:stop
            │
            ▼
[5] goClient.stop() (gomobile JNI)                          → Go Client.Stop() → ctx cancel
[6] Go-side Run() in EngineRunner thread rientra dal blocking call
        ├─ catch/finally: engineIsRunning = false
        └─ notifyServiceStateListeners(false)               → fire onStopped on all listeners
        │
        ▼
[7] serviceStateListener.onStopped() (registered in step 4)
        └─ engineRunner.runWithoutAuth()                    tool/EngineRunner.java:runWithoutAuth
            │
            ▼
[8] runClient(null, false) — new spawned thread
        ├─ profileManager.getActiveConfigPath() / getActiveStateFilePath()
        ├─ new AndroidPlatformFiles(...)
        ├─ notifyServiceStateListeners(true) → fire onStarted
        └─ goClient.runWithoutLogin(...)                    (gomobile JNI)
            │
            ▼
[9] Go-side: UpdateOrCreateConfig → Config.apply → applyMDMPolicy(loadMDMPolicy())
        ├─ loadMDMPolicy → LoadPolicy → loadPlatformPolicy
        │     → fetcher.Fetch() → JSON adapter → kotlinFetcher.FetchJSON()
        │     → MDMPolicyFetcher.fetchJSON() (JNI back)     app/.../MDMPolicyFetcher.java
        │         → RestrictionsManager.getApplicationRestrictions()
        │         → Bundle → JSON
        ├─ MDM overlay applied to Config
        └─ NewConnectClient(ctx, freshCfg) → engine start
        │
        ▼
[10] serviceStateListener.onStarted()                       tool/EngineRestarter.java:115
        ├─ isRestartInProgress = false
        ├─ engineRunner.setConnectionListener(savedListener)   [remove FilteringConnectionListener]
        └─ unsuppressAll(suppressed)                        [re-enable external listener]
        │
        ▼
[11] Engine raggiunge stato CONNECTED
        └─ ConnectionListener.onConnected() (non più filtrato)
            → UI: cerchio colorato + "Connected"

Summary by CodeRabbit

  • New Features
    • Added support for Android managed-config (MDM) to deliver enforced settings into the app.
    • Managed settings are now reflected in the UI as locked/read-only, including split-tunnel mode and related options.
    • When device policies change, the app now triggers an immediate restart to apply updates, and the engine receives the latest managed policy overlay.
    • Added a managed-config schema and localized descriptions for the enforced fields.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b2e5bf1f-08ac-409d-acfa-9be712d8b2db

📥 Commits

Reviewing files that changed from the base of the PR and between 8442624 and cee5fa9.

📒 Files selected for processing (1)
  • netbird
🚧 Files skipped from review as they are similar to previous changes (1)
  • netbird

📝 Walkthrough

Walkthrough

Adds end-to-end Android MDM managed-configuration support. A new app_restrictions.xml schema with 17 restriction keys is registered in the manifest. MDMPolicyFetcher reads RestrictionsManager bundles and serializes them to JSON for the Go engine via EngineRunner. VPNService listens for ACTION_APPLICATION_RESTRICTIONS_CHANGED and triggers an immediate engine restart. AdvancedFragment locks managed settings in the UI. The netbird submodule is bumped.

Changes

Android MDM Managed-Configuration Support

Layer / File(s) Summary
MDM restriction schema and string resources
app/src/main/res/xml/app_restrictions.xml, app/src/main/res/values/strings.xml, app/src/main/res/values/arrays.xml, app/src/main/AndroidManifest.xml
Defines 17 restriction keys with types and defaults in app_restrictions.xml; adds all MDM string titles/descriptions to strings.xml; adds splitTunnelMode display/value arrays to arrays.xml; registers the schema via APP_RESTRICTIONS meta-data in the manifest.
MDMPolicyFetcher and GoMobile bridge registration
tool/src/main/java/io/netbird/client/tool/MDMPolicyFetcher.java, tool/src/main/java/io/netbird/client/tool/EngineRunner.java, app/src/main/java/io/netbird/client/MyApplication.java, netbird
Adds MDMPolicyFetcher (implements PolicyFetcher) that reads RestrictionsManager bundles and serializes them to JSON via bundleToJSON; EngineRunner installs it per goClient instance; MyApplication documents process-wide registration removal; submodule bumped for Go-side interface.
MDM policy change broadcast and immediate engine restart
tool/src/main/java/io/netbird/client/tool/EngineRestarter.java, tool/src/main/java/io/netbird/client/tool/VPNService.java
EngineRestarter gains requestRestartNow() to post restartRunnable immediately without debounce; VPNService registers mdmPolicyChangedReceiver for ACTION_APPLICATION_RESTRICTIONS_CHANGED in onCreate() and unregisters it in onDestroy(), routing events to requestRestartNow().
AdvancedFragment MDM UI enforcement
app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java
Adds applyMDMLocks() called after switch initialization; reads RestrictionsManager and calls lockSwitchIfManaged() per restriction key to force switch state and disable interaction; for preSharedKey, disables the input field, displays the redaction sentinel, and disables the save button.

Sequence Diagram(s)

sequenceDiagram
  participant AndroidOS
  participant VPNService
  participant EngineRestarter
  participant EngineRunner
  participant MDMPolicyFetcher
  participant RestrictionsManager

  AndroidOS->>VPNService: ACTION_APPLICATION_RESTRICTIONS_CHANGED
  VPNService->>EngineRestarter: requestRestartNow()
  EngineRestarter->>EngineRunner: restartRunnable (immediate)
  EngineRunner->>MDMPolicyFetcher: fetchJSON()
  MDMPolicyFetcher->>RestrictionsManager: getApplicationRestrictions()
  RestrictionsManager-->>MDMPolicyFetcher: Bundle
  MDMPolicyFetcher-->>EngineRunner: JSON string
  EngineRunner->>EngineRunner: apply MDM policy overlay and run
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • netbirdio/android-client#167: Modifies EngineRestarter debounce and cancellation logic in VPNService/EngineRunner — directly adjacent to the new requestRestartNow() method introduced in this PR.

Suggested reviewers

  • pappz
  • lixmal

Poem

🐰 Hop, hop! The MDM hare
Brings policy bundles with flair,
JSON from bundles it spins,
Restarts without debounce wins,
And locks your switches with care! 🔒

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Mdm integration' is vague and generic, using only an acronym without context about what is being integrated or the scope of changes. Consider a more descriptive title such as 'Add Android MDM policy integration for engine restart' that clearly conveys the primary functionality being added.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mdm_integration
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch mdm_integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@riccardomanfrin riccardomanfrin requested a review from pappz June 15, 2026 10:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java`:
- Around line 358-370: In the lockSwitchIfManaged method, reorder the operations
to call setEnabled(false) before setChecked(value) so that the
OnCheckedChangeListener does not fire during MDM lock application. Additionally,
add an early-exit guard at the beginning of each OnCheckedChangeListener
implementation (in initializeEngineConfigSwitches and the Rosenpass listeners)
that checks if the switch/control is enabled and returns immediately if it is
disabled, preventing unnecessary preference commits and state manipulations.
- Around line 10-12: Remove the duplicate import statement for android.view.View
in the AdvancedFragment.java file. The import android.view.View appears twice
consecutively in the import block and only one instance should be retained.
Delete the second occurrence of the duplicate import android.view.View
statement, keeping the first one.

In `@tool/src/main/java/io/netbird/client/tool/EngineRestarter.java`:
- Around line 318-325: The requestRestartNow() method can drop pending restart
requests when a restart is already in progress because it immediately posts the
runnable, which then clears the restartScheduled flag in restartEngine() and
exits early without replaying the request. Instead of directly posting the
runnable, check if a restart is currently in progress using isRestartInProgress,
and if so, queue the request rather than posting it immediately. Add a queue
data structure to track pending restart requests, and modify restartEngine() to
drain and process any queued requests when the current restart cycle completes
(regardless of success, error, or timeout). This ensures that MDM policy changes
are not dropped and converge deterministically by replaying queued requests
after each restart cycle finishes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 17b0a625-d9d1-424d-a04d-c05ce89c4860

📥 Commits

Reviewing files that changed from the base of the PR and between 0e81e82 and f41075d.

📒 Files selected for processing (11)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/io/netbird/client/MDMPolicyFetcher.java
  • app/src/main/java/io/netbird/client/MyApplication.java
  • app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java
  • app/src/main/res/values/arrays.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/xml/app_restrictions.xml
  • netbird
  • tool/src/main/java/io/netbird/client/tool/EngineRestarter.java
  • tool/src/main/java/io/netbird/client/tool/EngineRunner.java
  • tool/src/main/java/io/netbird/client/tool/VPNService.java

Comment on lines +10 to 12
import android.view.View;
import android.view.LayoutInflater;
import android.view.View;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Duplicate import of android.view.View.

Line 10 and line 12 both import android.view.View.

🧹 Proposed fix
 import android.widget.CompoundButton;
 import android.widget.EditText;
-import android.view.View;
 import android.view.LayoutInflater;
 import android.view.View;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import android.view.View;
import android.view.LayoutInflater;
import android.view.View;
import android.view.LayoutInflater;
import android.view.View;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java` around
lines 10 - 12, Remove the duplicate import statement for android.view.View in
the AdvancedFragment.java file. The import android.view.View appears twice
consecutively in the import block and only one instance should be retained.
Delete the second occurrence of the duplicate import android.view.View
statement, keeping the first one.

Comment on lines +358 to +370
private void lockSwitchIfManaged(android.os.Bundle restrictions, String key,
CompoundButton switchCtrl, View parentLayout) {
if (switchCtrl == null || !restrictions.containsKey(key)) {
return;
}
boolean value = restrictions.getBoolean(key);
switchCtrl.setChecked(value);
switchCtrl.setEnabled(false);
if (parentLayout != null) {
parentLayout.setEnabled(false);
parentLayout.setClickable(false);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Listener side-effects fire during MDM lock application.

setChecked(value) at line 364 triggers the OnCheckedChangeListener before setEnabled(false) runs at line 365. Every MDM-locked switch will execute its listener, causing:

  • Unnecessary goPreferences.commit() calls for each locked switch
  • For the Rosenpass switch, the listener also manipulates the permissive switch's enabled/checked state (lines 131-134), potentially conflicting with MDM enforcement of rosenpassPermissive

Fix by disabling the switch before setting its value, then add guards in listeners:

🔧 Proposed fix for lockSwitchIfManaged
     private void lockSwitchIfManaged(android.os.Bundle restrictions, String key,
                                      CompoundButton switchCtrl, View parentLayout) {
         if (switchCtrl == null || !restrictions.containsKey(key)) {
             return;
         }
         boolean value = restrictions.getBoolean(key);
+        switchCtrl.setEnabled(false);
         switchCtrl.setChecked(value);
-        switchCtrl.setEnabled(false);
         if (parentLayout != null) {
             parentLayout.setEnabled(false);
             parentLayout.setClickable(false);
         }
     }

Then add an early-exit guard to each listener (example for one switch):

         binding.switchDisableClientRoutes.setOnCheckedChangeListener((buttonView, isChecked) -> {
+            if (!buttonView.isEnabled()) return; // Skip writes when MDM-locked
             try {
                 goPreferences.setDisableClientRoutes(isChecked);
                 goPreferences.commit();
             } catch (Exception e) {
                 Log.e(LOGTAG, "Failed to set disable client routes", e);
             }
         });

Apply the same guard to all listeners in initializeEngineConfigSwitches() and the Rosenpass listeners.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java` around
lines 358 - 370, In the lockSwitchIfManaged method, reorder the operations to
call setEnabled(false) before setChecked(value) so that the
OnCheckedChangeListener does not fire during MDM lock application. Additionally,
add an early-exit guard at the beginning of each OnCheckedChangeListener
implementation (in initializeEngineConfigSwitches and the Rosenpass listeners)
that checks if the switch/control is enabled and returns immediately if it is
disabled, preventing unnecessary preference commits and state manipulations.

Comment on lines +318 to +325
public void requestRestartNow() {
Log.d(LOGTAG, "explicit restart requested (no debounce)");
synchronized (restartLock) {
restartScheduled = true;
handler.removeCallbacks(restartRunnable);
handler.post(restartRunnable);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Immediate MDM restart requests can be dropped during an in-flight restart.

If requestRestartNow() is called while a restart is already in progress, the posted runnable can execute, clear the scheduled flag in restartEngine(), then exit early on isRestartInProgress. That drops the newer policy-change request instead of replaying it after the current cycle.

Queue the pending request and drain it when the current restart completes (success/error/timeout) so MDM changes converge deterministically.

Suggested direction
+// keep a pending-restart intent if a request arrives mid-cycle
+private boolean restartRequestedDuringInFlight = false;

 public void requestRestartNow() {
     Log.d(LOGTAG, "explicit restart requested (no debounce)");
     synchronized (restartLock) {
-        restartScheduled = true;
-        handler.removeCallbacks(restartRunnable);
-        handler.post(restartRunnable);
+        restartScheduled = true;
+        if (isRestartInProgress) {
+            restartRequestedDuringInFlight = true;
+            return;
+        }
+        handler.removeCallbacks(restartRunnable);
+        handler.post(restartRunnable);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tool/src/main/java/io/netbird/client/tool/EngineRestarter.java` around lines
318 - 325, The requestRestartNow() method can drop pending restart requests when
a restart is already in progress because it immediately posts the runnable,
which then clears the restartScheduled flag in restartEngine() and exits early
without replaying the request. Instead of directly posting the runnable, check
if a restart is currently in progress using isRestartInProgress, and if so,
queue the request rather than posting it immediately. Add a queue data structure
to track pending restart requests, and modify restartEngine() to drain and
process any queued requests when the current restart cycle completes (regardless
of success, error, or timeout). This ensures that MDM policy changes are not
dropped and converge deterministically by replaying queued requests after each
restart cycle finishes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@netbird`:
- Line 1: The netbird submodule is pinned to a non-existent commit
(bec26d5a14e7ac6f1d97ff533bf5275577bb30db) in the remote repository, preventing
the netbird.aar artifact generation which is required for MDMPolicyFetcher.java
to access the PolicyFetcher interface. Update the submodule pointer in the
.gitmodules file and the submodule reference to point to a valid, existing
commit in the netbird repository. Verify the commit exists by checking the
remote repository at https://github.com/netbirdio/netbird.git before updating.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4add5cc5-94d8-421f-ade9-d4e6afef3432

📥 Commits

Reviewing files that changed from the base of the PR and between f41075d and 0b62139.

📒 Files selected for processing (1)
  • netbird

Comment thread netbird Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tool/src/main/java/io/netbird/client/tool/MDMPolicyFetcher.java (1)

60-64: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Recursively normalize array elements before emitting JSON.

bundleToJSON() recurses for direct Bundle values, but array elements are appended raw. If an array contains structured values, the JSON payload diverges from the expected Android→Go contract.

Suggested fix
 private static JSONObject bundleToJSON(Bundle bundle) throws JSONException {
     JSONObject obj = new JSONObject();
     for (String key : bundle.keySet()) {
         Object value = bundle.get(key);
         if (value instanceof Bundle) {
             obj.put(key, bundleToJSON((Bundle) value));
         } else if (value instanceof Object[]) {
             JSONArray arr = new JSONArray();
             for (Object item : (Object[]) value) {
-                arr.put(item);
+                if (item instanceof Bundle) {
+                    arr.put(bundleToJSON((Bundle) item));
+                } else {
+                    arr.put(item);
+                }
             }
             obj.put(key, arr);
         } else {
             obj.put(key, value);
         }
     }
     return obj;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tool/src/main/java/io/netbird/client/tool/MDMPolicyFetcher.java` around lines
60 - 64, In the `bundleToJSON()` method where Object arrays are handled, the
array elements are being appended directly without recursive normalization. For
each item in the array, check if it is a Bundle instance and recursively call
`bundleToJSON()` on it (just as is done for direct Bundle values), or append the
raw item if it is not a Bundle. This ensures that structured values nested
within arrays are properly converted to JSON according to the Android→Go
contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tool/src/main/java/io/netbird/client/tool/MDMPolicyFetcher.java`:
- Around line 60-64: In the `bundleToJSON()` method where Object arrays are
handled, the array elements are being appended directly without recursive
normalization. For each item in the array, check if it is a Bundle instance and
recursively call `bundleToJSON()` on it (just as is done for direct Bundle
values), or append the raw item if it is not a Bundle. This ensures that
structured values nested within arrays are properly converted to JSON according
to the Android→Go contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4725d539-9354-472b-8893-c588310d5ce9

📥 Commits

Reviewing files that changed from the base of the PR and between 0b62139 and 8442624.

📒 Files selected for processing (4)
  • app/src/main/java/io/netbird/client/MyApplication.java
  • netbird
  • tool/src/main/java/io/netbird/client/tool/EngineRunner.java
  • tool/src/main/java/io/netbird/client/tool/MDMPolicyFetcher.java
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/io/netbird/client/MyApplication.java

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.

1 participant